add files inc
This commit is contained in:
@@ -47,11 +47,8 @@ add_action('rest_api_init', function() {
|
||||
'photo' => $photo,
|
||||
'photo_sizes' => $photo_sizes,
|
||||
|
||||
// Оставляем gravatar как запасной вариант, если фото нет
|
||||
'gravatar' => get_avatar_url($coauthor->user_email, ['size' => 192]),
|
||||
|
||||
// Для обратной совместимости
|
||||
'avatar' => $photo ?: get_avatar_url($coauthor->user_email, ['size' => 192]),
|
||||
'avatar' => $photo ?: '',
|
||||
|
||||
'url' => get_author_posts_url($coauthor->ID, $coauthor->user_nicename),
|
||||
'type' => $coauthor->type ?? 'guest-author'
|
||||
|
||||
@@ -24,6 +24,7 @@ if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
require "inc/action-scheduler-functions.php";
|
||||
require "inc/wp-cli-scheduler-commands.php";
|
||||
require "inc/adfox_on.php"; // управление adfox
|
||||
require "inc/generate_coauthors_cache.php"; // генерим кеш списка авторов
|
||||
}
|
||||
|
||||
include "inc/get_cached_alm.php";
|
||||
|
||||
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