add files inc
This commit is contained in:
@@ -47,11 +47,8 @@ add_action('rest_api_init', function() {
|
|||||||
'photo' => $photo,
|
'photo' => $photo,
|
||||||
'photo_sizes' => $photo_sizes,
|
'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),
|
'url' => get_author_posts_url($coauthor->ID, $coauthor->user_nicename),
|
||||||
'type' => $coauthor->type ?? 'guest-author'
|
'type' => $coauthor->type ?? 'guest-author'
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
|||||||
require "inc/action-scheduler-functions.php";
|
require "inc/action-scheduler-functions.php";
|
||||||
require "inc/wp-cli-scheduler-commands.php";
|
require "inc/wp-cli-scheduler-commands.php";
|
||||||
require "inc/adfox_on.php"; // управление adfox
|
require "inc/adfox_on.php"; // управление adfox
|
||||||
|
require "inc/generate_coauthors_cache.php"; // генерим кеш списка авторов
|
||||||
}
|
}
|
||||||
|
|
||||||
include "inc/get_cached_alm.php";
|
include "inc/get_cached_alm.php";
|
||||||
|
|||||||
166
inc/action-scheduler-functions.php
Normal file
166
inc/action-scheduler-functions.php
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// action-scheduler-functions.php (загружается только для воркера)
|
||||||
|
add_action( 'async_post_processing_trigger', 'handle_async_post_processing', 10, 1 );
|
||||||
|
|
||||||
|
|
||||||
|
function handle_async_post_processing( $post_id ) {
|
||||||
|
|
||||||
|
// Сохраняем оригинальное состояние кеширования
|
||||||
|
//$original_cache_addition = wp_suspend_cache_addition( true );
|
||||||
|
//$original_cache_invalidation = wp_suspend_cache_invalidation( true );
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Очищаем кеш поста
|
||||||
|
//clean_post_cache( $post_id );
|
||||||
|
|
||||||
|
// Очищаем кеш таксономий
|
||||||
|
/** $taxonomies = get_object_taxonomies( get_post_type( $post_id ) );
|
||||||
|
foreach ( $taxonomies as $taxonomy ) {
|
||||||
|
wp_cache_delete( 'all_ids', $taxonomy );
|
||||||
|
wp_cache_delete( 'get', $taxonomy );
|
||||||
|
delete_option( "{$taxonomy}_children" );
|
||||||
|
wp_cache_delete( 'last_changed', 'terms' );
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// Получаем свежие данные
|
||||||
|
$post = get_post( $post_id );
|
||||||
|
|
||||||
|
log_scheduler_activity( 'текущий пост:', $post_id );
|
||||||
|
|
||||||
|
if ( ! $post || 'publish' !== $post->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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
159
inc/action-scheduler-functions_15_10_25.php
Normal file
159
inc/action-scheduler-functions_15_10_25.php
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// action-scheduler-functions.php (загружается только для воркера)
|
||||||
|
add_action( 'async_post_processing_trigger', 'handle_async_post_processing', 10, 1 );
|
||||||
|
|
||||||
|
|
||||||
|
function handle_async_post_processing( $post_id ) {
|
||||||
|
|
||||||
|
// Сохраняем оригинальное состояние кеширования
|
||||||
|
$original_cache_addition = wp_suspend_cache_addition( true );
|
||||||
|
$original_cache_invalidation = wp_suspend_cache_invalidation( true );
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Очищаем кеш поста
|
||||||
|
clean_post_cache( $post_id );
|
||||||
|
|
||||||
|
// Очищаем кеш таксономий
|
||||||
|
$taxonomies = get_object_taxonomies( get_post_type( $post_id ) );
|
||||||
|
foreach ( $taxonomies as $taxonomy ) {
|
||||||
|
wp_cache_delete( 'all_ids', $taxonomy );
|
||||||
|
wp_cache_delete( 'get', $taxonomy );
|
||||||
|
delete_option( "{$taxonomy}_children" );
|
||||||
|
wp_cache_delete( 'last_changed', 'terms' );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем свежие данные
|
||||||
|
$post = get_post( $post_id );
|
||||||
|
|
||||||
|
if ( ! $post || 'publish' !== $post->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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
124
inc/add_adv_checked.php
Normal file
124
inc/add_adv_checked.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Регистрация метабокса и обработка сохранения
|
||||||
|
add_action('add_meta_boxes', function() {
|
||||||
|
// Метабокс для рекламного материала
|
||||||
|
add_meta_box(
|
||||||
|
'advertisement_meta_box',
|
||||||
|
'Рекламный текст',
|
||||||
|
function($post) {
|
||||||
|
$is_advertisement = get_post_meta($post->ID, '_is_advertisement', true);
|
||||||
|
wp_nonce_field('advertisement_nonce', 'advertisement_nonce_field');
|
||||||
|
echo '<label><input type="checkbox" name="is_advertisement" value="1" ' . checked($is_advertisement, '1', false) . ' /> Это рекламная публикация</label>';
|
||||||
|
},
|
||||||
|
['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 '<label><input type="checkbox" name="no_aeroflot" value="1" ' . checked($no_aeroflot, '1', false) . ' /> Не отправлять в Аэрофлот</label>';
|
||||||
|
},
|
||||||
|
['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 '<style>
|
||||||
|
/* Стили для рекламного метабокса - теплые тона */
|
||||||
|
#advertisement_meta_box {
|
||||||
|
background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
|
||||||
|
border: 2px solid #ffb300;
|
||||||
|
border-left: 4px solid #ffb300;
|
||||||
|
}
|
||||||
|
#advertisement_meta_box .hndle {
|
||||||
|
background: #ffb300;
|
||||||
|
color: white;
|
||||||
|
border-bottom: 2px solid #ff8f00;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
#advertisement_meta_box .hndle:before {
|
||||||
|
content: "📢";
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
#advertisement_meta_box label {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e65100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для Аэрофлота - синие тона как в бренде */
|
||||||
|
#aeroflot_meta_box {
|
||||||
|
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||||
|
border: 2px solid #1565c0;
|
||||||
|
border-left: 4px solid #1565c0;
|
||||||
|
}
|
||||||
|
#aeroflot_meta_box .hndle {
|
||||||
|
background: #1565c0;
|
||||||
|
color: white;
|
||||||
|
border-bottom: 2px solid #0d47a1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
#aeroflot_meta_box .hndle:before {
|
||||||
|
content: "✈️";
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
#aeroflot_meta_box label {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #0d47a1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для чекбоксов */
|
||||||
|
#advertisement_meta_box input[type="checkbox"],
|
||||||
|
#aeroflot_meta_box input[type="checkbox"] {
|
||||||
|
margin-right: 8px;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
#advertisement_meta_box input[type="checkbox"]:checked {
|
||||||
|
accent-color: #ffb300;
|
||||||
|
}
|
||||||
|
#aeroflot_meta_box input[type="checkbox"]:checked {
|
||||||
|
accent-color: #1565c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Выравнивание иконок */
|
||||||
|
#advertisement_meta_box .hndle,
|
||||||
|
#aeroflot_meta_box .hndle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>';
|
||||||
|
});
|
||||||
46
inc/add_adv_checked_advonly.php
Normal file
46
inc/add_adv_checked_advonly.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Регистрация метабокса и обработка сохранения
|
||||||
|
add_action('add_meta_boxes', function() {
|
||||||
|
add_meta_box(
|
||||||
|
'advertisement_meta_box',
|
||||||
|
'Рекламный материал',
|
||||||
|
function($post) {
|
||||||
|
$is_advertisement = get_post_meta($post->ID, '_is_advertisement', true);
|
||||||
|
wp_nonce_field('advertisement_nonce', 'advertisement_nonce_field');
|
||||||
|
echo '<label><input type="checkbox" name="is_advertisement" value="1" ' . checked($is_advertisement, '1', false) . ' /> Это рекламная публикация</label>';
|
||||||
|
},
|
||||||
|
['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 '<style>
|
||||||
|
#advertisement_meta_box {
|
||||||
|
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||||
|
border: 2px solid #2196f3;
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
#advertisement_meta_box .hndle {
|
||||||
|
background: #2196f3;
|
||||||
|
color: white;
|
||||||
|
border-bottom: 2px solid #1976d2;
|
||||||
|
}
|
||||||
|
#advertisement_meta_box label {
|
||||||
|
display: block;
|
||||||
|
padding: 12px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1565c0;
|
||||||
|
}
|
||||||
|
</style>';
|
||||||
|
});
|
||||||
74
inc/adfox_on.php
Normal file
74
inc/adfox_on.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* WP-CLI команды для управления рекламой
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH') && !defined('WP_CLI')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ad_Manager_Commands {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Управление настройкой рекламы
|
||||||
|
*/
|
||||||
|
public function __invoke($args, $assoc_args) {
|
||||||
|
if (empty($args)) {
|
||||||
|
WP_CLI::error('Укажите действие: status, on, off, toggle');
|
||||||
|
}
|
||||||
|
|
||||||
|
list($action) = $args;
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'status':
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
38
inc/admin/auto_check.php
Normal file
38
inc/admin/auto_check.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function add_auto_check_script_to_post_edit() {
|
||||||
|
$screen = get_current_screen();
|
||||||
|
if ($screen && $screen->base === 'post') {
|
||||||
|
?>
|
||||||
|
<script type="text/javascript">
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
// Проверяем, что это новый пост
|
||||||
|
if (window.location.href.indexOf('post-new.php') > -1) {
|
||||||
|
// Ждем загрузки DOM
|
||||||
|
setTimeout(function() {
|
||||||
|
// Устанавливаем атрибут checked="checked" для нужных чекбоксов
|
||||||
|
const checkboxes = [
|
||||||
|
'input[name="yzrssenabled_meta_value"][type="checkbox"]',
|
||||||
|
'input[name="ynrss_exclude"][type="checkbox"]',
|
||||||
|
'input[name="_send_telegram"][type="checkbox"]'
|
||||||
|
//'input[name="_hide_on_mainpage"][type="checkbox"]',
|
||||||
|
// Добавьте другие чекбоксы здесь:
|
||||||
|
// 'input[name="_another_field"][type="checkbox"]',
|
||||||
|
];
|
||||||
|
|
||||||
|
checkboxes.forEach(function(selector) {
|
||||||
|
const $checkbox = $(selector);
|
||||||
|
if ($checkbox.length) {
|
||||||
|
$checkbox.attr('checked', 'checked');
|
||||||
|
$checkbox.prop('checked', true);
|
||||||
|
//console.log('Установлен checked для:', selector);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action('admin_footer', 'add_auto_check_script_to_post_edit');
|
||||||
807
inc/admin/lock-terms.php
Normal file
807
inc/admin/lock-terms.php
Normal file
@@ -0,0 +1,807 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
add_action('admin_enqueue_scripts', function($hook) {
|
||||||
|
if ($hook !== 'post.php' && $hook !== 'post-new.php') return;
|
||||||
|
|
||||||
|
wp_enqueue_script('select2');
|
||||||
|
wp_enqueue_style('select2');
|
||||||
|
});
|
||||||
|
|
||||||
|
add_action('admin_footer', function () {
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
// Массив с настройками для каждой таксономии
|
||||||
|
$taxonomies_settings = [
|
||||||
|
'banned' => [
|
||||||
|
'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
|
||||||
|
]
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
jQuery(function ($) {
|
||||||
|
|
||||||
|
const taxonomiesSettings = <?php echo json_encode($taxonomies_settings); ?>;
|
||||||
|
|
||||||
|
Object.keys(taxonomiesSettings).forEach(tax => {
|
||||||
|
const settings = taxonomiesSettings[tax];
|
||||||
|
|
||||||
|
// Для post_tag используется другой ID контейнера
|
||||||
|
let box;
|
||||||
|
if (tax === 'post_tag') {
|
||||||
|
box = $('#tagsdiv-post_tag');
|
||||||
|
// Если не нашли по стандартному ID, пробуем найти по классу
|
||||||
|
if (!box.length) {
|
||||||
|
box = $('.tagsdiv-post_tag');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
box = $('#tagsdiv-' + tax);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!box.length) {
|
||||||
|
console.log('Container not found for taxonomy:', tax);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalInput = box.find('.taghint').parent().find('input');
|
||||||
|
const tagList = box.find('.tagchecklist');
|
||||||
|
const tagCloudLink = box.find('.tagcloud-link');
|
||||||
|
|
||||||
|
// Получаем классы с оригинального input
|
||||||
|
const originalClasses = originalInput.attr('class') || '';
|
||||||
|
|
||||||
|
// Скрываем стандартные элементы
|
||||||
|
originalInput.hide();
|
||||||
|
box.find('.ajaxtag').hide();
|
||||||
|
tagList.hide();
|
||||||
|
|
||||||
|
// Создаем новый элемент select с классами оригинала
|
||||||
|
const select = $('<select multiple="multiple" class="' + originalClasses + '"></select>');
|
||||||
|
select.insertBefore(tagList);
|
||||||
|
|
||||||
|
// Создаем скрытое поле для хранения ID терминов
|
||||||
|
const hiddenIdsInput = $('<input type="hidden" name="' + tax + '_ids" />');
|
||||||
|
hiddenIdsInput.insertAfter(select);
|
||||||
|
|
||||||
|
// Переменная для отслеживания последнего введенного текста
|
||||||
|
let lastInputText = '';
|
||||||
|
|
||||||
|
// Функция для обновления скрытых inputs
|
||||||
|
const updateHiddenInputs = function() {
|
||||||
|
let selectedNames = [];
|
||||||
|
let selectedIds = [];
|
||||||
|
|
||||||
|
// Получаем все выбранные значения из Select2
|
||||||
|
const selectedData = select.select2('data');
|
||||||
|
|
||||||
|
selectedData.forEach(item => {
|
||||||
|
const value = item.id;
|
||||||
|
const text = item.text;
|
||||||
|
|
||||||
|
selectedNames.push(text);
|
||||||
|
|
||||||
|
// Для новых тегов используем название вместо ID
|
||||||
|
if (value.toString().startsWith('NEW_')) {
|
||||||
|
// Сохраняем название тега (без префикса NEW_)
|
||||||
|
selectedIds.push(text);
|
||||||
|
} else {
|
||||||
|
// Существующий ID термина
|
||||||
|
selectedIds.push(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Для совместимости со стандартным input WordPress
|
||||||
|
originalInput.val(selectedNames.join(','));
|
||||||
|
// Для нашего обработчика сохранения
|
||||||
|
hiddenIdsInput.val(selectedIds.join(','));
|
||||||
|
|
||||||
|
console.log('Updated hidden inputs for', tax, ':', {
|
||||||
|
names: selectedNames.join(','),
|
||||||
|
ids: selectedIds.join(',')
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для принудительного добавления тега
|
||||||
|
const forceAddTag = function(tagText) {
|
||||||
|
if (!tagText || tagText.trim() === '') return false;
|
||||||
|
|
||||||
|
const trimmedText = tagText.trim();
|
||||||
|
const newTagId = 'NEW_' + trimmedText;
|
||||||
|
|
||||||
|
// Проверяем, нет ли уже такого тега в выбранных
|
||||||
|
const existingOptions = select.find('option');
|
||||||
|
let alreadyExists = false;
|
||||||
|
|
||||||
|
existingOptions.each(function() {
|
||||||
|
if ($(this).val() === newTagId || $(this).text().toLowerCase() === trimmedText.toLowerCase()) {
|
||||||
|
alreadyExists = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!alreadyExists && settings.allow_new_tags) {
|
||||||
|
const option = new Option(trimmedText, newTagId, true, true);
|
||||||
|
select.append(option);
|
||||||
|
select.trigger('change');
|
||||||
|
|
||||||
|
console.log('Force added tag:', trimmedText);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Настройки Select2 в зависимости от таксономии
|
||||||
|
const select2Options = {
|
||||||
|
ajax: {
|
||||||
|
url: ajaxurl,
|
||||||
|
dataType: 'json',
|
||||||
|
delay: 250,
|
||||||
|
data: (params) => ({
|
||||||
|
action: 'get_existing_terms',
|
||||||
|
taxonomy: tax,
|
||||||
|
search: params.term || ''
|
||||||
|
}),
|
||||||
|
processResults: function (data) {
|
||||||
|
return {
|
||||||
|
results: data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placeholder: '',
|
||||||
|
allowClear: true,
|
||||||
|
width: '100%',
|
||||||
|
minimumInputLength: 1, // Минимальная длина для поиска
|
||||||
|
matcher: function(params, data) {
|
||||||
|
// Если нет поискового запроса, показываем все результаты
|
||||||
|
if ($.trim(params.term) === '') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, содержит ли текст данные поискового запроса
|
||||||
|
if (data.text.toLowerCase().indexOf(params.term.toLowerCase()) > -1) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не нашли совпадение, скрываем результат
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Для banned и post_tag - только выбор из существующих
|
||||||
|
// Для keys - разрешаем создание новых тегов
|
||||||
|
if (settings.allow_new_tags) {
|
||||||
|
select2Options.tags = true;
|
||||||
|
select2Options.createTag = function (params) {
|
||||||
|
var term = $.trim(params.term);
|
||||||
|
|
||||||
|
if (term === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, есть ли уже такой термин в результатах поиска
|
||||||
|
// Если есть - не создаем новый тег
|
||||||
|
return {
|
||||||
|
id: 'NEW_' + term,
|
||||||
|
text: term,
|
||||||
|
newTag: true
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Добавляем валидацию для новых тегов
|
||||||
|
select2Options.insertTag = function (data, tag) {
|
||||||
|
// Проверяем, не существует ли уже такого тега в данных
|
||||||
|
const existingTag = data.find(item =>
|
||||||
|
item.text.toLowerCase() === tag.text.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingTag) {
|
||||||
|
// Если тег уже существует, не добавляем новый
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем новый тег в начало списка
|
||||||
|
data.unshift(tag);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
select2Options.tags = false;
|
||||||
|
// Для таксономий без создания новых тегов запрещаем любые пользовательские вводы
|
||||||
|
select2Options.createTag = function (params) {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация select2
|
||||||
|
select.select2(select2Options);
|
||||||
|
|
||||||
|
// Обработчики для гарантированного добавления тегов
|
||||||
|
if (settings.allow_new_tags) {
|
||||||
|
// Обработчик ввода - сохраняем последний введенный текст
|
||||||
|
select.on('input', '.select2-search__field', function(e) {
|
||||||
|
lastInputText = $(this).val();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик keydown на самом select элементе
|
||||||
|
select.on('keydown', function(e) {
|
||||||
|
if (e.which === 13) { // Enter
|
||||||
|
const select2Container = select.next('.select2-container');
|
||||||
|
const searchInput = select2Container.find('.select2-search__field');
|
||||||
|
const currentText = searchInput.val().trim();
|
||||||
|
|
||||||
|
if (currentText !== '') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
|
// Даем немного времени Select2 на обработку
|
||||||
|
setTimeout(() => {
|
||||||
|
if (forceAddTag(currentText)) {
|
||||||
|
searchInput.val('');
|
||||||
|
updateHiddenInputs();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Дополнительный обработчик на document для перехвата всех Enter
|
||||||
|
$(document).on('keydown', function(e) {
|
||||||
|
if (e.which === 13) {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
if ($(activeElement).hasClass('select2-search__field')) {
|
||||||
|
const select2Container = $(activeElement).closest('.select2-container');
|
||||||
|
const relatedSelect = $('select').filter(function() {
|
||||||
|
return $(this).next('.select2-container').is(select2Container);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (relatedSelect.length && settings.allow_new_tags) {
|
||||||
|
const currentText = $(activeElement).val().trim();
|
||||||
|
if (currentText !== '') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (forceAddTag(currentText)) {
|
||||||
|
$(activeElement).val('');
|
||||||
|
updateHiddenInputs();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Обработчик для случаев, когда Select2 не успевает обработать тег
|
||||||
|
select.on('select2:closing', function(e) {
|
||||||
|
const select2Container = select.next('.select2-container');
|
||||||
|
const searchInput = select2Container.find('.select2-search__field');
|
||||||
|
const currentText = searchInput.val().trim();
|
||||||
|
|
||||||
|
if (currentText !== '') {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (forceAddTag(currentText)) {
|
||||||
|
searchInput.val('');
|
||||||
|
updateHiddenInputs();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработчики событий Select2
|
||||||
|
select.on('change', function () {
|
||||||
|
console.log('Select2 changed for', tax, 'selected values:', $(this).val());
|
||||||
|
updateHiddenInputs();
|
||||||
|
});
|
||||||
|
|
||||||
|
select.on('select2:select', function (e) {
|
||||||
|
console.log('Select2 select for', tax, 'selected item:', e.params.data);
|
||||||
|
updateHiddenInputs();
|
||||||
|
});
|
||||||
|
|
||||||
|
select.on('select2:unselect', function (e) {
|
||||||
|
console.log('Select2 unselect for', tax, 'unselected item:', e.params.data);
|
||||||
|
updateHiddenInputs();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Загружаем выбранные термы в select2
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
data: {
|
||||||
|
action: 'get_post_terms',
|
||||||
|
post_id: <?php echo (int)$post->ID; ?>,
|
||||||
|
taxonomy: tax
|
||||||
|
}
|
||||||
|
}).done(function (terms) {
|
||||||
|
terms.forEach(t => {
|
||||||
|
let option = new Option(t.text, t.id, true, true);
|
||||||
|
select.append(option);
|
||||||
|
});
|
||||||
|
select.trigger('change');
|
||||||
|
|
||||||
|
// Обновляем скрытые inputs
|
||||||
|
updateHiddenInputs();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Переменная для хранения текущего облака
|
||||||
|
let currentTagCloud = null;
|
||||||
|
|
||||||
|
// Обработчик клика по ссылке "Выбрать из часто используемых меток"
|
||||||
|
tagCloudLink.off('click').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
|
// Если облако уже открыто - закрываем его
|
||||||
|
if (currentTagCloud && currentTagCloud.is(':visible')) {
|
||||||
|
currentTagCloud.remove();
|
||||||
|
currentTagCloud = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем предыдущее облако если есть
|
||||||
|
if (currentTagCloud) {
|
||||||
|
currentTagCloud.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Загружаем популярные теги через AJAX
|
||||||
|
$.ajax({
|
||||||
|
url: ajaxurl,
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
data: {
|
||||||
|
action: 'get_popular_terms',
|
||||||
|
taxonomy: tax
|
||||||
|
}
|
||||||
|
}).done(function(terms) {
|
||||||
|
// Создаем свое облако тегов в стандартном стиле WordPress
|
||||||
|
if (terms.length > 0) {
|
||||||
|
currentTagCloud = $('<div class="the-tagcloud" style="margin-top: 10px;"></div>');
|
||||||
|
|
||||||
|
// Находим минимальное и максимальное количество использования
|
||||||
|
let minCount = Math.min(...terms.map(term => term.count));
|
||||||
|
let maxCount = Math.max(...terms.map(term => term.count));
|
||||||
|
|
||||||
|
terms.forEach(term => {
|
||||||
|
const tagLink = $('<a href="#" class="tag-cloud-link"></a>');
|
||||||
|
tagLink.text(term.name);
|
||||||
|
tagLink.attr('data-id', term.id);
|
||||||
|
|
||||||
|
// Рассчитываем размер шрифта на основе популярности
|
||||||
|
const fontSize = calculateFontSize(term.count, minCount, maxCount);
|
||||||
|
tagLink.css('font-size', fontSize + 'px');
|
||||||
|
|
||||||
|
currentTagCloud.append(tagLink);
|
||||||
|
currentTagCloud.append(' ');
|
||||||
|
});
|
||||||
|
|
||||||
|
box.append(currentTagCloud);
|
||||||
|
|
||||||
|
// Обработчик клика по тегам в нашем облаке
|
||||||
|
currentTagCloud.on('click', 'a', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const tagName = $(this).text();
|
||||||
|
const tagId = $(this).data('id');
|
||||||
|
|
||||||
|
// Добавляем тег в Select2
|
||||||
|
if (!select.find('option[value="' + tagId + '"]').length) {
|
||||||
|
let option = new Option(tagName, tagId, true, true);
|
||||||
|
select.append(option);
|
||||||
|
select.trigger('change');
|
||||||
|
updateHiddenInputs();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Закрываем облако после выбора
|
||||||
|
currentTagCloud.remove();
|
||||||
|
currentTagCloud = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция для расчета размера шрифта на основе популярности
|
||||||
|
function calculateFontSize(count, minCount, maxCount) {
|
||||||
|
// Минимальный и максимальный размер шрифта в px
|
||||||
|
const minSize = 11;
|
||||||
|
const maxSize = 18;
|
||||||
|
|
||||||
|
// Если все теги имеют одинаковую популярность
|
||||||
|
if (minCount === maxCount) {
|
||||||
|
return (minSize + maxSize) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рассчитываем размер пропорционально популярности
|
||||||
|
const scale = (count - minCount) / (maxCount - minCount);
|
||||||
|
return minSize + (scale * (maxSize - minSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Закрываем облако при клике вне его
|
||||||
|
$(document).on('click', function(e) {
|
||||||
|
if (currentTagCloud && !$(e.target).closest('.the-tagcloud').length && !$(e.target).closest('.tagcloud-link').length) {
|
||||||
|
currentTagCloud.remove();
|
||||||
|
currentTagCloud = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.tagcloud-link').filter(function() {
|
||||||
|
return $(this).text().trim() === 'Выбрать из часто используемых меток';
|
||||||
|
}).text('Выбрать из часто используемых');
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Стили с фоном select, цветными рамками и placeholder -->
|
||||||
|
<style>
|
||||||
|
.select2-container {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
.select2-selection {
|
||||||
|
min-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для banned */
|
||||||
|
#tagsdiv-banned .select2-container--default .select2-selection--multiple {
|
||||||
|
background-color: <?php echo $taxonomies_settings['banned']['background']; ?> !important;
|
||||||
|
}
|
||||||
|
#tagsdiv-banned .select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: <?php echo $taxonomies_settings['banned']['background']; ?> !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: <?php echo $taxonomies_settings['banned']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
#tagsdiv-banned .select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||||
|
border-color: <?php echo $taxonomies_settings['banned']['color']; ?> !important;
|
||||||
|
box-shadow: 0 0 0 1px <?php echo $taxonomies_settings['banned']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
/* Placeholder для banned */
|
||||||
|
#tagsdiv-banned .select2-container--default .select2-selection--multiple .select2-selection__placeholder {
|
||||||
|
color: <?php echo $taxonomies_settings['banned']['placeholder_color']; ?> !important;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
/* Курсор для banned */
|
||||||
|
#tagsdiv-banned .select2-container--default .select2-search--inline .select2-search__field {
|
||||||
|
color: <?php echo $taxonomies_settings['banned']['placeholder_color']; ?> !important;
|
||||||
|
caret-color: <?php echo $taxonomies_settings['banned']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
#tagsdiv-banned .select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: <?php echo $taxonomies_settings['banned']['background']; ?> !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: <?php echo $taxonomies_settings['banned']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для keys */
|
||||||
|
#tagsdiv-keys .select2-container--default .select2-selection--multiple {
|
||||||
|
background-color: <?php echo $taxonomies_settings['keys']['background']; ?> !important;
|
||||||
|
}
|
||||||
|
#tagsdiv-keys .select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: <?php echo $taxonomies_settings['keys']['background']; ?> !important;
|
||||||
|
color: #2271b1 !important;
|
||||||
|
border-color: <?php echo $taxonomies_settings['keys']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
#tagsdiv-keys .select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||||
|
border-color: <?php echo $taxonomies_settings['keys']['color']; ?> !important;
|
||||||
|
box-shadow: 0 0 0 1px <?php echo $taxonomies_settings['keys']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
/* Placeholder для keys */
|
||||||
|
#tagsdiv-keys .select2-container--default .select2-selection--multiple .select2-selection__placeholder {
|
||||||
|
color: <?php echo $taxonomies_settings['keys']['placeholder_color']; ?> !important;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
/* Курсор для keys */
|
||||||
|
#tagsdiv-keys .select2-container--default .select2-search--inline .select2-search__field {
|
||||||
|
color: <?php echo $taxonomies_settings['keys']['placeholder_color']; ?> !important;
|
||||||
|
caret-color: <?php echo $taxonomies_settings['keys']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
#tagsdiv-keys .select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: <?php echo $taxonomies_settings['keys']['background']; ?> !important;
|
||||||
|
color: #2271b1 !important;
|
||||||
|
border-color: <?php echo $taxonomies_settings['keys']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для post_tag */
|
||||||
|
#tagsdiv-post_tag .select2-container--default .select2-selection--multiple,
|
||||||
|
.tagsdiv-post_tag .select2-container--default .select2-selection--multiple {
|
||||||
|
background-color: <?php echo $taxonomies_settings['post_tag']['background']; ?> !important;
|
||||||
|
}
|
||||||
|
#tagsdiv-post_tag .select2-container--default .select2-selection--multiple .select2-selection__choice,
|
||||||
|
.tagsdiv-post_tag .select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: <?php echo $taxonomies_settings['post_tag']['background']; ?> !important;
|
||||||
|
color: <?php echo $taxonomies_settings['post_tag']['color']; ?> !important;
|
||||||
|
border-color: <?php echo $taxonomies_settings['post_tag']['color']; ?> !important;
|
||||||
|
} !impor
|
||||||
|
#tagsdiv-post_tag .select2-container--default.select2-container--focus .select2-selection--multiple,
|
||||||
|
.tagsdiv-post_tag .select2-container--default.select2-container--focus .select2-selection--multiple {
|
||||||
|
border-color: <?php echo $taxonomies_settings['post_tag']['color']; ?> !important;
|
||||||
|
box-shadow: 0 0 0 1px <?php echo $taxonomies_settings['post_tag']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
/* Placeholder для post_tag */
|
||||||
|
#tagsdiv-post_tag .select2-container--default .select2-selection--multiple .select2-selection__placeholder,
|
||||||
|
.tagsdiv-post_tag .select2-container--default .select2-selection--multiple .select2-selection__placeholder {
|
||||||
|
color: <?php echo $taxonomies_settings['post_tag']['placeholder_color']; ?> !important;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
/* Курсор для post_tag */
|
||||||
|
#tagsdiv-post_tag .select2-container--default .select2-search--inline .select2-search__field,
|
||||||
|
.tagsdiv-post_tag .select2-container--default .select2-search--inline .select2-search__field {
|
||||||
|
color: <?php echo $taxonomies_settings['post_tag']['placeholder_color']; ?> !important;
|
||||||
|
caret-color: <?php echo $taxonomies_settings['post_tag']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
#tagsdiv-post_tag .select2-container--default .select2-selection--multiple .select2-selection__choice,
|
||||||
|
.tagsdiv-post_tag .select2-container--default .select2-selection--multiple .select2-selection__choice {
|
||||||
|
background-color: <?php echo $taxonomies_settings['post_tag']['background']; ?> !important;
|
||||||
|
color: <?php echo $taxonomies_settings['post_tag']['color']; ?> !important;
|
||||||
|
border-color: <?php echo $taxonomies_settings['post_tag']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Общие стили для кнопок удаления */
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||||
|
color: inherit !important;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для текста в поле ввода (для всех таксономий) */
|
||||||
|
.select2-container--default .select2-search--inline .select2-search__field {
|
||||||
|
background: transparent !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для облака тегов в стандартном стиле WordPress */
|
||||||
|
.the-tagcloud {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ccd0d4;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 2;
|
||||||
|
}
|
||||||
|
.the-tagcloud a {
|
||||||
|
color: #2271b1 !important;
|
||||||
|
text-decoration: none;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 2px 5px 2px 0;
|
||||||
|
}
|
||||||
|
.the-tagcloud a:hover {
|
||||||
|
color: #135e96 !important;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
/* Стили для обводки в выпадающем списке */
|
||||||
|
#tagsdiv-banned .select2-container--default .select2-search__field:focus {
|
||||||
|
outline: 1px solid <?php echo $taxonomies_settings['banned']['color']; ?> !important;
|
||||||
|
outline-offset: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tagsdiv-keys .select2-container--default .select2-search__field:focus {
|
||||||
|
outline: 1px solid <?php echo $taxonomies_settings['keys']['color']; ?> !important;
|
||||||
|
outline-offset: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tagsdiv-post_tag .select2-container--default .select2-search__field:focus,
|
||||||
|
.tagsdiv-post_tag .select2-container--default .select2-search__field:focus {
|
||||||
|
outline: 1px solid <?php echo $taxonomies_settings['post_tag']['color']; ?> !important;
|
||||||
|
outline-offset: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Дополнительно для браузеров которые используют box-shadow для фокуса */
|
||||||
|
#tagsdiv-banned .select2-container--default .select2-search__field:focus {
|
||||||
|
box-shadow: 0 0 0 1px <?php echo $taxonomies_settings['banned']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tagsdiv-keys .select2-container--default .select2-search__field:focus {
|
||||||
|
box-shadow: 0 0 0 1px <?php echo $taxonomies_settings['keys']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tagsdiv-post_tag .select2-container--default .select2-search__field:focus,
|
||||||
|
.tagsdiv-post_tag .select2-container--default .select2-search__field:focus {
|
||||||
|
box-shadow: 0 0 0 1px <?php echo $taxonomies_settings['post_tag']['color']; ?> !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagsdiv .howto {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
<?php
|
||||||
|
});
|
||||||
|
|
||||||
|
// AJAX обработчик для получения существующих терминов с сортировкой по релевантности
|
||||||
|
add_action('wp_ajax_get_existing_terms', function () {
|
||||||
|
$taxonomy = sanitize_key($_GET['taxonomy']);
|
||||||
|
$search = sanitize_text_field($_GET['search'] ?? '');
|
||||||
|
|
||||||
|
$args = [
|
||||||
|
'taxonomy' => $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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
271
inc/article-rss-feed.php
Normal file
271
inc/article-rss-feed.php
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Custom RSS Feed for profile_article posts
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Защита от прямого доступа
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomProfileArticleRSS {
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
add_action('init', [$this, 'add_rss_endpoint']);
|
||||||
|
add_action('template_redirect', [$this, 'generate_rss_feed']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Добавляем endpoint для RSS ленты
|
||||||
|
*/
|
||||||
|
public function add_rss_endpoint() {
|
||||||
|
add_rewrite_rule(
|
||||||
|
'^rss-feed/?$',
|
||||||
|
'index.php?custom_rss_feed=1',
|
||||||
|
'top'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_rewrite_tag('%custom_rss_feed%', '([^&]+)');
|
||||||
|
add_rewrite_tag('%rss_date%', '([^&]+)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерируем RSS ленту
|
||||||
|
*/
|
||||||
|
public function generate_rss_feed() {
|
||||||
|
if (get_query_var('custom_rss_feed') || isset($_GET['rss-feed'])) {
|
||||||
|
$date = isset($_GET['date']) ? sanitize_text_field($_GET['date']) : date('Y-m-d');
|
||||||
|
|
||||||
|
// Валидация даты
|
||||||
|
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
|
||||||
|
$date = date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->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\b[^>]*>.*?<\/script>/is', '', $content);
|
||||||
|
$content = preg_replace('/<style\b[^>]*>.*?<\/style>/is', '', $content);
|
||||||
|
$content = preg_replace('/<iframe\b[^>]*>.*?<\/iframe>/is', '', $content);
|
||||||
|
$content = preg_replace('/<form\b[^>]*>.*?<\/form>/is', '', $content);
|
||||||
|
$content = preg_replace('/<input[^>]*>/is', '', $content);
|
||||||
|
$content = preg_replace('/<button[^>]*>.*?<\/button>/is', '', $content);
|
||||||
|
$content = preg_replace('/<select[^>]*>.*?<\/select>/is', '', $content);
|
||||||
|
$content = preg_replace('/<textarea[^>]*>.*?<\/textarea>/is', '', $content);
|
||||||
|
$content = str_replace('<p></p>', '', $content);
|
||||||
|
|
||||||
|
// Удаляем определенные классы
|
||||||
|
$content = preg_replace('/<div[^>]*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 '<?xml version="1.0" encoding="' . get_option('blog_charset') . '"?>';
|
||||||
|
?>
|
||||||
|
<rss version="2.0"
|
||||||
|
xmlns:yandex="http://news.yandex.ru"
|
||||||
|
xmlns:media="http://search.yahoo.com/mrss/"
|
||||||
|
xmlns:turbo="http://turbo.yandex.ru">
|
||||||
|
|
||||||
|
<channel>
|
||||||
|
<title><?php echo esc_xml(get_bloginfo('name')); ?></title>
|
||||||
|
<link><?php echo esc_url(home_url()); ?></link>
|
||||||
|
<description><?php echo esc_xml(get_bloginfo('description')); ?></description>
|
||||||
|
<language><?php echo esc_xml(get_bloginfo('language')); ?></language>
|
||||||
|
<lastBuildDate><?php echo esc_xml(gmdate(DATE_RSS)); ?></lastBuildDate>
|
||||||
|
<pubDate><?php echo esc_xml(gmdate(DATE_RSS)); ?></pubDate>
|
||||||
|
<guid>https://profile.ru</guid>
|
||||||
|
<generator>Информационное агенство Деловой журнал Профиль</generator>
|
||||||
|
<image>
|
||||||
|
<url><?php echo esc_url($this->get_site_icon_url()); ?></url>
|
||||||
|
<title><?php echo esc_xml(get_bloginfo('name')); ?></title>
|
||||||
|
<link><?php echo esc_url(home_url()); ?></link>
|
||||||
|
</image>
|
||||||
|
<?php if (empty($posts)): ?>
|
||||||
|
<item>
|
||||||
|
<title>No posts for period <?php echo esc_xml($start_date); ?> to <?php echo esc_xml($end_date); ?></title>
|
||||||
|
<link><?php echo esc_url(home_url()); ?></link>
|
||||||
|
<description>No posts found for this period</description>
|
||||||
|
<pubDate><?php echo esc_xml(gmdate(DATE_RSS)); ?></pubDate>
|
||||||
|
<guid isPermaLink="false">no-posts-<?php echo esc_xml($start_date . '-to-' . $end_date); ?></guid>
|
||||||
|
</item>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($posts as $post):
|
||||||
|
$post = new WP_Post($post);
|
||||||
|
$post_url = get_permalink($post->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);
|
||||||
|
?>
|
||||||
|
<item>
|
||||||
|
<title><?php echo esc_xml(get_the_title($post->ID)); ?></title>
|
||||||
|
<link><?php echo esc_url($post_url); ?></link>
|
||||||
|
<description><?php echo esc_xml($excerpt); ?></description>
|
||||||
|
<pubDate><?php echo esc_xml($post_date); ?></pubDate>
|
||||||
|
<?php if (!empty($authors)): ?>
|
||||||
|
<?php
|
||||||
|
$author_names = array();
|
||||||
|
foreach ($authors as $author) {
|
||||||
|
$author_names[] = $author->display_name;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<author><?php echo esc_xml(implode(', ', $author_names)); ?></author>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php $default_author = get_the_author_meta('display_name', $post->post_author); ?>
|
||||||
|
<author><?php echo esc_xml($default_author); ?></author>
|
||||||
|
<?php endif; ?>
|
||||||
|
<guid><?php echo esc_url($post_url); ?></guid>
|
||||||
|
<?php if ($thumbnail): ?>
|
||||||
|
<imageAnnounce>
|
||||||
|
<url><?php echo esc_url($thumbnail); ?></url>
|
||||||
|
<title><?php echo esc_xml(get_the_title($post->ID)); ?></title>
|
||||||
|
<description><?php echo esc_xml($excerpt); ?></description>
|
||||||
|
</imageAnnounce>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($thumbnail): ?>
|
||||||
|
<image>
|
||||||
|
<url><?php echo esc_url($thumbnail); ?></url>
|
||||||
|
<title><?php echo esc_xml(get_the_title($post->ID)); ?></title>
|
||||||
|
<description><?php echo esc_xml($excerpt); ?></description>
|
||||||
|
<link><?php echo esc_url($post_url); ?></link>
|
||||||
|
</image>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php
|
||||||
|
$categories = get_the_category($post->ID);
|
||||||
|
foreach ($categories as $category) {
|
||||||
|
echo '<category>' . esc_xml($category->name) . '</category>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<content:encoded>
|
||||||
|
<![CDATA[<?php echo $content; ?>]]>
|
||||||
|
</content:encoded>
|
||||||
|
</item>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получаем контент поста
|
||||||
|
*/
|
||||||
|
private function get_post_content($post) {
|
||||||
|
$content = apply_filters('the_content', $post->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']);
|
||||||
195
inc/generate_coauthors_cache.php
Normal file
195
inc/generate_coauthors_cache.php
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* CLI команда для генерации кеша соавторов
|
||||||
|
*
|
||||||
|
* Запуск: wp coauthors generate-cache
|
||||||
|
* Или: wp eval "generate_coauthors_cache();"
|
||||||
|
*/
|
||||||
|
if (defined('WP_CLI') && WP_CLI) {
|
||||||
|
/**
|
||||||
|
* Генерирует кеш всех соавторов
|
||||||
|
*/
|
||||||
|
function generate_coauthors_cache_cli() {
|
||||||
|
WP_CLI::line('Начинаю генерацию кеша соавторов...');
|
||||||
|
$result = generate_coauthors_cache();
|
||||||
|
|
||||||
|
if ($result['success']) {
|
||||||
|
WP_CLI::success($result['message']);
|
||||||
|
WP_CLI::line("Всего соавторов: {$result['total']}");
|
||||||
|
WP_CLI::line("Файл сохранен: {$result['file']}");
|
||||||
|
} else {
|
||||||
|
WP_CLI::error($result['message']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WP_CLI::add_command('coauthors generate-cache', 'generate_coauthors_cache_cli');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Генерирует кеш всех соавторов и сохраняет в JSON файл
|
||||||
|
*
|
||||||
|
* @return array Результат операции
|
||||||
|
*/
|
||||||
|
function generate_coauthors_cache() {
|
||||||
|
if (!function_exists('get_coauthors')) {
|
||||||
|
return array(
|
||||||
|
'success' => 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';
|
||||||
|
}
|
||||||
363
inc/graphql.php
Normal file
363
inc/graphql.php
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Общая функция для получения соавторов
|
||||||
|
function get_coauthors_for_graphql($post_id) {
|
||||||
|
if (!function_exists('get_coauthors')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$coauthors = get_coauthors($post_id);
|
||||||
|
$users = [];
|
||||||
|
|
||||||
|
foreach ($coauthors as $coauthor) {
|
||||||
|
// Пропускаем если это основной автор (он уже в поле author)
|
||||||
|
// if ($coauthor->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);
|
||||||
196
inc/journal_issue.php
Normal file
196
inc/journal_issue.php
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Создаем Custom Post Type для номеров изданий
|
||||||
|
function create_journal_issue_cpt() {
|
||||||
|
$labels = array(
|
||||||
|
'name' => 'Номера изданий',
|
||||||
|
'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 '<select name="journal_issue_selected" style="width:100%;">';
|
||||||
|
echo '<option value="">— Не выбран —</option>';
|
||||||
|
|
||||||
|
foreach ($journal_issues as $issue) {
|
||||||
|
$selected = selected($selected_name, $issue->post_title, false);
|
||||||
|
echo '<option value="' . esc_attr($issue->post_title) . '" ' . $selected . '>' . esc_html($issue->post_title) . '</option>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</select>';
|
||||||
|
echo '<p class="description" style="margin-top:5px;">Выберите один номер издания</p>';
|
||||||
|
echo '<p class="description"><small><a href="' . admin_url('edit.php?post_type=journal_issue') . '">Управление номерами изданий</a></small></p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем сохранение выбранного номера
|
||||||
|
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 '<span style="background:#f0f0f0; padding:2px 6px; border-radius:3px; font-size:12px;">' . $term->name . '</span>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo '<span style="color:#ccc;">—</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 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');
|
||||||
20
inc/meta_keywords.php
Normal file
20
inc/meta_keywords.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
add_action('wp_head', function() {
|
||||||
|
// Для записей и страниц
|
||||||
|
if (is_single() || is_page()) {
|
||||||
|
$post_id = get_queried_object_id();
|
||||||
|
|
||||||
|
// Получаем термины из таксономии 'keys'
|
||||||
|
$terms = get_the_terms($post_id, 'keys');
|
||||||
|
|
||||||
|
if ($terms && !is_wp_error($terms)) {
|
||||||
|
$keywords = array();
|
||||||
|
foreach ($terms as $term) {
|
||||||
|
$keywords[] = $term->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<meta name="keywords" content="' . esc_attr(implode(', ', $keywords)) . '">' . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
0
inc/opensearch.php
Normal file
0
inc/opensearch.php
Normal file
43
inc/popular-json.php
Normal file
43
inc/popular-json.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$json_path = CACHED_TEMPLATE.'json';
|
||||||
|
|
||||||
|
$ids = array_map(
|
||||||
|
function ($item) { return $item->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));
|
||||||
222
inc/realtime-debug.php
Normal file
222
inc/realtime-debug.php
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Real-time Execution Logger for WordPress
|
||||||
|
* Логирует выполнение страницы в реальном времени
|
||||||
|
*/
|
||||||
|
|
||||||
|
class RealtimePageLogger {
|
||||||
|
private static $instance;
|
||||||
|
private $log_file;
|
||||||
|
private $request_id;
|
||||||
|
private $start_time;
|
||||||
|
|
||||||
|
public static function get_instance() {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
$this->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();
|
||||||
|
}
|
||||||
511
inc/realtimesqllogger.php
Normal file
511
inc/realtimesqllogger.php
Normal file
@@ -0,0 +1,511 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Real-time SQL Query Logger with PRE-execution tracking
|
||||||
|
*/
|
||||||
|
|
||||||
|
class RealtimeSqlLogger {
|
||||||
|
private static $instance;
|
||||||
|
private $log_file;
|
||||||
|
private $request_id;
|
||||||
|
private $start_time;
|
||||||
|
private $pending_queries = [];
|
||||||
|
private $log_enabled = true;
|
||||||
|
private $current_location = 'unknown';
|
||||||
|
|
||||||
|
public static function get_instance() {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
$this->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 '<div style="background:#1e1e1e;padding:20px;margin:20px;border:1px solid #444;font-family:monospace;font-size:11px;max-height:400px;overflow:auto;color:#fff;">';
|
||||||
|
echo '<h3 style="color:#fff;">🎯 SQL Debug Log (Live)</h3>';
|
||||||
|
echo '<pre style="color:#ccc;line-height:1.4;">' . htmlspecialchars($log_content) . '</pre>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
24
inc/rossiya.php
Normal file
24
inc/rossiya.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
function replace_category_with_tag($query) {
|
||||||
|
// Проверяем, что это главный запрос и не в админке
|
||||||
|
if (!$query->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');
|
||||||
34
inc/schedule_async_post_processing.php
Normal file
34
inc/schedule_async_post_processing.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
function schedule_async_post_processing( $post_id, $post, $update ) {
|
||||||
|
|
||||||
|
if ( wp_is_post_revision( $post_id ) ||
|
||||||
|
wp_is_post_autosave( $post_id ) ||
|
||||||
|
defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! in_array( $post->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 ); // Низкий приоритет
|
||||||
|
|
||||||
|
|
||||||
221
inc/sql_result.php
Normal file
221
inc/sql_result.php
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
add_action('wp_footer', 'display_sql_queries_in_footer', 9999);
|
||||||
|
|
||||||
|
function display_sql_queries_in_footer() {
|
||||||
|
if (!current_user_can('administrator')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if (empty($wpdb->queries)) {
|
||||||
|
echo '<div style="background: #fff; padding: 20px; margin: 20px; border: 1px solid #ccc; font-family: monospace; font-size: 14px; position: relative; z-index: 99999;">';
|
||||||
|
echo '<p>Нет данных о запросах. Убедитесь, что в wp-config.php добавлена строка:</p>';
|
||||||
|
echo '<code style="background: #f4f4f4; padding: 5px; display: block;">define( \'SAVEQUERIES\', true );</code>';
|
||||||
|
echo '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$queries = $wpdb->queries;
|
||||||
|
$total_time = 0;
|
||||||
|
foreach ($queries as $query) {
|
||||||
|
$total_time += $query[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<div id="sql-debug-panel" style="background: #fff; padding: 20px; margin: 20px; border: 1px solid #ccc; font-family: monospace; font-size: 14px; position: relative; z-index: 99999;">';
|
||||||
|
|
||||||
|
// Панель управления сортировкой
|
||||||
|
echo '<div style="margin-bottom: 20px; padding: 10px; background: #f5f5f5; border-radius: 5px;">';
|
||||||
|
echo '<h3 style="margin-top: 0; display: inline-block; margin-right: 20px;">SQL Запросы</h3>';
|
||||||
|
echo '<button onclick="sortQueries(\'time-desc\')" style="margin-right: 10px; padding: 5px 10px; background: #e74c3c; color: white; border: none; border-radius: 3px; cursor: pointer;">Самые медленные</button>';
|
||||||
|
echo '<button onclick="sortQueries(\'time-asc\')" style="margin-right: 10px; padding: 5px 10px; background: #27ae60; color: white; border: none; border-radius: 3px; cursor: pointer;">Самые быстрые</button>';
|
||||||
|
echo '<button onclick="sortQueries(\'default\')" style="margin-right: 10px; padding: 5px 10px; background: #3498db; color: white; border: none; border-radius: 3px; cursor: pointer;">По умолчанию</button>';
|
||||||
|
echo '<span style="margin-left: 20px; font-weight: bold;">Всего запросов: ' . count($queries) . '</span>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
echo '<table id="sql-queries-table" style="width: 100%; border-collapse: collapse; margin-top: 15px;">';
|
||||||
|
echo '<thead>';
|
||||||
|
echo '<tr style="background: #2c3e50; color: #fff; cursor: pointer;">';
|
||||||
|
echo '<th style="padding: 8px; border: 1px solid #ddd; text-align: left;" onclick="sortQueries(\'time-desc\')">Время ⬇</th>';
|
||||||
|
echo '<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Запрос</th>';
|
||||||
|
echo '<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Вызвавший код</th>';
|
||||||
|
echo '</tr>';
|
||||||
|
echo '</thead>';
|
||||||
|
echo '<tbody>';
|
||||||
|
|
||||||
|
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 '<tr class="sql-query-row">';
|
||||||
|
echo '<td style="padding: 8px; border: 1px solid #ddd; color: ' . $color . '; font-weight: bold;" data-time="' . $time_seconds . '">' . $time . ' ms</td>';
|
||||||
|
echo '<td style="padding: 8px; border: 1px solid #ddd; word-break: break-all;">' . htmlspecialchars($query[0]) . '</td>';
|
||||||
|
echo '<td style="padding: 8px; border: 1px solid #ddd; font-size: 12px; color: #666;">' . $query[2] . '</td>';
|
||||||
|
echo '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '</tbody>';
|
||||||
|
echo '</table>';
|
||||||
|
|
||||||
|
echo '<div style="margin-top: 15px; padding: 10px; background: #f8f9fa; border-left: 4px solid #3498db;">';
|
||||||
|
echo 'Общее время SQL запросов: <strong>' . number_format($total_time * 1000, 2) . ' ms</strong><br>';
|
||||||
|
echo 'Среднее время на запрос: <strong>' . number_format(($total_time / count($queries)) * 1000, 2) . ' ms</strong>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
// Добавляем JavaScript для сортировки
|
||||||
|
echo '
|
||||||
|
<script>
|
||||||
|
function sortQueries(sortType) {
|
||||||
|
const table = document.getElementById("sql-queries-table");
|
||||||
|
const tbody = table.querySelector("tbody");
|
||||||
|
const rows = Array.from(tbody.querySelectorAll("tr.sql-query-row"));
|
||||||
|
|
||||||
|
// Сбрасываем классы активной сортировки у заголовков
|
||||||
|
const headers = table.querySelectorAll("th");
|
||||||
|
headers.forEach(header => {
|
||||||
|
header.innerHTML = header.innerHTML.replace(" ⬇", "").replace(" ⬆", "");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Добавляем индикатор сортировки
|
||||||
|
if (sortType === "time-desc") {
|
||||||
|
headers[0].innerHTML = "Время ⬇";
|
||||||
|
} else if (sortType === "time-asc") {
|
||||||
|
headers[0].innerHTML = "Время ⬆";
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.sort((a, b) => {
|
||||||
|
const timeA = parseFloat(a.querySelector("td[data-time]").getAttribute("data-time"));
|
||||||
|
const timeB = parseFloat(b.querySelector("td[data-time]").getAttribute("data-time"));
|
||||||
|
|
||||||
|
switch(sortType) {
|
||||||
|
case "time-desc":
|
||||||
|
return timeB - timeA; // Самые медленные first
|
||||||
|
case "time-asc":
|
||||||
|
return timeA - timeB; // Самые быстрые first
|
||||||
|
case "default":
|
||||||
|
return 0; // Оригинальный порядок
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Очищаем и перезаполняем tbody
|
||||||
|
while (tbody.firstChild) {
|
||||||
|
tbody.removeChild(tbody.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем поиск по запросам
|
||||||
|
function addSearchFeature() {
|
||||||
|
const panel = document.getElementById("sql-debug-panel");
|
||||||
|
const searchHtml = \`
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<input type="text" id="sql-search" placeholder="Поиск по запросам..."
|
||||||
|
style="padding: 8px; width: 300px; border: 1px solid #ddd; border-radius: 3px;">
|
||||||
|
<button onclick="filterQueries()" style="padding: 8px 15px; background: #3498db; color: white; border: none; border-radius: 3px; cursor: pointer;">Поиск</button>
|
||||||
|
<button onclick="clearSearch()" style="padding: 8px 15px; background: #95a5a6; color: white; border: none; border-radius: 3px; cursor: pointer;">Очистить</button>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
|
||||||
|
const controlsDiv = panel.querySelector("div");
|
||||||
|
controlsDiv.insertAdjacentHTML("afterend", searchHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterQueries() {
|
||||||
|
const searchTerm = document.getElementById("sql-search").value.toLowerCase();
|
||||||
|
const rows = document.querySelectorAll("tr.sql-query-row");
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const queryText = row.querySelector("td:nth-child(2)").textContent.toLowerCase();
|
||||||
|
const callerText = row.querySelector("td:nth-child(3)").textContent.toLowerCase();
|
||||||
|
|
||||||
|
if (queryText.includes(searchTerm) || callerText.includes(searchTerm)) {
|
||||||
|
row.style.display = "";
|
||||||
|
} else {
|
||||||
|
row.style.display = "none";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSearch() {
|
||||||
|
document.getElementById("sql-search").value = "";
|
||||||
|
const rows = document.querySelectorAll("tr.sql-query-row");
|
||||||
|
rows.forEach(row => {
|
||||||
|
row.style.display = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализируем поиск при загрузке
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
addSearchFeature();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Добавляем стили и скрипты
|
||||||
|
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 '
|
||||||
|
<style>
|
||||||
|
#sql-debug-panel {
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 1200px;
|
||||||
|
box-shadow: 0 0 20px rgba(0,0,0,0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-query-row:hover {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-btn {
|
||||||
|
margin-right: 10px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-desc { background: #e74c3c; color: white; }
|
||||||
|
.sort-asc { background: #27ae60; color: white; }
|
||||||
|
.sort-default { background: #3498db; color: white; }
|
||||||
|
|
||||||
|
#sql-search {
|
||||||
|
padding: 8px;
|
||||||
|
width: 300px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
';
|
||||||
|
}
|
||||||
53
inc/test_action_scheduler.php
Normal file
53
inc/test_action_scheduler.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ТЕСТИРОВАНИЕ ACTION SCHEDULER
|
||||||
|
* Код для проверки работы фоновых задач
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Регистрируем обработчик
|
||||||
|
add_action( 'test_action_scheduler_task', 'test_action_scheduler_log_to_file', 10, 2 );
|
||||||
|
|
||||||
|
function test_action_scheduler_log_to_file( $post_id, $post_title ) {
|
||||||
|
$log_file = ABSPATH . 'action-scheduler-test.log';
|
||||||
|
|
||||||
|
$log_entry = sprintf(
|
||||||
|
"[%s] Задача выполнена | Post ID: %d | Title: %s\n",
|
||||||
|
current_time( 'Y-m-d H:i:s' ),
|
||||||
|
$post_id,
|
||||||
|
$post_title
|
||||||
|
);
|
||||||
|
|
||||||
|
file_put_contents( $log_file, $log_entry, FILE_APPEND | LOCK_EX );
|
||||||
|
//error_log( 'Action Scheduler Test: ' . trim( $log_entry ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
function test_schedule_action_on_save_post( $post_id, $post, $update ) {
|
||||||
|
// Проверки
|
||||||
|
if ( wp_is_post_revision( $post_id ) ||
|
||||||
|
wp_is_post_autosave( $post_id ) ||
|
||||||
|
defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! in_array( $post->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 );
|
||||||
|
|
||||||
159
inc/wp-cli-scheduler-commands.php
Normal file
159
inc/wp-cli-scheduler-commands.php
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
class Scheduler_CLI_Commands extends WP_CLI_Command {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Принудительно запустить задачу Action Scheduler, даже если она failed
|
||||||
|
*
|
||||||
|
* ## OPTIONS
|
||||||
|
*
|
||||||
|
* <action_id>
|
||||||
|
* : 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
|
||||||
|
*
|
||||||
|
* <action_id>
|
||||||
|
* : 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' );
|
||||||
Reference in New Issue
Block a user