From ed4d79b7067832f7bfb8767040a60d051cef2a69 Mon Sep 17 00:00:00 2001 From: Profile Profile Date: Mon, 9 Mar 2026 20:51:08 +0300 Subject: [PATCH] add files inc --- api/coauthor.php | 5 +- functions.php | 1 + inc/action-scheduler-functions.php | 166 ++++ inc/action-scheduler-functions_15_10_25.php | 159 ++++ inc/add_adv_checked.php | 124 +++ inc/add_adv_checked_advonly.php | 46 ++ inc/adfox_on.php | 74 ++ inc/admin/auto_check.php | 38 + inc/admin/lock-terms.php | 807 ++++++++++++++++++++ inc/article-rss-feed.php | 271 +++++++ inc/generate_coauthors_cache.php | 195 +++++ inc/graphql.php | 363 +++++++++ inc/journal_issue.php | 196 +++++ inc/meta_keywords.php | 20 + inc/opensearch.php | 0 inc/popular-json.php | 43 ++ inc/realtime-debug.php | 222 ++++++ inc/realtimesqllogger.php | 511 +++++++++++++ inc/rossiya.php | 24 + inc/schedule_async_post_processing.php | 34 + inc/sql_result.php | 221 ++++++ inc/test_action_scheduler.php | 53 ++ inc/wp-cli-scheduler-commands.php | 159 ++++ 23 files changed, 3728 insertions(+), 4 deletions(-) create mode 100644 inc/action-scheduler-functions.php create mode 100644 inc/action-scheduler-functions_15_10_25.php create mode 100644 inc/add_adv_checked.php create mode 100644 inc/add_adv_checked_advonly.php create mode 100644 inc/adfox_on.php create mode 100644 inc/admin/auto_check.php create mode 100644 inc/admin/lock-terms.php create mode 100644 inc/article-rss-feed.php create mode 100644 inc/generate_coauthors_cache.php create mode 100644 inc/graphql.php create mode 100644 inc/journal_issue.php create mode 100644 inc/meta_keywords.php create mode 100644 inc/opensearch.php create mode 100644 inc/popular-json.php create mode 100644 inc/realtime-debug.php create mode 100644 inc/realtimesqllogger.php create mode 100644 inc/rossiya.php create mode 100644 inc/schedule_async_post_processing.php create mode 100644 inc/sql_result.php create mode 100644 inc/test_action_scheduler.php create mode 100644 inc/wp-cli-scheduler-commands.php diff --git a/api/coauthor.php b/api/coauthor.php index ec3ff6f..eccf078 100644 --- a/api/coauthor.php +++ b/api/coauthor.php @@ -47,11 +47,8 @@ add_action('rest_api_init', function() { 'photo' => $photo, 'photo_sizes' => $photo_sizes, - // Оставляем gravatar как запасной вариант, если фото нет - 'gravatar' => get_avatar_url($coauthor->user_email, ['size' => 192]), - // Для обратной совместимости - 'avatar' => $photo ?: get_avatar_url($coauthor->user_email, ['size' => 192]), + 'avatar' => $photo ?: '', 'url' => get_author_posts_url($coauthor->ID, $coauthor->user_nicename), 'type' => $coauthor->type ?? 'guest-author' diff --git a/functions.php b/functions.php index 746d2c5..2499b27 100644 --- a/functions.php +++ b/functions.php @@ -24,6 +24,7 @@ if ( defined( 'WP_CLI' ) && WP_CLI ) { require "inc/action-scheduler-functions.php"; require "inc/wp-cli-scheduler-commands.php"; require "inc/adfox_on.php"; // управление adfox + require "inc/generate_coauthors_cache.php"; // генерим кеш списка авторов } include "inc/get_cached_alm.php"; diff --git a/inc/action-scheduler-functions.php b/inc/action-scheduler-functions.php new file mode 100644 index 0000000..c4503de --- /dev/null +++ b/inc/action-scheduler-functions.php @@ -0,0 +1,166 @@ +post_status ) { + return false; + } + + $post_url = get_permalink( $post_id ); + $post_title = $post->post_title; + + $delay = 0; + + //google news + as_schedule_single_action( + time() + $delay, + 'process_sitemap_submission', + [], + 'sitemap_generation' + ); + + //IndexNow + as_schedule_single_action( + time() + 5, + 'process_indexnow_submission', + [ $post_id, $post_url, $post_title ], + 'post_publish_processing' + ); + + return true; + + } catch ( Exception $e ) { + // Логируем ошибку + error_log( "failed for post {$post_id}: " . $e->getMessage() ); + log_scheduler_activity( "failed for post {$post_id}: " . $e->getMessage() ); + + // Пробрасываем исключение для перевода задачи в Failed + throw $e; + } + +} + + +function log_scheduler_activity( $message, $post_id = null ) { + + $log_file = ABSPATH . 'scheduler.log'; + $log_file = '/var/www/profile/html/scheduler.log'; + + $log_entry = sprintf( + "[%s] %s%s\n", + current_time( 'Y-m-d H:i:s' ), + $post_id ? "Post {$post_id}: " : "", + $message + ); + + file_put_contents( $log_file, $log_entry, FILE_APPEND | LOCK_EX ); +} + + + +// generation google news php82 /usr/bin/wp eval-file test-theme-load.php --path=/var/www/profile/html +add_action('process_sitemap_submission', 'handle_sitemap_submission'); + +function handle_sitemap_submission() { + + $generator = new AK_Sitemap_Generator(); + $result = $generator->generate_news_sitemap([ 'profile_article', 'anew', 'yellow' ]); + + if ($result) { + error_log('News Sitemap generated successfully'); + } else { + error_log('News Sitemap generation failed'); + } + + return $result; + +} + +//index now +function process_indexnow_submission( $post_id, $post_url, $post_title ) { + + + try { + + // Проверяем флаг отправки + if ( get_post_meta( $post_id, '_indexnow_sent', true ) ) { + return; + } + + // Ваша логика отправки в IndexNow + $api_key = 'b1a2g3d4i8f6g7h8i9juyg0k11l12'; + //$domain = parse_url( home_url(), PHP_URL_HOST ); + $domain = 'https://profile.ru'; + + + $body = array( + 'host' => $domain, + 'key' => $api_key, + // 'keyLocation' => esc_url( home_url( '/' . $api_key . '.txt' ) ), // ❌ НЕ НУЖНО + 'urlList' => array( $post_url ) + ); + + $response = wp_remote_post( 'https://api.indexnow.org/IndexNow', array( + 'body' => wp_json_encode( $body ), + 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ), + 'timeout' => 30, + )); + + // ПРАВИЛЬНАЯ ПРОВЕРКА ОТВЕТА + if ( is_wp_error( $response ) ) { + log_scheduler_activity( "IndexNow WP Error for post {$post_id}: " . $response->get_error_message(), $post_id ); + return false; + } + + $response_code = wp_remote_retrieve_response_code( $response ); + $response_body = wp_remote_retrieve_body( $response ); + + // IndexNow возвращает 200/201 при успехе + if ( in_array( $response_code, array( 200, 201, 202 ) ) ) { + // Помечаем как успешно отправленный + update_post_meta( $post_id, '_indexnow_sent', current_time( 'mysql' ) ); + log_scheduler_activity( "IndexNow success {$post_url}", $post_id ); + return true; + } else { + log_scheduler_activity( "IndexNow failed {$post_url}. Status: {$response_code} | Response: {$response_body}", $post_id ); + return false; + } + + } catch ( Exception $e ) { + // Логируем ошибку + error_log( "IndexNow failed for post {$post_id}: " . $e->getMessage() ); + + echo $e->getMessage(); + + // Пробрасываем исключение для перевода задачи в Failed + throw $e; + } + + +} \ No newline at end of file diff --git a/inc/action-scheduler-functions_15_10_25.php b/inc/action-scheduler-functions_15_10_25.php new file mode 100644 index 0000000..615a4de --- /dev/null +++ b/inc/action-scheduler-functions_15_10_25.php @@ -0,0 +1,159 @@ +post_status ) { + return false; + } + + $post_url = get_permalink( $post_id ); + $post_title = $post->post_title; + + //google news + as_schedule_single_action( + time() + $delay, + 'process_sitemap_submission', + [], + 'sitemap_generation' + ); + + //IndexNow + as_schedule_single_action( + time() + 5, + 'process_indexnow_submission', + [ $post_id, $post_url, $post_title ], + 'post_publish_processing' + ); + + return true; + + } catch ( Exception $e ) { + // Логируем ошибку + error_log( "failed for post {$post_id}: " . $e->getMessage() ); + log_scheduler_activity( "failed for post {$post_id}: " . $e->getMessage() ); + + // Пробрасываем исключение для перевода задачи в Failed + throw $e; + } + +} + + +function log_scheduler_activity( $message, $post_id = null ) { + + $log_file = ABSPATH . 'scheduler.log'; + + $log_entry = sprintf( + "[%s] %s%s\n", + current_time( 'Y-m-d H:i:s' ), + $post_id ? "Post {$post_id}: " : "", + $message + ); + + file_put_contents( $log_file, $log_entry, FILE_APPEND | LOCK_EX ); +} + + + +// generation google news +add_action('process_sitemap_submission', 'handle_sitemap_submission'); + +function handle_sitemap_submission() { + $generator = new AK_Sitemap_Generator(); + $result = $generator->generate_news_sitemap([ 'profile_article', 'anew', 'yellow' ]); + + if ($result) { + error_log('News Sitemap generated successfully'); + } else { + error_log('News Sitemap generation failed'); + } + + return $result; +} + +//index now +function process_indexnow_submission( $post_id, $post_url, $post_title ) { + + + try { + + // Проверяем флаг отправки + if ( get_post_meta( $post_id, '_indexnow_sent', true ) ) { + return; + } + + // Ваша логика отправки в IndexNow + $api_key = 'b1a2g3d4i8f6g7h8i9juyg0k11l12'; + //$domain = parse_url( home_url(), PHP_URL_HOST ); + $domain = 'https://profile.ru'; + + + $body = array( + 'host' => $domain, + 'key' => $api_key, + // 'keyLocation' => esc_url( home_url( '/' . $api_key . '.txt' ) ), // ❌ НЕ НУЖНО + 'urlList' => array( $post_url ) + ); + + $response = wp_remote_post( 'https://api.indexnow.org/IndexNow', array( + 'body' => wp_json_encode( $body ), + 'headers' => array( 'Content-Type' => 'application/json; charset=utf-8' ), + 'timeout' => 30, + )); + + // ПРАВИЛЬНАЯ ПРОВЕРКА ОТВЕТА + if ( is_wp_error( $response ) ) { + log_scheduler_activity( "IndexNow WP Error for post {$post_id}: " . $response->get_error_message(), $post_id ); + return false; + } + + $response_code = wp_remote_retrieve_response_code( $response ); + $response_body = wp_remote_retrieve_body( $response ); + + // IndexNow возвращает 200/201 при успехе + if ( in_array( $response_code, array( 200, 201, 202 ) ) ) { + // Помечаем как успешно отправленный + update_post_meta( $post_id, '_indexnow_sent', current_time( 'mysql' ) ); + log_scheduler_activity( "IndexNow success {$post_url}", $post_id ); + return true; + } else { + log_scheduler_activity( "IndexNow failed {$post_url}. Status: {$response_code} | Response: {$response_body}", $post_id ); + return false; + } + + } catch ( Exception $e ) { + // Логируем ошибку + error_log( "IndexNow failed for post {$post_id}: " . $e->getMessage() ); + + echo $e->getMessage(); + + // Пробрасываем исключение для перевода задачи в Failed + throw $e; + } + + +} \ No newline at end of file diff --git a/inc/add_adv_checked.php b/inc/add_adv_checked.php new file mode 100644 index 0000000..3fa4391 --- /dev/null +++ b/inc/add_adv_checked.php @@ -0,0 +1,124 @@ +ID, '_is_advertisement', true); + wp_nonce_field('advertisement_nonce', 'advertisement_nonce_field'); + echo ''; + }, + ['anew', 'yellow', 'profile_article'], + 'side', + 'low' + ); + + // Метабокс для Аэрофлота (только для profile_article) + add_meta_box( + 'aeroflot_meta_box', + 'Лента Аэрофлота', + function($post) { + $no_aeroflot = get_post_meta($post->ID, '_no_aeroflot', true); + wp_nonce_field('aeroflot_nonce', 'aeroflot_nonce_field'); + echo ''; + }, + ['profile_article'], // Только для этого типа записей + 'side', + 'low' + ); +}); + +add_action('save_post', function($post_id) { + // Сохранение рекламного материала + if (isset($_POST['advertisement_nonce_field']) && wp_verify_nonce($_POST['advertisement_nonce_field'], 'advertisement_nonce')) { + if (!defined('DOING_AUTOSAVE') || !DOING_AUTOSAVE) { + if (current_user_can('edit_post', $post_id)) { + update_post_meta($post_id, '_is_advertisement', isset($_POST['is_advertisement']) ? '1' : '0'); + } + } + } + + // Сохранение настройки Аэрофлот (только для profile_article) + if (isset($_POST['aeroflot_nonce_field']) && wp_verify_nonce($_POST['aeroflot_nonce_field'], 'aeroflot_nonce')) { + if (!defined('DOING_AUTOSAVE') || !DOING_AUTOSAVE) { + if (current_user_can('edit_post', $post_id)) { + update_post_meta($post_id, '_no_aeroflot', isset($_POST['no_aeroflot']) ? '1' : '0'); + } + } + } +}); + +add_action('admin_head', function() { + echo ''; +}); \ No newline at end of file diff --git a/inc/add_adv_checked_advonly.php b/inc/add_adv_checked_advonly.php new file mode 100644 index 0000000..d692177 --- /dev/null +++ b/inc/add_adv_checked_advonly.php @@ -0,0 +1,46 @@ +ID, '_is_advertisement', true); + wp_nonce_field('advertisement_nonce', 'advertisement_nonce_field'); + echo ''; + }, + ['anew', 'yellow', 'profile_article'], + 'side', + 'low' + ); +}); + +add_action('save_post', function($post_id) { + if (!isset($_POST['advertisement_nonce_field']) || !wp_verify_nonce($_POST['advertisement_nonce_field'], 'advertisement_nonce')) return; + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; + if (!current_user_can('edit_post', $post_id)) return; + + update_post_meta($post_id, '_is_advertisement', isset($_POST['is_advertisement']) ? '1' : '0'); +}); + +add_action('admin_head', function() { + echo ''; +}); \ No newline at end of file diff --git a/inc/adfox_on.php b/inc/adfox_on.php new file mode 100644 index 0000000..114a70d --- /dev/null +++ b/inc/adfox_on.php @@ -0,0 +1,74 @@ +show_status(); + break; + case 'on': + $this->turn_on(); + break; + case 'off': + $this->turn_off(); + break; + case 'toggle': + $this->toggle(); + break; + default: + WP_CLI::error("Неизвестная команда: {$action}. Используйте: status, on, off, toggle"); + } + } + + private function show_status() { + $show_ad = get_option('show_ad', 0); + $status = ((int)$show_ad === 1) ? '✅ включена' : '❌ выключена'; + + WP_CLI::line("Текущий статус рекламы: {$status}"); + WP_CLI::line("Значение в базе: {$show_ad}"); + } + + private function turn_on() { + update_option('show_ad', 1); + WP_CLI::success('✅ Реклама включена'); + $this->show_status(); + } + + private function turn_off() { + update_option('show_ad', 0); + WP_CLI::success('❌ Реклама выключена'); + $this->show_status(); + } + + private function toggle() { + $current = get_option('show_ad', 0); + $new_value = ((int)$current === 1) ? 0 : 1; + $action = ($new_value === 1) ? '✅ включена' : '❌ выключена'; + + update_option('show_ad', $new_value); + WP_CLI::success("Реклама {$action}"); + $this->show_status(); + } +} + +// Регистрируем команду +if (defined('WP_CLI') && WP_CLI) { + WP_CLI::add_command('ad', 'Ad_Manager_Commands'); +} \ No newline at end of file diff --git a/inc/admin/auto_check.php b/inc/admin/auto_check.php new file mode 100644 index 0000000..bb32667 --- /dev/null +++ b/inc/admin/auto_check.php @@ -0,0 +1,38 @@ +base === 'post') { + ?> + + [ + 'background' => '#9e5a63', // фон для banned + 'color' => '#ffffff', // белый цвет для рамки + 'placeholder_color' => '#ffffff', // цвет placeholder для banned + 'allow_new_tags' => false // запрещаем новые теги для banned + ], + 'keys' => [ + 'background' => '#e8f2ff', // синий фон для keys + 'color' => '#2271b1', // синий цвет для рамки + 'placeholder_color' => '#2271b1', // цвет placeholder для keys + 'allow_new_tags' => true // разрешаем новые теги для keys + ], + 'post_tag' => [ + 'background' => '#31708e', // светлый фон для post_tag + 'color' => '#ffffff', // серый цвет для рамки + 'placeholder_color' => '#ffffff', // цвет placeholder для post_tag + 'allow_new_tags' => false // запрещаем новые теги для post_tag + ] + ]; + ?> + + + + + $taxonomy, + 'hide_empty' => false, + 'number' => 100, + ]; + + // Если есть поисковый запрос, добавляем поиск и сортировку по релевантности + if (!empty($search)) { + $args['search'] = $search; + $args['orderby'] = 'name'; + $args['order'] = 'ASC'; + } + + $terms = get_terms($args); + + $result = []; + foreach ($terms as $term) { + $result[] = [ + 'id' => $term->term_id, + 'text' => $term->name + ]; + } + + // Сортируем по релевантности, если есть поисковый запрос + if (!empty($search)) { + usort($result, function($a, $b) use ($search) { + $a_text = $a['text']; + $b_text = $b['text']; + $search_lower = strtolower($search); + + // Приоритет: точное совпадение + if (strtolower($a_text) === $search_lower && strtolower($b_text) !== $search_lower) { + return -1; + } + if (strtolower($b_text) === $search_lower && strtolower($a_text) !== $search_lower) { + return 1; + } + + // Приоритет: начинается с поискового запроса + $a_starts = stripos($a_text, $search) === 0; + $b_starts = stripos($b_text, $search) === 0; + + if ($a_starts && !$b_starts) { + return -1; + } + if ($b_starts && !$a_starts) { + return 1; + } + + // Приоритет: содержит поисковый запрос + $a_contains = stripos($a_text, $search) !== false; + $b_contains = stripos($b_text, $search) !== false; + + if ($a_contains && !$b_contains) { + return -1; + } + if ($b_contains && !$a_contains) { + return 1; + } + + // Если одинаковый приоритет - сортируем по алфавиту + return strcasecmp($a_text, $b_text); + }); + } + + wp_send_json($result); +}); + +// AJAX обработчик для получения терминов поста +add_action('wp_ajax_get_post_terms', function () { + $taxonomy = sanitize_key($_POST['taxonomy']); + $post_id = (int)$_POST['post_id']; + + $terms = wp_get_post_terms($post_id, $taxonomy); + + $result = []; + foreach ($terms as $term) { + $result[] = [ + 'id' => $term->term_id, + 'text' => $term->name + ]; + } + + wp_send_json($result); +}); + +// AJAX обработчик для получения популярных тегов +add_action('wp_ajax_get_popular_terms', function () { + $taxonomy = sanitize_key($_GET['taxonomy']); + + // Получаем популярные теги (с наибольшим количеством постов) + $terms = get_terms([ + 'taxonomy' => $taxonomy, + 'orderby' => 'count', + 'order' => 'DESC', + 'number' => 20, + 'hide_empty' => false, + ]); + + $result = []; + foreach ($terms as $term) { + $result[] = [ + 'id' => $term->term_id, + 'name' => $term->name, + 'count' => $term->count // Добавляем количество использований + ]; + } + + wp_send_json($result); +}); + +// Обработчик сохранения с исправленной логикой для новых тегов +add_action('save_post', function($post_id) { + // Проверяем права пользователя + if (!current_user_can('edit_post', $post_id)) return; + + // Убираем автосохранение + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; + + // Отладочная информация + error_log('Save post hook called for post: ' . $post_id); + error_log('POST data for keys: ' . ($_POST['keys_ids'] ?? 'not set')); + error_log('POST data for banned: ' . ($_POST['banned_ids'] ?? 'not set')); + error_log('POST data for post_tag: ' . ($_POST['post_tag_ids'] ?? 'not set')); + + $taxonomies = ['banned', 'keys', 'post_tag']; + + foreach ($taxonomies as $taxonomy) { + if (isset($_POST[$taxonomy . '_ids'])) { + $values = explode(',', $_POST[$taxonomy . '_ids']); + $term_ids = []; + + foreach ($values as $value) { + $value = trim($value); + if (empty($value)) continue; + + // Если значение НЕ число, это новый тег (только для keys) + if (!is_numeric($value) && $taxonomy === 'keys') { + // Это название нового тега + $term_name = $value; + if (!empty($term_name)) { + // Создаем новый терм + $new_term = wp_insert_term($term_name, $taxonomy); + if (!is_wp_error($new_term)) { + $term_ids[] = $new_term['term_id']; + error_log('Created new term: ' . $term_name . ' with ID: ' . $new_term['term_id']); + } else if ($new_term->get_error_code() === 'term_exists') { + // Если терм уже существует, получаем его ID + $existing_term = get_term_by('name', $term_name, $taxonomy); + if ($existing_term) { + $term_ids[] = $existing_term->term_id; + error_log('Term already exists: ' . $term_name . ' with ID: ' . $existing_term->term_id); + } + } else { + error_log('Error creating term: ' . $term_name . ' - ' . $new_term->get_error_message()); + } + } + } else { + // Существующий ID термина + $term_ids[] = intval($value); + } + } + + $term_ids = array_filter($term_ids); + if (!empty($term_ids)) { + wp_set_object_terms($post_id, $term_ids, $taxonomy, false); + error_log('Set terms for ' . $taxonomy . ': ' . implode(', ', $term_ids)); + } else { + // Если нет терминов, очищаем + wp_set_object_terms($post_id, [], $taxonomy, false); + error_log('Cleared terms for ' . $taxonomy); + } + } +} +}); \ No newline at end of file diff --git a/inc/article-rss-feed.php b/inc/article-rss-feed.php new file mode 100644 index 0000000..f39d52d --- /dev/null +++ b/inc/article-rss-feed.php @@ -0,0 +1,271 @@ +output_rss_feed($date); + exit; + } + } + + /** + * Получаем URL иконки сайта + */ +private function get_site_icon_url() { + $site_icon_id = get_option('site_icon'); + if ($site_icon_id) { + return wp_get_attachment_image_url($site_icon_id, 'full'); + } + + // Fallback на логотип или дефолтную иконку + $custom_logo_id = get_theme_mod('custom_logo'); + if ($custom_logo_id) { + return wp_get_attachment_image_url($custom_logo_id, 'full'); + } + + return home_url('/wp-admin/images/wordpress-logo.png'); +} + + +private function get_clean_post_content($post) { + + $content = apply_filters('the_content', $post->post_content); + + // Удаляем запрещенные для Турбо элементы + $content = preg_replace('/]*>.*?<\/script>/is', '', $content); + $content = preg_replace('/]*>.*?<\/style>/is', '', $content); + $content = preg_replace('/]*>.*?<\/iframe>/is', '', $content); + $content = preg_replace('/]*>.*?<\/form>/is', '', $content); + $content = preg_replace('/]*>/is', '', $content); + $content = preg_replace('/]*>.*?<\/button>/is', '', $content); + $content = preg_replace('/]*>.*?<\/select>/is', '', $content); + $content = preg_replace('/]*>.*?<\/textarea>/is', '', $content); + $content = str_replace('

', '', $content); + + // Удаляем определенные классы + $content = preg_replace('/]*class="[^"]*(ads|advert|banner|social|share|widget|comments)[^"]*"[^>]*>.*?<\/div>/is', '', $content); + + // Разрешаем только турбо-совместимые теги + $allowed_tags = [ + 'p' => ['class' => [], 'style' => []], + 'br' => [], + 'strong' => [], + 'em' => [], + 'b' => [], + 'i' => [], + 'ul' => [], + 'ol' => [], + 'li' => [], + 'h1' => [], + 'h2' => [], + 'h3' => [], + 'h4' => [], + 'h5' => [], + 'h6' => [], + 'a' => ['href' => [], 'title' => []], + 'img' => ['src' => [], 'alt' => [], 'title' => [], 'width' => [], 'height' => []], + 'blockquote' => [], + 'figure' => [], + 'header' => [], + ]; + + $content = wp_kses($content, $allowed_tags); + + return $content; +} + + /** + * Формируем XML ленту + */ +private function output_rss_feed($date) { + global $wpdb; + + // Определяем диапазон дат: 7 дней до указанной даты + $end_date = $date; // указанная дата + $start_date = date('Y-m-d', strtotime($date . ' -6 days')); // 7 дней назад (включая указанную дату) + + //такая лента формируется на каждую дату и содержит все статьи за 7 дней до указанной даты + $posts = $wpdb->get_results($wpdb->prepare(" + SELECT * + FROM {$wpdb->posts} + WHERE post_type = 'profile_article' + AND post_status = 'publish' + AND DATE(post_date) BETWEEN %s AND %s + AND post_password = '' + AND ID NOT IN ( + SELECT post_id + FROM {$wpdb->postmeta} + WHERE meta_key IN ('_no_aeroflot', '_is_advertisement', '_only_link_access', '_hide_on_website', '_hide_on_mainpage') + AND meta_value = '1' + ) + ORDER BY post_date DESC +", $start_date, $end_date)); + + // Конвертируем в объекты WP_Post + $posts = array_map(function($post) { + return new WP_Post($post); + }, $posts); + + // Устанавливаем заголовки + header('Content-Type: application/rss+xml; charset=' . get_option('blog_charset'), true); + + // Начинаем вывод XML + echo ''; + ?> + + + + <?php echo esc_xml(get_bloginfo('name')); ?> + + + + + + https://profile.ru + Информационное агенство Деловой журнал Профиль + + get_site_icon_url()); ?> + <?php echo esc_xml(get_bloginfo('name')); ?> + + + + + No posts for period <?php echo esc_xml($start_date); ?> to <?php echo esc_xml($end_date); ?> + + No posts found for this period + + no-posts- + + + ID); + $post_date = gmdate(DATE_RSS, strtotime($post->post_date_gmt)); + $excerpt = get_the_excerpt($post->ID); + $content = $this->get_clean_post_content($post); + $thumbnail = get_the_post_thumbnail_url($post->ID, 'large'); + // Получаем авторов из плагина Co-Authors + $authors = get_coauthors($post->ID); + ?> + + <?php echo esc_xml(get_the_title($post->ID)); ?> + + + + + display_name; + } + ?> + + + post_author); ?> + + + + + + + <?php echo esc_xml(get_the_title($post->ID)); ?> + + + + + + + <?php echo esc_xml(get_the_title($post->ID)); ?> + + + + + ID); + foreach ($categories as $category) { + echo '' . esc_xml($category->name) . ''; + } + ?> + + ]]> + + + + + + + + post_content); + $content = str_replace(']]>', ']]>', $content); + return $content; + } + + /** + * Активация - сбрасываем rewrite rules + */ + public static function activate() { + flush_rewrite_rules(); + } + + /** + * Деактивация - чистим rewrite rules + */ + public static function deactivate() { + flush_rewrite_rules(); + } +} + +// Инициализация +new CustomProfileArticleRSS(); + +// Хуки активации/деактивации +register_activation_hook(__FILE__, ['CustomProfileArticleRSS', 'activate']); +register_deactivation_hook(__FILE__, ['CustomProfileArticleRSS', 'deactivate']); \ No newline at end of file diff --git a/inc/generate_coauthors_cache.php b/inc/generate_coauthors_cache.php new file mode 100644 index 0000000..8f2ecaa --- /dev/null +++ b/inc/generate_coauthors_cache.php @@ -0,0 +1,195 @@ + false, + 'message' => 'Плагин CoAuthors Plus не активен' + ); + } + + global $wpdb, $coauthors_plus; + + $cache_file = get_coauthors_cache_file_path(); + $cache_data = array( + 'generated' => current_time('mysql'), + 'total' => 0, + 'authors' => array(), + 'by_display_name' => array(), // Индекс по display_name + 'by_last_name' => array(), // Индекс по фамилии + 'by_login' => array(), // Индекс по логину + 'by_nicename' => array() // Индекс по nicename + ); + + // Получаем всех соавторов через термины таксономии 'author' + $terms = get_terms(array( + 'taxonomy' => 'author', + 'hide_empty' => false, + 'number' => 0 // Получаем все + )); + + $authors = array(); + $processed_ids = array(); + + WP_CLI::line('Получение соавторов из терминов...'); + $progress = \WP_CLI\Utils\make_progress_bar('Обработка соавторов', count($terms)); + + foreach ($terms as $term) { + $coauthor = $coauthors_plus->get_coauthor_by('user_nicename', $term->slug); + + if ($coauthor && !in_array($coauthor->ID, $processed_ids)) { + $authors[] = $coauthor; + $processed_ids[] = $coauthor->ID; + } + $progress->tick(); + } + $progress->finish(); + + // Дополнительно: получаем авторов из постов за последние 2 года (на случай если в терминах не все) + WP_CLI::line('Проверка авторов в постах...'); + $two_years_ago = date('Y-m-d H:i:s', strtotime('-2 years')); + + $post_ids = $wpdb->get_col($wpdb->prepare( + "SELECT DISTINCT ID FROM {$wpdb->posts} + WHERE post_type IN ('post', 'profile_article') + AND post_status = 'publish' + AND post_date >= %s + ORDER BY post_date DESC + LIMIT 5000", + $two_years_ago + )); + + $post_progress = \WP_CLI\Utils\make_progress_bar('Поиск авторов в постах', count($post_ids)); + + foreach ($post_ids as $pid) { + foreach (get_coauthors($pid) as $ca) { + if (!in_array($ca->ID, $processed_ids)) { + $authors[] = $ca; + $processed_ids[] = $ca->ID; + } + } + $post_progress->tick(); + } + $post_progress->finish(); + + // Формируем структуру данных для кеша + WP_CLI::line('Формирование структуры кеша...'); + $total = count($authors); + + foreach ($authors as $author) { + $display_name = $author->display_name; + $last_name = extract_last_name($display_name); + + $author_data = array( + 'ID' => $author->ID, + 'display_name' => $display_name, + 'last_name' => $last_name, + 'user_login' => $author->user_login, + 'user_nicename' => $author->user_nicename, + 'type' => isset($author->type) ? $author->type : 'wpuser', + 'description' => isset($author->description) ? $author->description : '', + 'show_only_articles' => get_user_meta($author->ID, 'show_only_articles', true) + ); + + // Добавляем в основной массив + $cache_data['authors'][] = $author_data; + + // Индексируем по display_name (полное имя) + $display_name_key = sanitize_key_for_cache($display_name); + $cache_data['by_display_name'][$display_name_key] = $author_data; + + // Индексируем по фамилии + if (!empty($last_name)) { + $last_name_key = sanitize_key_for_cache($last_name); + if (!isset($cache_data['by_last_name'][$last_name_key])) { + $cache_data['by_last_name'][$last_name_key] = array(); + } + $cache_data['by_last_name'][$last_name_key][] = $author_data; + } + + // Индексируем по логину + if (!empty($author->user_login)) { + $login_key = sanitize_key_for_cache($author->user_login); + $cache_data['by_login'][$login_key] = $author_data; + } + + // Индексируем по nicename + if (!empty($author->user_nicename)) { + $nicename_key = sanitize_key_for_cache($author->user_nicename); + $cache_data['by_nicename'][$nicename_key] = $author_data; + } + } + + $cache_data['total'] = $total; + + // Сохраняем в файл + $written = file_put_contents($cache_file, json_encode($cache_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + if ($written === false) { + return array( + 'success' => false, + 'message' => "Ошибка при записи файла кеша: {$cache_file}" + ); + } + + return array( + 'success' => true, + 'message' => "Кеш соавторов успешно сгенерирован", + 'total' => $total, + 'file' => $cache_file + ); +} + +/** + * Извлекает фамилию из полного имени + * Предполагает, что фамилия - последнее слово + */ +function extract_last_name($full_name) { + $parts = explode(' ', trim($full_name)); + return end($parts); +} + +/** + * Очищает строку для использования в качестве ключа кеша + */ +function sanitize_key_for_cache($string) { + return mb_strtolower(trim(preg_replace('/\s+/', ' ', $string))); +} + +/** + * Возвращает путь к файлу кеша + */ +function get_coauthors_cache_file_path() { + $upload_dir = wp_upload_dir(); + return $upload_dir['basedir'] . '/coauthors-cache.json'; +} \ No newline at end of file diff --git a/inc/graphql.php b/inc/graphql.php new file mode 100644 index 0000000..e3d701f --- /dev/null +++ b/inc/graphql.php @@ -0,0 +1,363 @@ +ID == $author_id) continue; + + if (isset($coauthor->type) && $coauthor->type === 'guest-author') { + // Для гостевых авторов + $users[] = [ + '__typename' => 'GuestAuthor', + 'id' => $coauthor->user_nicename, + 'name' => $coauthor->display_name, + 'firstName' => $coauthor->first_name, + 'lastName' => $coauthor->last_name, + 'description' => $coauthor->description, + 'avatar' => get_avatar_url($coauthor->user_email, ['size' => 96]), + 'url' => get_author_posts_url($coauthor->ID, $coauthor->user_nicename), + 'type' => 'guest-author' + ]; + } else { + // Для обычных пользователей + $user = get_user_by('id', $coauthor->ID); + if ($user) { + $avatar_url = ''; + if (function_exists('get_avatar_url')) { + $avatar_url = get_avatar_url($user->ID, ['size' => 96]); + } + + $users[] = [ + '__typename' => 'User', + 'id' => 'user-' . $user->ID, + 'name' => $user->display_name, + 'firstName' => $user->first_name, + 'lastName' => $user->last_name, + 'description' => $user->description, + 'avatar' => $avatar_url, + 'url' => get_author_posts_url($user->ID), + 'databaseId' => $user->ID + ]; + } + } + } + + return $users; +} + + + + + + +add_filter( 'register_post_type_args', function( $args, $post_type ) { + + if ( 'profile_article' === $post_type ) { + $args['show_in_graphql'] = true; + $args['graphql_single_name'] = 'ProfileArticle'; + $args['graphql_plural_name'] = 'ProfileArticles'; + } + + if ( 'anew' === $post_type ) { + $args['show_in_graphql'] = true; + $args['graphql_single_name'] = 'ANew'; // или 'Anew' - смотрите комментарий ниже + $args['graphql_plural_name'] = 'ANews'; // или 'Anews' - смотрите комментарий ниже + } + + + return $args; +}, 10, 2 ); + +//цвет меню +add_action('graphql_register_types', function() { + + // Поле цвета для пунктов меню + register_graphql_field('MenuItem', 'menuItemColor', [ + 'type' => 'String', + 'description' => __('Custom color for menu item', 'your-textdomain'), + 'resolve' => function($menu_item) { + $color = get_post_meta($menu_item->databaseId, '_menu_item_color', true); + return !empty($color) ? $color : null; + } + ]); + + // Поле цвета для категорий + register_graphql_field( 'Category', 'color', [ + 'type' => 'String', + 'description' => __( 'Background color class for category badge', 'your-textdomain' ), + 'resolve' => function( $term ) { + $color = get_field( 'color', 'category_' . $term->term_id ); + return ! empty( $color ) ? $color : 'bg-blue'; + } + ] ); + + // Соавторы + +$post_types_with_coauthors = ['ProfileArticle', 'ANew']; // Исправлено: ANew +foreach ($post_types_with_coauthors as $post_type) { + register_graphql_field($post_type, 'coauthors', [ + 'type' => ['list_of' => 'User'], + 'description' => sprintf(__('Co-authors of the %s', 'your-textdomain'), $post_type), + 'resolve' => function($post_object) use ($post_type) { + $post_id = $post_object->databaseId; + return get_coauthors_for_graphql($post_id); + } + ]); +} + + +}); + + + +//вторчиный заг +add_action('graphql_register_types', function() { + + // Для ProfileArticle + register_graphql_field('ProfileArticle', 'secondaryTitle', [ + 'type' => 'String', + 'description' => __('Secondary title from Secondary Title plugin', 'your-textdomain'), + 'resolve' => function($post_object) { + $post_id = $post_object->databaseId; + + // Прямое получение поля, которое использует плагин + $secondary_title = get_post_meta($post_id, 'secondary_post_title', true); + + // Если пусто, проверяем через функцию плагина (если она существует) + if (empty($secondary_title) && function_exists('get_secondary_title')) { + $secondary_title = get_secondary_title($post_id); + } + + return !empty($secondary_title) ? $secondary_title : null; + } + ]); + + // Для Anews + register_graphql_field('Anews', 'secondaryTitle', [ + 'type' => 'String', + 'description' => __('Secondary title from Secondary Title plugin', 'your-textdomain'), + 'resolve' => function($post_object) { + $post_id = $post_object->databaseId; + + $secondary_title = get_post_meta($post_id, 'secondary_post_title', true); + + if (empty($secondary_title) && function_exists('get_secondary_title')) { + $secondary_title = get_secondary_title($post_id); + } + + return !empty($secondary_title) ? $secondary_title : null; + } + ]); +}); + + + + + +/** + * Регистрация colonItem для ACF Checkbox + */ +add_action('graphql_register_types', function() { + + // Регистрируем поле как Boolean + register_graphql_field('ProfileArticle', 'colonItem', [ + 'type' => 'Boolean', + 'description' => 'Флаг поста колонки', + 'resolve' => function($post) { + $value = get_field('colon_item', $post->ID); + + // ACF Checkbox возвращает массив или false + // Если checkbox отмечен, get_field вернет массив: ['true'] + // Если не отмечен - false или пустой массив + + if (is_array($value) && in_array('true', $value)) { + return true; + } + + return false; + } + ]); + + // Для типа Anew (если нужно) + register_graphql_field('Anew', 'colonItem', [ + 'type' => 'Boolean', + 'description' => 'Флаг поста колонки', + 'resolve' => function($post) { + $value = get_field('colon_item', $post->ID); + + if (is_array($value) && in_array('true', $value)) { + return true; + } + + return false; + } + ]); + + // Регистрируем where аргумент + register_graphql_field('RootQueryToProfileArticleConnectionWhereArgs', 'colonItemEquals', [ + 'type' => 'Boolean', + 'description' => 'Фильтровать по полю colonItem', + ]); + + // Для Anew + register_graphql_field('RootQueryToAnewConnectionWhereArgs', 'colonItemEquals', [ + 'type' => 'Boolean', + 'description' => 'Фильтровать по полю colonItem', + ]); +}); + +/** + * Фильтрация для ACF Checkbox + */ +add_filter('graphql_post_object_connection_query_args', function($query_args, $source, $args, $context, $info) { + + if (isset($args['where']['colonItemEquals'])) { + $colon_value = $args['where']['colonItemEquals']; + + if (!isset($query_args['meta_query'])) { + $query_args['meta_query'] = []; + } + + if ($colon_value === true) { + // Для ACF Checkbox используем LIKE с сериализованным значением + $query_args['meta_query'][] = [ + 'key' => 'colon_item', + 'value' => '"true"', // Ищем "true" внутри сериализованного массива + 'compare' => 'LIKE' + ]; + } else { + // Ищем посты без этого значения + $query_args['meta_query'][] = [ + 'relation' => 'OR', + [ + 'key' => 'colon_item', + 'value' => '"true"', + 'compare' => 'NOT LIKE' + ], + [ + 'key' => 'colon_item', + 'compare' => 'NOT EXISTS' + ] + ]; + } + } + + return $query_args; +}, 10, 5); + + + + + +/** + * Добавление поддержки фильтрации по mainItem для ProfileArticle + */ +add_action('graphql_register_types', function() { + + // Регистрируем ACF поле mainItem в GraphQL + register_graphql_field('ProfileArticle', 'mainItem', [ + 'type' => 'Boolean', + 'description' => 'Флаг главного поста', + 'resolve' => function($post) { + $value = get_field('main_item', $post->ID); + + // ACF Checkbox возвращает массив или false + // Если checkbox отмечен, get_field вернет массив: ['true'] + // Если не отмечен - false или пустой массив + + if (is_array($value) && in_array('true', $value)) { + return true; + } + + return false; + } + ]); + + // Регистрируем кастомный where аргумент для ProfileArticle + register_graphql_field('RootQueryToProfileArticleConnectionWhereArgs', 'mainItemEquals', [ + 'type' => 'Boolean', + 'description' => 'Фильтровать статьи профиля по полю mainItem', + ]); +}); + +/** + * Применяем фильтрацию через meta_query для mainItem + */ +add_filter('graphql_post_object_connection_query_args', function($query_args, $source, $args, $context, $info) { + + // Проверяем наличие аргумента mainItemEquals + if (isset($args['where']['mainItemEquals'])) { + $main_value = $args['where']['mainItemEquals']; + + if (!isset($query_args['meta_query'])) { + $query_args['meta_query'] = []; + } + + if ($main_value === true) { + // Для ACF Checkbox используем LIKE с сериализованным значением + $query_args['meta_query'][] = [ + 'key' => 'main_item', + 'value' => '"true"', // Ищем "true" внутри сериализованного массива + 'compare' => 'LIKE' + ]; + } else { + // Ищем посты без этого значения + $query_args['meta_query'][] = [ + 'relation' => 'OR', + [ + 'key' => 'main_item', + 'value' => '"true"', + 'compare' => 'NOT LIKE' + ], + [ + 'key' => 'main_item', + 'compare' => 'NOT EXISTS' + ] + ]; + } + } + + return $query_args; +}, 10, 5); + + + + + + +// авторы +add_action('graphql_register_types', function() { + // Добавляем аргумент coauthorLogin + register_graphql_field('RootQueryToContentNodeConnectionWhereArgs', 'coauthorLogin', [ + 'type' => 'String', + 'description' => __('Filter by coauthor login (nicename)', 'textdomain'), + ]); +}); + +add_filter('graphql_post_object_connection_query_args', function($query_args, $source, $args, $context, $info) { + // Фильтр по логину + if (isset($args['where']['coauthorLogin']) && !empty($args['where']['coauthorLogin'])) { + $login = sanitize_user($args['where']['coauthorLogin']); + + // Находим пользователя по логину + $user = get_user_by('login', $login); + if ($user) { + $query_args['author'] = $user->ID; + } + } + + // Фильтр по имени (если нужен) + if (isset($args['where']['coauthorName']) && !empty($args['where']['coauthorName'])) { + $query_args['author_name'] = sanitize_title($args['where']['coauthorName']); + } + + return $query_args; +}, 10, 5); \ No newline at end of file diff --git a/inc/journal_issue.php b/inc/journal_issue.php new file mode 100644 index 0000000..dc41b2a --- /dev/null +++ b/inc/journal_issue.php @@ -0,0 +1,196 @@ + 'Номера изданий', + 'singular_name' => 'Номер издания', + 'menu_name' => 'Номера изданий', + 'all_items' => 'Все номера', + 'add_new' => 'Добавить новый', + 'add_new_item' => 'Добавить новый номер', + 'edit_item' => 'Редактировать номер', + 'new_item' => 'Новый номер', + 'view_item' => 'Просмотреть номер', + 'search_items' => 'Поиск номеров', + ); + $args = array( + 'label' => 'journal_issue', + 'labels' => $labels, + 'description' => 'Архивы номеров журналов', + 'public' => true, + 'show_in_menu' => true, + 'menu_icon' => 'dashicons-book-alt', + 'supports' => array('title', 'thumbnail', 'editor'), + 'has_archive' => false, + ); + register_post_type('journal_issue', $args); + + // Включаем поддержку миниатюр (на всякий случай) + add_post_type_support('journal_issue', 'thumbnail'); +} +add_action('init', 'create_journal_issue_cpt'); + +// Создаем скрытую Таксономию только для связи (без пункта меню) +add_action('init', function() { + $labels = array( + 'name' => 'Номера изданий', + 'singular_name' => 'Номер издания', + ); + $args = array( + 'labels' => $labels, + 'hierarchical' => true, + 'public' => false, + 'show_ui' => false, // Полностью скрываем из админки + 'show_admin_column' => true, + 'show_in_rest' => false, + 'capabilities' => array( + 'manage_terms' => 'manage_options', + 'edit_terms' => 'manage_options', + 'delete_terms' => 'manage_options', + 'assign_terms' => 'edit_posts', + ), + 'meta_box_cb' => false, + ); + + register_taxonomy('article_journal_issue', array('profile_article'), $args); +}); + +// Заменяем стандартный метабокс на выпадающий список +add_action('admin_menu', function() { + add_meta_box('journal_issue_dropdown', 'Номер издания', 'custom_journal_issue_dropdown', 'profile_article', 'side', 'default'); +}); + +// Выпадающий список для выбора номера +function custom_journal_issue_dropdown($post) { + // Получаем номера изданий из CPT journal_issue + $journal_issues = get_posts(array( + 'post_type' => 'journal_issue', + 'numberposts' => -1, + 'orderby' => 'title', + 'order' => 'DESC', + 'post_status' => 'publish' + )); + + // Получаем выбранный термин + $selected_terms = wp_get_object_terms($post->ID, 'article_journal_issue', array('fields' => 'names')); + $selected_name = !empty($selected_terms) ? $selected_terms[0] : ''; + + echo ''; + echo '

Выберите один номер издания

'; + echo '

Управление номерами изданий

'; +} + +// Обрабатываем сохранение выбранного номера +add_action('save_post_profile_article', function($post_id) { + // Проверяем автосохранение + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; + + // Проверяем права пользователя + if (!current_user_can('edit_post', $post_id)) return; + + // Обрабатываем выбор из выпадающего списка + if (isset($_POST['journal_issue_selected'])) { + $selected_title = sanitize_text_field($_POST['journal_issue_selected']); + + if (empty($selected_title)) { + // Если выбран пустой вариант - удаляем все термины + wp_set_object_terms($post_id, array(), 'article_journal_issue'); + } else { + // Создаем или получаем термин на основе выбранного номера + $term = term_exists($selected_title, 'article_journal_issue'); + if (!$term) { + $term = wp_insert_term($selected_title, 'article_journal_issue'); + } + + if (!is_wp_error($term)) { + wp_set_object_terms($post_id, array($term['term_id']), 'article_journal_issue'); + } + } + } +}); + +// Добавляем быстрый выбор в таблице постов (колонка) +add_filter('manage_profile_article_posts_columns', function($columns) { + $columns['journal_issue'] = 'Номер издания'; + return $columns; +}); + +add_action('manage_profile_article_posts_custom_column', function($column, $post_id) { + if ($column === 'journal_issue') { + $terms = get_the_terms($post_id, 'article_journal_issue'); + if ($terms && !is_wp_error($terms)) { + foreach ($terms as $term) { + echo '' . $term->name . ''; + } + } else { + echo ''; + } + } +}, 10, 2); + +// Функция для автоматической синхронизации терминов при создании/изменении номера +function sync_journal_issue_term($post_id) { + // Проверяем, что это номер издания + if (get_post_type($post_id) !== 'journal_issue') return; + + // Пропускаем автосохранение + if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; + + $issue = get_post($post_id); + if ($issue && $issue->post_status === 'publish') { + // Создаем или обновляем термин + $term = term_exists($issue->post_title, 'article_journal_issue'); + if (!$term) { + wp_insert_term($issue->post_title, 'article_journal_issue'); + } else { + // Обновляем название термина, если изменилось название номера + wp_update_term($term['term_id'], 'article_journal_issue', array( + 'name' => $issue->post_title + )); + } + } +} + +// Синхронизируем при сохранении номера издания +add_action('save_post_journal_issue', 'sync_journal_issue_term'); + +// Удаляем термин при удалении номера издания +function delete_journal_issue_term($post_id) { + if (get_post_type($post_id) !== 'journal_issue') return; + + $issue = get_post($post_id); + if ($issue) { + $term = term_exists($issue->post_title, 'article_journal_issue'); + if ($term) { + wp_delete_term($term['term_id'], 'article_journal_issue'); + } + } +} + +add_action('before_delete_post', 'delete_journal_issue_term'); + +// Инициализация существующих номеров при активации +function init_existing_journal_issues() { + $journal_issues = get_posts(array( + 'post_type' => 'journal_issue', + 'numberposts' => -1, + 'post_status' => 'publish' + )); + + foreach ($journal_issues as $issue) { + $term = term_exists($issue->post_title, 'article_journal_issue'); + if (!$term) { + wp_insert_term($issue->post_title, 'article_journal_issue'); + } + } +} +register_activation_hook(__FILE__, 'init_existing_journal_issues'); \ No newline at end of file diff --git a/inc/meta_keywords.php b/inc/meta_keywords.php new file mode 100644 index 0000000..453ba33 --- /dev/null +++ b/inc/meta_keywords.php @@ -0,0 +1,20 @@ +name; + } + + echo '' . "\n"; + } + } +}); diff --git a/inc/opensearch.php b/inc/opensearch.php new file mode 100644 index 0000000..e69de29 diff --git a/inc/popular-json.php b/inc/popular-json.php new file mode 100644 index 0000000..14af983 --- /dev/null +++ b/inc/popular-json.php @@ -0,0 +1,43 @@ +id; }, + json_decode(get_option("ppp_options") ?: '[]') + ); + + $posts = get_posts([ + "post_type" => ["anew", "yellow"], + "post_status" => "publish", + "posts_per_page" => 20, + "ignore_sticky_posts" => true, + "post__in" => $ids, + "orderby" => "post__in", + "meta_query" => [[ + "key" => "_thumbnail_id", + "compare" => "EXISTS" + ]] + ]); + + $data = array_map(function($post) { + $thumbnail = get_the_post_thumbnail_url($post->ID, 'thumb-264'); + + return [ + 'id' => $post->ID, + 'title' => get_the_title($post->ID), + 'link' => get_permalink($post->ID), + 'thumbnail' => $thumbnail ?: wp_get_attachment_image_url(1357368, 'thumb-264'), + 'alt' => get_the_title($post->ID) + ]; + + }, $posts); + + // Создаем директорию если не существует + if (!file_exists($json_path)) { + wp_mkdir_p($json_path); + } + + $json_file = $json_path.'popular'; + + file_put_contents($json_file, json_encode($data, JSON_UNESCAPED_UNICODE)); \ No newline at end of file diff --git a/inc/realtime-debug.php b/inc/realtime-debug.php new file mode 100644 index 0000000..58ae947 --- /dev/null +++ b/inc/realtime-debug.php @@ -0,0 +1,222 @@ +log_file = WP_CONTENT_DIR . '/realtime-debug.log'; + $this->request_id = uniqid('req_', true); + $this->start_time = microtime(true); + + // Очищаем файл при инициализации + $this->clean_log_file(); + + $this->init(); + } + +/** + * Очистка лог-файла с проверками + */ +private function clean_log_file() { + // Если файл не существует, создаем его + if (!file_exists($this->log_file)) { + if (touch($this->log_file)) { + chmod($this->log_file, 0644); + $this->log_message("Создан новый лог-файл", 'SYSTEM'); + } else { + error_log('Не удалось создать файл realtime-debug.log'); + return; + } + } + + // Проверяем доступность для записи + if (!is_writable($this->log_file)) { + error_log('Файл realtime-debug.log недоступен для записи'); + return; + } + + // Очищаем файл + if (file_put_contents($this->log_file, '') === false) { + error_log('Не удалось очистить файл realtime-debug.log'); + } else { + $this->log_message("Лог-файл очищен", 'SYSTEM'); + } +} + + private function init() { + // Начинаем логирование как можно раньше + add_action('plugins_loaded', [$this, 'log_plugins_loaded'], 1); + add_action('setup_theme', [$this, 'log_setup_theme'], 1); + add_action('after_setup_theme', [$this, 'log_after_setup_theme'], 1); + add_action('init', [$this, 'log_init'], 1); + + // Логируем все основные этапы + add_action('wp_loaded', [$this, 'log_wp_loaded']); + add_action('parse_query', [$this, 'log_parse_query']); + add_action('pre_get_posts', [$this, 'log_pre_get_posts']); + add_action('wp', [$this, 'log_wp']); + + // Логируем SQL запросы + add_filter('query', [$this, 'log_sql_query']); + + // Логируем шаблоны + add_filter('template_include', [$this, 'log_template_include'], 9999); + + // Логируем завершение + add_action('shutdown', [$this, 'log_shutdown'], 9999); + + // Логируем хуки в реальном времени + $this->setup_hook_logging(); + } + + public function setup_hook_logging() { + $important_hooks = [ + 'template_redirect', + 'get_header', + 'wp_head', + 'the_post', + 'loop_start', + 'loop_end', + 'get_sidebar', + 'get_footer', + 'wp_footer', + 'admin_bar_menu', + 'wp_enqueue_scripts', + 'wp_print_styles', + 'wp_print_scripts' + ]; + + foreach ($important_hooks as $hook) { + add_action($hook, function() use ($hook) { + $this->log_hook($hook); + }, 1); + } + } + + private function log_message($message, $level = 'INFO') { + $timestamp = microtime(true); + $elapsed = round(($timestamp - $this->start_time) * 1000, 2); + $memory = memory_get_usage(true); + + $log = sprintf( + "[%s] %s | %6.2fms | %8s | %s | %s\n", + date('H:i:s'), + $this->request_id, + $elapsed, + size_format($memory, 0), + $level, + $message + ); + + file_put_contents($this->log_file, $log, FILE_APPEND); + } + + public function log_plugins_loaded() { + $this->log_message('PLUGINS_LOADED - Плагины загружены'); + } + + public function log_setup_theme() { + $this->log_message('SETUP_THEME - Начинается загрузка темы'); + } + + public function log_after_setup_theme() { + $this->log_message('AFTER_SETUP_THEME - Тема загружена'); + } + + public function log_init() { + $this->log_message('INIT - WordPress инициализирован'); + } + + public function log_wp_loaded() { + $this->log_message('WP_LOADED - WordPress полностью загружен'); + } + + public function log_parse_query() { + global $wp_query; + $this->log_message(sprintf('PARSE_QUERY - Запрос: %s', $wp_query->query_vars['pagename'] ?? 'main')); + } + + public function log_pre_get_posts($query) { + if ($query->is_main_query()) { + $this->log_message('PRE_GET_POSTS - Основной запрос к posts'); + } + } + + public function log_wp() { + $this->log_message('WP - Запрос обработан, готовим данные для шаблона'); + } + + public function log_sql_query($query) { + $trimmed = trim($query); + if (!empty($trimmed) && !str_starts_with($trimmed, '/*')) { + //$short_query = substr($trimmed, 0, 150); + //if (strlen($trimmed) > 150) { + // $short_query .= '...'; + // } + $this->log_message("SQL: {$query}", 'SQL'); + } + return $query; + } + + public function log_hook($hook) { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 4); + $caller = $this->get_caller_info($backtrace); + $this->log_message("HOOK: {$hook} → {$caller}", 'HOOK'); + } + + public function log_template_include($template) { + $template_name = basename($template); + $this->log_message("TEMPLATE: {$template_name}", 'TEMPLATE'); + return $template; + } + + public function log_shutdown() { + $total_time = round((microtime(true) - $this->start_time) * 1000, 2); + $peak_memory = memory_get_peak_usage(true); + + $this->log_message("SHUTDOWN - Завершение работы ({$total_time}ms, " . + size_format($peak_memory) . ")", 'FINISH'); + } + + private function get_caller_info($backtrace) { + foreach ($backtrace as $trace) { + if (isset($trace['file']) && + !str_contains($trace['file'], 'wp-includes') && + !str_contains($trace['file'], 'wp-admin')) { + $file = basename($trace['file']); + $line = $trace['line'] ?? 'unknown'; + return "{$file}:{$line}"; + } + } + return 'unknown'; + } +} + +// Инициализируем логгер только если включен дебаг +//if (defined('WP_DEBUG') && WP_DEBUG) { + RealtimePageLogger::get_instance(); +//} + +// Функция для очистки лога +add_action('wp_ajax_clear_realtime_log', 'clear_realtime_debug_log'); +function clear_realtime_debug_log() { + if (current_user_can('administrator')) { + file_put_contents(WP_CONTENT_DIR . '/realtime-debug.log', ''); + echo "Realtime log cleared!"; + } + wp_die(); +} \ No newline at end of file diff --git a/inc/realtimesqllogger.php b/inc/realtimesqllogger.php new file mode 100644 index 0000000..d4acf4d --- /dev/null +++ b/inc/realtimesqllogger.php @@ -0,0 +1,511 @@ +log_file = WP_CONTENT_DIR . '/sql-debug.log'; + $this->request_id = uniqid('req_', true); + $this->start_time = microtime(true); + + $this->init_log_file(); + $this->init(); + } + + private function init_log_file() { + if (!is_writable(WP_CONTENT_DIR)) { + $this->log_enabled = false; + error_log('Директория wp-content недоступна для записи'); + return; + } + + if (!file_exists($this->log_file)) { + if (@touch($this->log_file)) { + @chmod($this->log_file, 0644); + } + } + + // Очищаем файл при каждой инициализации + @file_put_contents($this->log_file, ''); + } + + private function init() { + if (!$this->log_enabled) return; + + // Трекинг местоположения в WordPress + $this->setup_location_tracking(); + + // Перехватываем запрос ДО выполнения + add_filter('query', [$this, 'log_query_before_execution'], 1, 1); + + // Логируем результат выполнения + add_filter('query', [$this, 'log_query_after_execution'], 9999, 2); + + // Логируем ошибки запросов + add_action('wpdb_error', [$this, 'log_query_error'], 10, 2); + + add_action('shutdown', [$this, 'log_pending_queries'], 9999); + } + + /** + * Настраиваем трекинг местоположения в WordPress + */ + private function setup_location_tracking() { + // Основные хуки WordPress для определения местоположения + $location_hooks = [ + 'plugins_loaded' => 'PLUGINS_LOADED', + 'setup_theme' => 'SETUP_THEME', + 'after_setup_theme' => 'AFTER_SETUP_THEME', + 'init' => 'INIT', + 'wp_loaded' => 'WP_LOADED', + 'parse_query' => 'PARSE_QUERY', + 'pre_get_posts' => 'PRE_GET_POSTS', + 'wp' => 'WP_MAIN_QUERY', + 'template_redirect' => 'TEMPLATE_REDIRECT', + 'get_header' => 'HEADER', + 'wp_head' => 'WP_HEAD', + 'loop_start' => 'LOOP_START', + 'the_post' => 'THE_POST', + 'loop_end' => 'LOOP_END', + 'get_sidebar' => 'SIDEBAR', + 'get_footer' => 'FOOTER', + 'wp_footer' => 'WP_FOOTER', + 'shutdown' => 'SHUTDOWN' + ]; + + foreach ($location_hooks as $hook => $location) { + add_action($hook, function() use ($location) { + $this->current_location = $location; + $this->log_message("📍 LOCATION: {$location}", 'LOCATION', false); + }, 1); + } + + // Специальные хуки для контента + add_action('the_content', function($content) { + $this->current_location = 'THE_CONTENT'; + return $content; + }); + + add_action('the_title', function($title) { + $this->current_location = 'THE_TITLE'; + return $title; + }); + + add_action('the_excerpt', function($excerpt) { + $this->current_location = 'THE_EXCERPT'; + return $excerpt; + }); + } + + /** + * Перехватываем запрос ДО выполнения + */ + public function log_query_before_execution($query) { + if (!$this->log_enabled) return $query; + + $trimmed = trim($query); + if (empty($trimmed)) return $query; + + $query_hash = md5($query . microtime(true)); + $backtrace_info = $this->get_detailed_caller_info(); + + // Сохраняем запрос как pending + $this->pending_queries[$query_hash] = [ + 'query' => $query, + 'start_time' => microtime(true), + 'backtrace' => $backtrace_info['short'], + 'detailed_backtrace' => $backtrace_info['full'], + 'component' => $this->get_component_info($backtrace_info['file']), + 'location' => $this->current_location, + 'status' => 'PENDING' + ]; + + $this->log_message( + "🚦 QUERY QUEUED: " . $this->shorten_query($query), + 'SQL-QUEUE', + false + ); + + $this->log_message( + " 📍 Source: " . $backtrace_info['short'], + 'SQL-QUEUE', + false + ); + + $this->log_message( + " 🏷️ Component: " . $this->pending_queries[$query_hash]['component'], + 'SQL-QUEUE', + false + ); + + $this->log_message( + " 🏠 Location: " . $this->current_location, + 'SQL-QUEUE', + false + ); + + return $query; + } + + /** + * Логируем запрос ПОСЛЕ выполнения + */ + public function log_query_after_execution($query, $result = null) { + if (!$this->log_enabled) return $query; + + $trimmed = trim($query); + if (empty($trimmed)) return $query; + + $query_hash = md5($query . microtime(true)); + $execution_time = 0; + $caller_info = ''; + $component_info = ''; + $location_info = ''; + + // Находим соответствующий pending запрос + foreach ($this->pending_queries as $hash => $pending) { + if ($pending['query'] === $query) { + $execution_time = microtime(true) - $pending['start_time']; + $this->pending_queries[$hash]['status'] = 'COMPLETED'; + $this->pending_queries[$hash]['execution_time'] = $execution_time; + $caller_info = $pending['backtrace']; + $component_info = $pending['component']; + $location_info = $pending['location']; + break; + } + } + + $status = $result === false ? 'FAILED' : 'COMPLETED'; + $time_ms = round($execution_time * 1000, 2); + + $this->log_message( + "✅ QUERY {$status}: {$time_ms}ms - " . $this->shorten_query($query), + $status === 'FAILED' ? 'SQL-ERROR' : 'SQL-DONE', + false + ); + + $this->log_message( + " 📍 Source: " . $caller_info, + $status === 'FAILED' ? 'SQL-ERROR' : 'SQL-DONE', + false + ); + + $this->log_message( + " 🏷️ Component: " . $component_info, + $status === 'FAILED' ? 'SQL-ERROR' : 'SQL-DONE', + false + ); + + $this->log_message( + " 🏠 Location: " . $location_info, + $status === 'FAILED' ? 'SQL-ERROR' : 'SQL-DONE', + false + ); + + // Для медленных запросов добавляем дополнительную информацию + if ($time_ms > 100 && $status !== 'FAILED') { + $this->log_message( + " ⚠️ SLOW QUERY: {$time_ms}ms - consider optimization", + 'SQL-SLOW', + false + ); + } + + return $query; + } + + /** + * Логируем ошибки запросов + */ + public function log_query_error($error, $query) { + if (!$this->log_enabled) return; + + $this->log_message( + "❌ QUERY ERROR: " . $error, + 'SQL-ERROR', + false + ); + + $this->log_message( + " 💥 Failed query: " . $this->shorten_query($query), + 'SQL-ERROR', + false + ); + + // Находим информацию о вызывающем коде для ошибочного запроса + foreach ($this->pending_queries as $pending) { + if ($pending['query'] === $query) { + $this->log_message( + " 📍 Source: " . $pending['backtrace'], + 'SQL-ERROR', + false + ); + + $this->log_message( + " 🏷️ Component: " . $pending['component'], + 'SQL-ERROR', + false + ); + + $this->log_message( + " 🏠 Location: " . $pending['location'], + 'SQL-ERROR', + false + ); + break; + } + } + } + + /** + * Логируем зависшие запросы + */ + public function log_pending_queries() { + if (!$this->log_enabled || empty($this->pending_queries)) return; + + $current_time = microtime(true); + $hung_queries = 0; + + foreach ($this->pending_queries as $hash => $query_info) { + if ($query_info['status'] === 'PENDING') { + $hang_time = round(($current_time - $query_info['start_time']) * 1000, 2); + $hung_queries++; + + $this->log_message( + "⚠️ HUNG QUERY: {$hang_time}ms - " . $this->shorten_query($query_info['query']), + 'SQL-HUNG', + false + ); + + $this->log_message( + " 📍 Source: " . $query_info['backtrace'], + 'SQL-HUNG', + false + ); + + $this->log_message( + " 🏷️ Component: " . $query_info['component'], + 'SQL-HUNG', + false + ); + + $this->log_message( + " 🏠 Location: " . $query_info['location'], + 'SQL-HUNG', + false + ); + + $this->log_message( + " 🔍 Full backtrace:\n" . $query_info['detailed_backtrace'], + 'SQL-HUNG', + false + ); + } + } + + if ($hung_queries > 0) { + $this->log_message( + "🔴 Found {$hung_queries} hung queries!", + 'SQL-HUNG', + false + ); + } + + // Логируем статистику по всем запросам + $this->log_query_statistics(); + } + + /** + * Логируем статистику выполнения запросов + */ + private function log_query_statistics() { + $total_queries = count($this->pending_queries); + $completed_queries = 0; + $failed_queries = 0; + $total_time = 0; + + $queries_by_location = []; + $queries_by_component = []; + + foreach ($this->pending_queries as $query) { + // Статистика по местоположению + $location = $query['location']; + if (!isset($queries_by_location[$location])) { + $queries_by_location[$location] = 0; + } + $queries_by_location[$location]++; + + // Статистика по компонентам + $component = $query['component']; + if (!isset($queries_by_component[$component])) { + $queries_by_component[$component] = 0; + } + $queries_by_component[$component]++; + + if ($query['status'] === 'COMPLETED') { + $completed_queries++; + $total_time += $query['execution_time'] ?? 0; + } elseif ($query['status'] === 'FAILED') { + $failed_queries++; + } + } + + $avg_time = $completed_queries > 0 ? round(($total_time / $completed_queries) * 1000, 2) : 0; + + $this->log_message( + "📊 STATS: Total: {$total_queries} | Completed: {$completed_queries} | " . + "Failed: {$failed_queries} | Avg: {$avg_time}ms", + 'STATS', + false + ); + + // Статистика по местоположению + if (!empty($queries_by_location)) { + $this->log_message("📊 QUERIES BY LOCATION:", 'STATS', false); + arsort($queries_by_location); + foreach ($queries_by_location as $location => $count) { + $this->log_message(" {$location}: {$count} queries", 'STATS', false); + } + } + + // Статистика по компонентам + if (!empty($queries_by_component)) { + $this->log_message("📊 QUERIES BY COMPONENT:", 'STATS', false); + arsort($queries_by_component); + foreach ($queries_by_component as $component => $count) { + $this->log_message(" {$component}: {$count} queries", 'STATS', false); + } + } + } + + private function log_message($message, $level = 'INFO', $check_enabled = true) { + if ($check_enabled && !$this->log_enabled) return; + + $timestamp = microtime(true); + $elapsed = round(($timestamp - $this->start_time) * 1000, 2); + + $log = sprintf( + "[%s] %s | %6.2fms | %-12s | %s\n", + date('H:i:s'), + substr($this->request_id, -6), + $elapsed, + $level, + $message + ); + + @file_put_contents($this->log_file, $log, FILE_APPEND | LOCK_EX); + } + + /** + * Получаем детальную информацию о вызывающем коде + */ + private function get_detailed_caller_info() { + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 15); + $short_info = 'unknown'; + $full_info = ''; + $file_path = ''; + + foreach ($backtrace as $index => $trace) { + if (isset($trace['file']) && + !str_contains($trace['file'], 'wp-includes') && + !str_contains($trace['file'], 'wp-admin')) { + + $file = basename($trace['file']); + $line = $trace['line'] ?? 'unknown'; + $function = $trace['function'] ?? 'unknown'; + + if ($short_info === 'unknown') { + $short_info = "{$file}:{$line} ({$function})"; + $file_path = $trace['file']; + } + + $full_info .= sprintf("#%d %s(%d): %s()\n", + $index, + $trace['file'], + $trace['line'] ?? 0, + $function + ); + } + } + + return [ + 'short' => $short_info, + 'full' => $full_info ?: 'No backtrace available', + 'file' => $file_path + ]; + } + + /** + * Определяем компонент (плагин/тема) по пути файла + */ + private function get_component_info($file_path) { + if (empty($file_path)) return 'unknown'; + + $abspath = ABSPATH; + + // Определяем тип компонента + if (str_contains($file_path, WP_PLUGIN_DIR)) { + $relative_path = str_replace(WP_PLUGIN_DIR . '/', '', $file_path); + $parts = explode('/', $relative_path); + return 'Plugin: ' . $parts[0]; + } elseif (str_contains($file_path, get_template_directory())) { + $theme = wp_get_theme(); + return 'Theme: ' . $theme->get('Name'); + } elseif (str_contains($file_path, get_stylesheet_directory())) { + $theme = wp_get_theme(); + return 'Child Theme: ' . $theme->get('Name'); + } elseif (str_contains($file_path, $abspath . 'wp-content/mu-plugins')) { + return 'MU-Plugin'; + } elseif (str_contains($file_path, $abspath . 'wp-content')) { + return 'Other (wp-content)'; + } elseif (str_contains($file_path, $abspath)) { + return 'WordPress Core'; + } + + return 'External'; + } + + private function shorten_query($query, $length = 120) { + $trimmed = trim($query); + if (strlen($trimmed) <= $length) return $trimmed; + + return substr($trimmed, 0, $length) . '...'; + } +} + +// Инициализируем логгер +//if (defined('WP_DEBUG') && WP_DEBUG) { + RealtimeSqlLogger::get_instance(); +//} + +// Функция для просмотра текущего состояния +add_action('wp_footer', 'show_sql_debug_info', 9999); +function show_sql_debug_info() { + if (!current_user_can('administrator')) return; + + $log_file = WP_CONTENT_DIR . '/sql-debug.log'; + + if (file_exists($log_file)) { + $log_content = file_get_contents($log_file); + echo '
'; + echo '

🎯 SQL Debug Log (Live)

'; + echo '
' . htmlspecialchars($log_content) . '
'; + echo '
'; + } +} \ No newline at end of file diff --git a/inc/rossiya.php b/inc/rossiya.php new file mode 100644 index 0000000..a27e394 --- /dev/null +++ b/inc/rossiya.php @@ -0,0 +1,24 @@ +is_main_query() || is_admin()) { + return; + } + + // Проверяем, что это архив рубрики с ID 3347 + if ($query->is_category(3347)) { + // Убираем фильтр по категории + $query->set('cat', ''); + $query->set('category__in', ''); + + // Устанавливаем фильтр по тегу + $query->set('tag', 'rossiya'); + + // Сбрасываем флаги, чтобы WordPress думал, что это архив тега + $query->is_category = false; + $query->is_tag = true; + $query->is_archive = true; + } +} +add_action('pre_get_posts', 'replace_category_with_tag'); \ No newline at end of file diff --git a/inc/schedule_async_post_processing.php b/inc/schedule_async_post_processing.php new file mode 100644 index 0000000..2bf3852 --- /dev/null +++ b/inc/schedule_async_post_processing.php @@ -0,0 +1,34 @@ +post_type, array( 'profile_article', 'anew', 'yellow' ), true ) ) { + return; + } + + if ( 'publish' !== $post->post_status ) { + return; + } + + // НЕМЕДЛЕННО ставим одну быструю задачу + as_schedule_single_action( + time() + 2, // Минимальная задержка + 'async_post_processing_trigger', + array( $post_id ), + 'async_processing' + ); + + +} + + +add_action( 'save_post', 'schedule_async_post_processing', 99, 3 ); // Низкий приоритет + + diff --git a/inc/sql_result.php b/inc/sql_result.php new file mode 100644 index 0000000..da2a785 --- /dev/null +++ b/inc/sql_result.php @@ -0,0 +1,221 @@ +queries)) { + echo '
'; + echo '

Нет данных о запросах. Убедитесь, что в wp-config.php добавлена строка:

'; + echo 'define( \'SAVEQUERIES\', true );'; + echo '
'; + return; + } + + $queries = $wpdb->queries; + $total_time = 0; + foreach ($queries as $query) { + $total_time += $query[1]; + } + + echo '
'; + + // Панель управления сортировкой + echo '
'; + echo '

SQL Запросы

'; + echo ''; + echo ''; + echo ''; + echo 'Всего запросов: ' . count($queries) . ''; + echo '
'; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ($queries as $query) { + $time = number_format($query[1] * 1000, 2); + $time_seconds = $query[1]; + + $color = '#27ae60'; + if ($query[1] > 0.1) $color = '#f39c12'; + if ($query[1] > 0.5) $color = '#e74c3c'; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo ''; + echo '
Время ⬇ЗапросВызвавший код
' . $time . ' ms' . htmlspecialchars($query[0]) . '' . $query[2] . '
'; + + echo '
'; + echo 'Общее время SQL запросов: ' . number_format($total_time * 1000, 2) . ' ms
'; + echo 'Среднее время на запрос: ' . number_format(($total_time / count($queries)) * 1000, 2) . ' ms'; + echo '
'; + + echo '
'; + + // Добавляем JavaScript для сортировки + echo ' + + '; +} + + +// Добавляем стили и скрипты +add_action('wp_enqueue_scripts', 'add_sql_debug_styles'); + +function add_sql_debug_styles() { + if (!current_user_can('administrator')) { + return; + } + + wp_enqueue_style('sql-debug-style', false); + echo ' + + '; +} \ No newline at end of file diff --git a/inc/test_action_scheduler.php b/inc/test_action_scheduler.php new file mode 100644 index 0000000..a6ebb08 --- /dev/null +++ b/inc/test_action_scheduler.php @@ -0,0 +1,53 @@ +post_type, array( 'profile_article','anew','yellow' ), true ) ) { + return; + } + + if ( 'publish' !== $post->post_status ) { + return; + } + + // Планируем задачу + as_schedule_single_action( + time() + 60, + 'test_action_scheduler_task', + array( $post_id, $post->post_title ), + 'test' + ); + + //error_log( "Задача запланирована для поста ID: {$post_id}" ); +} + +add_action( 'save_post', 'test_schedule_action_on_save_post', 10, 3 ); + diff --git a/inc/wp-cli-scheduler-commands.php b/inc/wp-cli-scheduler-commands.php new file mode 100644 index 0000000..4090abc --- /dev/null +++ b/inc/wp-cli-scheduler-commands.php @@ -0,0 +1,159 @@ + + * : ID задачи Action Scheduler + * + * [--retry] + * : Сбросить статус failed перед запуском + * + * ## EXAMPLES + * + * wp scheduler force-run-action 11843340 + * wp scheduler force-run-action 456 --retry + * + * @param array $args + * @param array $assoc_args + */ +public function force_run_action( $args, $assoc_args ) { + list( $action_id ) = $args; + $retry = isset( $assoc_args['retry'] ); + + WP_CLI::log( "Принудительный запуск задачи ID: {$action_id}" ); + + global $wpdb; + + // Получаем информацию о задаче + $action = $wpdb->get_row( $wpdb->prepare( + "SELECT * FROM {$wpdb->prefix}actionscheduler_actions WHERE action_id = %d", + $action_id + ) ); + + if ( ! $action ) { + WP_CLI::error( "Задача с ID {$action_id} не найдена." ); + return; + } + + WP_CLI::log( "Статус: " . $action->status ); + WP_CLI::log( "Хук: " . $action->hook ); + + // Если нужно сбросить статус failed + if ( $retry && 'failed' === $action->status ) { + $updated = $wpdb->update( + "{$wpdb->prefix}actionscheduler_actions", + array( 'status' => 'pending' ), + array( 'action_id' => $action_id ), + array( '%s' ), + array( '%d' ) + ); + + if ( $updated ) { + WP_CLI::log( "Статус сброшен с failed на pending" ); + } + } + + // Запускаем задачу через Action Scheduler + try { + // Создаем экземпляр задачи + $store = ActionScheduler::store(); + $action_obj = $store->fetch_action( $action_id ); + + if ( ! $action_obj ) { + WP_CLI::error( "Не удалось создать объект задачи." ); + return; + } + + WP_CLI::log( "Запуск задачи..." ); + + // Выполняем задачу + $start_time = microtime( true ); + $action_obj->execute(); + $execution_time = microtime( true ) - $start_time; + + // Проверяем новый статус + $new_status = $wpdb->get_var( $wpdb->prepare( + "SELECT status FROM {$wpdb->prefix}actionscheduler_actions WHERE action_id = %d", + $action_id + ) ); + + WP_CLI::success( sprintf( + "Задача выполнена за %.2f секунд. Новый статус: %s", + $execution_time, + $new_status + ) ); + + } catch ( Exception $e ) { + WP_CLI::error( "Ошибка выполнения: " . $e->getMessage() ); + } +} + + + + + + + + /** + * Сбросить статус failed задачи на pending + * + * ## OPTIONS + * + * + * : ID задачи Action Scheduler + * + * ## EXAMPLES + * + * wp scheduler reset-failed-action 123 + */ + + public function reset_failed_action( $args, $assoc_args ) { + + list( $action_id ) = $args; + + global $wpdb; + + $action = $wpdb->get_row( $wpdb->prepare( + "SELECT status FROM {$wpdb->prefix}actionscheduler_actions WHERE action_id = %d", + $action_id + ) ); + + if ( ! $action ) { + WP_CLI::error( "Задача с ID {$action_id} не найдена." ); + return; + } + + if ( 'failed' !== $action->status ) { + WP_CLI::warning( "Статус задачи не 'failed', а '{$action->status}'. Сброс не требуется." ); + return; + } + + $updated = $wpdb->update( + "{$wpdb->prefix}actionscheduler_actions", + array( 'status' => 'pending' ), + array( 'action_id' => $action_id ), + array( '%s' ), + array( '%d' ) + ); + + if ( $updated ) { + WP_CLI::success( "Статус задачи сброшен с failed на pending." ); + } else { + WP_CLI::error( "Не удалось сбросить статус задачи." ); + } + } + + + + + +} + +// Регистрируем команды - ЭТО ОБЯЗАТЕЛЬНО! +WP_CLI::add_command( 'scheduler', 'Scheduler_CLI_Commands' ); \ No newline at end of file