Use your hosting provider's native API to purge the entire site cache in one click.
These options are independent of cache plugins and target the server-level cache layer.
' . esc_html__('Settings saved but sitemap generation failed: the sitemap data could not be stored, so no sitemap is being served. Check your object cache and database write settings, then try again.', 'metasync') . '
' . esc_html__('Video sitemap generation failed. Check if it is enabled and no conflicting plugins are active.', 'metasync') . '
';
}
}
// Handle form submissions
if (isset($_POST['metasync_sitemap_nonce'])) {
check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce');
if (isset($_POST['generate_sitemap'])) {
// Auto-disable other sitemap generators before generating
$disabled_plugins = $sitemap_generator->disable_other_sitemap_generators();
// Generate news/video sitemaps FIRST so they exist when the main sitemap builds its index
$news_opts = get_option('metasync_news_sitemap_settings', []);
$video_opts = get_option('metasync_video_sitemap_settings', []);
$extras = [];
if (!empty($news_opts['enabled'])) {
if ($sitemap_generator->generate_news_sitemap()) {
$extras[] = 'news';
}
}
// Reclaim unreferenced memory between the news and video passes
// so the video generator starts with more headroom (they share
// one request). Each generator already releases its own object
// cache via clean_post_cache(), so no cache flush is needed here.
if (function_exists('gc_collect_cycles')) {
gc_collect_cycles();
}
if (!empty($video_opts['enabled'])) {
if ($sitemap_generator->generate_video_sitemap()) {
$extras[] = 'video';
}
}
// Same inter-pass GC before the main sitemap pass begins.
if (function_exists('gc_collect_cycles')) {
gc_collect_cycles();
}
// Generate main sitemap (its index will include news/video since they now exist)
$result = $sitemap_generator->generate_sitemap();
if (is_wp_error($result)) {
$error_msg = $result->get_error_message();
error_log('[MetaSync] Sitemap generation failed: ' . $error_msg);
echo '
' . esc_html__('Sitemap generation failed: the sitemap data could not be stored, so no sitemap is being served. Check your object cache and database write settings, then try again.', 'metasync') . '
';
} else {
$message = esc_html__('Sitemap generated successfully!', 'metasync');
if (!empty($extras)) {
$message .= ' ' . sprintf(
esc_html__('Also generated %s sitemap(s).', 'metasync'),
implode(' & ', $extras)
);
}
if ($disabled_plugins) {
$message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
}
// Check if robots.txt was updated
$robots_result = get_transient('metasync_sitemap_robots_updated');
if ($robots_result && $robots_result['success']) {
if ($robots_result['action'] === 'added') {
$message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
} elseif ($robots_result['action'] === 'updated') {
$message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
} elseif ($robots_result['action'] === 'created') {
$message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
}
delete_transient('metasync_sitemap_robots_updated');
}
echo '
';
} elseif (isset($_POST['delete_general_sitemap'])) {
$deleted = $sitemap_generator->delete_sitemap('general');
if ($deleted) {
// Disable auto-update only when the general sitemap is removed
update_option('metasync_sitemap_auto_update', false);
// Re-enable WP core sitemap only if no other MetaSync sitemaps remain
if (!$sitemap_generator->sitemap_exists()) {
delete_option('metasync_disable_wp_sitemap');
}
echo '
' . esc_html__('Failed to delete video sitemap. The file may not exist or is not writable.', 'metasync') . '
';
}
} elseif (isset($_POST['delete_sitemap'])) {
// Delete all sitemaps: main + news + video (handled by delete_sitemap)
$deleted = $sitemap_generator->delete_sitemap();
if ($deleted) {
// Also disable auto-update when deleting
update_option('metasync_sitemap_auto_update', false);
// Re-enable WP core sitemap so the site isn't left with zero sitemaps
delete_option('metasync_disable_wp_sitemap');
echo '
' . esc_html__('Failed to delete sitemaps. The files may not exist or are not writable.', 'metasync') . '
';
}
} elseif (isset($_POST['enable_other_sitemaps'])) {
// Re-enable other sitemap plugins
$enabled_plugins = $sitemap_generator->enable_other_sitemap_generators();
if ($enabled_plugins) {
echo '
' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '
';
} else {
echo '
' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '
';
}
}
}
// Get sitemap info
$sitemap_exists = $sitemap_generator->sitemap_exists();
$sitemap_url = $sitemap_generator->get_sitemap_url();
$url_count = $sitemap_generator->count_urls();
$last_generated = $sitemap_generator->get_last_generated_time();
$auto_update_enabled = get_option('metasync_sitemap_auto_update', false);
$active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins();
// Main sitemap content settings
$sitemap_settings = get_option('metasync_sitemap_settings', [
'post_types' => [],
'categories' => [],
'tags' => [],
'taxonomies' => [],
'excluded_urls' => '',
]);
// News and video sitemap settings for tabs
$news_settings = get_option('metasync_news_sitemap_settings', [
'enabled' => false,
'post_types' => ['post'],
'categories' => [],
'tags' => [],
'taxonomies' => [],
'excluded_urls' => '',
'publication_name' => '',
'publication_language' => '',
]);
$video_settings = get_option('metasync_video_sitemap_settings', [
'enabled' => false,
'post_types' => ['post', 'page'],
'auto_detect' => true,
'taxonomies' => [],
'excluded_urls' => '',
]);
// Load view
require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php';
}
/**
* Custom Pages page callback
*/
public function create_admin_custom_pages_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_custom_pages_page();
}
/**
* 404 Monitor page callback
*/
public function create_admin_404_monitor_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_404_monitor_page();
}
/**
* SEO Health dashboard page callback
*/
public function create_admin_seo_health_page()
{
require_once plugin_dir_path(__FILE__) . 'class-metasync-seo-health.php';
Metasync_SEO_Health::get_instance()->render_page();
}
/**
* Site Verification page callback
*/
public function create_admin_search_engine_verification_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_search_engine_verification_page();
}
/**
* Local Business page callback
*/
public function create_admin_local_business_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_local_business_page();
}
/**
* Code Snippets page callback
*/
public function create_admin_code_snippets_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_code_snippets_page();
}
/**
* Schema Markup settings page callback
*/
public function create_admin_schema_markup_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_schema_markup_page();
}
/**
* Breadcrumbs settings page callback
*/
public function create_admin_breadcrumbs_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_breadcrumbs_page();
}
/**
* Google Instant Index Setting page callback
*/
public function create_admin_google_instant_index_page()
{
$this->render_layout_open('Instant Indexing', 'instant_index', 'Submit URLs to Google for instant indexing via the Indexing API.');
// Render shared Google Index credentials section
if (!function_exists('google_index_direct')) {
if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
} else {
error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php');
return;
}
}
$google_index = google_index_direct();
$service_info = $google_index->get_service_account_info();
$is_configured = !isset($service_info['error']);
$saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-index-api-settings.php';
// Render post types selection with save form
$options = get_option('metasync_options_instant_indexing', ['post_types' => []]);
$post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
?>
render_layout_close();
}
/**
* Google Console page callback
*/
public function create_admin_google_console_page()
{
$this->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
$service_info = function_exists('google_index_direct') ? google_index_direct()->get_service_account_info() : ['error' => 'Module not loaded'];
$is_configured = !isset($service_info['error']);
include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php';
$this->render_layout_close();
}
/**
* Bing Console page callback
*/
public function create_admin_bing_console_page()
{
$this->render_layout_open('Bing Console', 'bing_console', 'Submit URLs to Bing for instant indexing via IndexNow.');
require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
$bing_instant_index = new Metasync_Bing_Instant_Index();
$bing_instant_index->show_bing_instant_indexing_console();
$this->render_layout_close();
}
/**
* Global Options page callback
*/
public function create_admin_global_settings_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_global_settings_page();
}
/**
* Common Meta Options page callback
*/
public function create_admin_common_meta_settings_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_common_meta_settings_page();
}
/**
* Social meta page callback
*/
public function create_admin_social_meta_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_social_meta_page();
}
/**
* Indexation Control page callback
*/
public function create_admin_seo_controls_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_seo_controls_page();
}
/**
* redirection page callback with tabs
*/
public function create_admin_redirections_page()
{
Metasync_Redirections_Admin::get_instance($this->db_redirection, $this)->create_admin_redirections_page();
}
/**
* Display transient error/success messages for redirections
*/
public function display_redirection_messages()
{
Metasync_Redirections_Admin::get_instance($this->db_redirection, $this)->display_redirection_messages();
}
/**
* Display admin notice when batch processing was deferred due to high CPU load.
* Reads transient set by Metasync_CPU_Monitor::record_deferral() and clears it.
*/
public function display_cpu_deferral_notice()
{
$data = get_transient( Metasync_CPU_Monitor::DEFER_NOTICE_TRANSIENT );
if ( ! $data || ! is_array( $data ) ) {
return;
}
delete_transient( Metasync_CPU_Monitor::DEFER_NOTICE_TRANSIENT );
echo '
';
printf(
/* translators: 1: plugin name, 2: current load, 3: threshold, 4: core count */
esc_html__( '%1$s: Batch processing was deferred — server CPU load (%2$s) exceeded the threshold (%3$s on %4$s cores). Processing will resume automatically.', 'metasync' ),
esc_html( Metasync::get_effective_plugin_name() ),
esc_html( $data['load'] ),
esc_html( $data['threshold'] ),
esc_html( $data['cores'] )
);
echo '
';
}
/**
* Display info notice when another SEO plugin also generates /llms.txt.
*
* MetaSync always serves its own version when enabled (priority 1). This
* notice simply informs the admin that another plugin was detected.
*/
public function display_llms_txt_conflict_notice()
{
if (!get_transient('metasync_llms_conflict')) {
return;
}
echo '
';
echo esc_html(sprintf(__('Note: Another SEO plugin (Yoast, Rank Math, or AIOSEO) may also be generating /llms.txt. %s\'s version takes priority when enabled.', 'metasync'), Metasync::get_effective_plugin_name()));
echo '
';
}
/**
* AJAX handler for updating database structure
*/
public function ajax_update_db_structure()
{
Metasync_Admin_Ajax::instance()->ajax_update_db_structure();
}
/**
* AJAX handler to save wizard progress
*
* @since 1.0.0
*/
public function ajax_save_wizard_progress()
{
Metasync_Admin_Ajax::instance()->ajax_save_wizard_progress();
}
/**
* AJAX handler to complete wizard
*
* @since 1.0.0
*/
public function ajax_complete_wizard()
{
Metasync_Admin_Ajax::instance()->ajax_complete_wizard();
}
/**
* AJAX handler to validate robots.txt content
*/
public function ajax_validate_robots()
{
Metasync_Admin_Ajax::instance()->ajax_validate_robots();
}
/**
* AJAX handler to get default robots.txt content
*/
public function ajax_get_default_robots()
{
Metasync_Admin_Ajax::instance()->ajax_get_default_robots();
}
/**
* AJAX handler to preview robots.txt backup content
*/
public function ajax_preview_robots_backup()
{
Metasync_Admin_Ajax::instance()->ajax_preview_robots_backup();
}
/**
* AJAX handler to delete robots.txt backup
*/
public function ajax_delete_robots_backup()
{
Metasync_Admin_Ajax::instance()->ajax_delete_robots_backup();
}
/**
* AJAX handler to restore robots.txt backup
*/
public function ajax_restore_robots_backup()
{
Metasync_Admin_Ajax::instance()->ajax_restore_robots_backup();
}
/**
* AJAX handler to fetch a paginated page of robots.txt backups
*/
public function ajax_get_robots_backups()
{
Metasync_Admin_Ajax::instance()->ajax_get_robots_backups();
}
public function ajax_create_redirect_from_404()
{
Metasync_Admin_Ajax::instance()->ajax_create_redirect_from_404();
}
/**
* AJAX handler for testing host blocking with GET request
*/
public function ajax_test_host_blocking_get()
{
Metasync_Admin_Ajax::instance()->ajax_test_host_blocking_get();
}
/**
* AJAX handler for testing host blocking with POST request
*/
public function ajax_test_host_blocking_post()
{
Metasync_Admin_Ajax::instance()->ajax_test_host_blocking_post();
}
/**
* Register REST API endpoint for ping
*/
public function register_ping_rest_endpoint()
{
register_rest_route('metasync/v1', '/ping', array(
'methods' => array('GET', 'POST'),
'callback' => array($this, 'handle_ping_rest_endpoint'),
'permission_callback' => '__return_true', // Allow public access
'args' => array(
'test' => array(
'description' => 'Optional test parameter',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
),
));
}
/**
* Handle REST API ping endpoint
*/
public function handle_ping_rest_endpoint($request)
{
// Get request method
$method = $request->get_method();
// Prepare response data
$response_data = array(
'response' => 'pong',
'method' => $method,
'timestamp' => current_time('mysql'),
'site_url' => home_url(),
'plugin_version' => METASYNC_VERSION
);
// Add request data for POST requests
if ($method === 'POST') {
$body = $request->get_body();
if (!empty($body)) {
$response_data['received_data'] = json_decode($body, true);
}
// Add any query parameters
$params = $request->get_params();
if (!empty($params)) {
$response_data['query_params'] = $params;
}
}
// Add test parameter if provided
$test_param = $request->get_param('test');
if (!empty($test_param)) {
$response_data['test_param'] = $test_param;
}
return new WP_REST_Response($response_data, 200);
}
/**
* Site error logs page callback
*/
public function create_admin_error_logs_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_error_logs_page();
}
/**
* Compatibility page callback
*/
public function create_admin_compatibility_page()
{
Metasync_Compatibility_Checker::instance()->create_admin_compatibility_page($this);
}
/**
* Sync Log page callback
*/
public function create_admin_sync_log_page()
{
// Classes are now autoloaded
$sync_db = new Metasync_Sync_History_Database();
// Handle AJAX requests for sync log data
if (wp_doing_ajax()) {
$this->handle_sync_log_ajax();
return;
}
// Get pagination parameters
$page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
$per_page = Metasync_Per_Page_Helper::resolve('sync_log', 10);
$offset = ($page - 1) * $per_page;
// Get filters
$filters = [
// UI exposes date_range and status only. We compute date_from/date_to based on date_range
'date_range' => isset($_GET['date_range']) ? sanitize_text_field(wp_unslash($_GET['date_range'])) : '',
'status' => isset($_GET['status']) ? sanitize_text_field(wp_unslash($_GET['status'])) : '',
];
// Map date_range to concrete date_from/date_to for DB queries
$date_range = $filters['date_range'];
$wp_now_ts = current_time('timestamp');
$date_from = '';
$date_to = '';
if (!empty($date_range)) {
// End boundary is now by default
$date_to = date('Y-m-d H:i:s', $wp_now_ts);
if ($date_range === 'today') {
$start_ts = strtotime('today', $wp_now_ts);
$date_from = date('Y-m-d H:i:s', $start_ts);
} elseif ($date_range === 'yesterday') {
$start_ts = strtotime('yesterday', $wp_now_ts);
$end_ts = strtotime('today', $wp_now_ts) - 1; // end of yesterday
$date_from = date('Y-m-d H:i:s', $start_ts);
$date_to = date('Y-m-d H:i:s', $end_ts);
} elseif ($date_range === 'this_week') {
$start_of_week = (int) get_option('start_of_week', 1); // 0=Sun, 1=Mon
$day_of_week = (int) date('w', $wp_now_ts); // 0=Sun..6=Sat
// Convert start_of_week to PHP's 0..6 where 0=Sunday
$delta_days = ($day_of_week - $start_of_week + 7) % 7;
$start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
$date_from = date('Y-m-d H:i:s', $start_ts);
} elseif ($date_range === 'this_month') {
$start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
$date_from = date('Y-m-d H:i:s', $start_ts);
} elseif ($date_range === 'all') {
// no bounds
}
}
if (!empty($date_from)) {
$filters['date_from'] = $date_from;
}
if (!empty($date_to)) {
$filters['date_to'] = $date_to;
}
// Remove empty filters
$filters = array_filter($filters);
// Get sync history records
$sync_records = $sync_db->getAllRecords($per_page, $offset, $filters);
$total_records = $sync_db->get_count($filters);
$total_pages = ceil($total_records / $per_page);
// Get statistics
$stats = $sync_db->get_statistics();
$this->render_layout_open('Changes Log', 'sync_log', 'Track recent content synchronizations and changes from external tools.');
?>
Changes Log
Recent content synchronizations from external tools.
Records are automatically removed after 90 days.
No sync records found
Sync records will appear here when content/pages receive new updates.
render_layout_close(); ?>
$value) {
if (!empty($value)) {
$query_parts[] = $key . '=' . urlencode($value);
}
}
// Preserve the user-selected results-per-page value across page
// navigation so a non-default page size survives clicking a page link.
$per_page_key = Metasync_Per_Page_Helper::request_key('sync_log');
if (isset($_GET[$per_page_key])) {
$per_page = (int) $_GET[$per_page_key];
if (in_array($per_page, Metasync_Per_Page_Helper::allowed_values(), true)) {
$query_parts[] = $per_page_key . '=' . $per_page;
}
}
return !empty($query_parts) ? '&' . implode('&', $query_parts) : '';
}
/**
* Handle AJAX requests for sync log data
*/
private function handle_sync_log_ajax()
{
// This can be used for future AJAX functionality like real-time updates
wp_die();
}
/**
* AJAX: Clear all Sync Log records (admin-only, nonce protected).
*/
public function ajax_clear_sync_log()
{
check_ajax_referer('metasync_clear_sync_log', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
}
$sync_db = new Metasync_Sync_History_Database();
$sync_db->clear_logs();
wp_send_json_success(['message' => 'Sync log cleared successfully.']);
}
/**
* AJAX: Rollback a single MCP Client sync history entry.
*/
public function ajax_rollback_mcp_change()
{
check_ajax_referer('metasync_rollback_mcp_change', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
}
$id = isset($_POST['sync_history_id']) ? intval($_POST['sync_history_id']) : 0;
if (!$id) {
wp_send_json_error(['message' => 'Invalid sync history ID.']);
}
$result = Metasync_MCP_Sync_Logger::rollback($id);
if ($result['success']) {
wp_send_json_success(['message' => $result['message']]);
} else {
wp_send_json_error(['message' => $result['message']]);
}
}
/**
* Render compatibility sections
*/
private function render_compatibility_sections()
{
Metasync_Compatibility_Checker::instance()->render_compatibility_sections();
}
/**
* Render Page Builders section
*/
private function render_page_builders_section()
{
Metasync_Compatibility_Checker::instance()->render_page_builders_section();
}
/**
* Render SEO Plugins section
*/
private function render_seo_plugins_section()
{
Metasync_Compatibility_Checker::instance()->render_seo_plugins_section();
}
/**
* Render Cache Plugins section
*/
private function render_cache_plugins_section()
{
Metasync_Compatibility_Checker::instance()->render_cache_plugins_section();
}
/**
* Render Lock Section button for protected tabs
*
* @param string $tab The tab identifier (general, whitelabel, advanced)
*/
private function render_lock_button($tab)
{
Metasync_Compatibility_Checker::instance()->render_lock_button($tab);
}
/**
* Get Page Builders compatibility information
*/
private function get_page_builders_compatibility()
{
return Metasync_Compatibility_Checker::instance()->get_page_builders_compatibility();
}
/**
* Get SEO Plugins compatibility information
*/
private function get_seo_plugins_compatibility()
{
return Metasync_Compatibility_Checker::instance()->get_seo_plugins_compatibility();
}
/**
* Get Cache Plugins compatibility information
*/
private function get_cache_plugins_compatibility()
{
return Metasync_Compatibility_Checker::instance()->get_cache_plugins_compatibility();
}
/**
* Check if a plugin is installed and active
* @deprecated Use get_plugin_status() instead
*/
private function is_plugin_installed($plugin_file)
{
return Metasync_Compatibility_Checker::instance()->is_plugin_installed($plugin_file);
}
/**
* Get detailed plugin status (installed and/or active)
* Checks multiple plugin file paths (e.g., free and premium versions)
*
* @param array $plugin_files Array of plugin file paths to check (e.g., ['free/plugin.php', 'pro/plugin.php'])
* @param bool $is_core Whether this is a WordPress core feature (always installed/active)
* @param string $theme_name Optional theme name to check if it's a theme instead of plugin
* @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null]
*/
private function get_plugin_status($plugin_files, $is_core = false, $theme_name = null)
{
return Metasync_Compatibility_Checker::instance()->get_plugin_status($plugin_files, $is_core, $theme_name);
}
/**
* Get plugin logo URL (optimized for performance)
*/
private function get_plugin_logo($plugin_key, $type)
{
return Metasync_Compatibility_Checker::instance()->get_plugin_logo($plugin_key, $type);
}
public function creat_error_Logs_List()
{
Metasync_Admin_Pages::get_instance($this)->creat_error_Logs_List();
}
/**
* Site error logs page callback
*/
public function create_admin_heartbeat_error_logs_page()
{
Metasync_Admin_Pages::get_instance($this)->create_admin_heartbeat_error_logs_page();
}
/**
* Handle session management early for whitelabel functionality
*/
private function handle_session_management_early()
{
Metasync_Connect_Manager::instance()->handle_session_management_early();
}
/**
* @deprecated 2.5.12 Use Metasync_Auth_Manager instead of sessions for authentication
*/
private function safe_session_start() {
// This method is deprecated and no longer used
// Authentication now uses Metasync_Auth_Manager with WordPress transients and user meta
_deprecated_function(__METHOD__, '2.5.12', 'Metasync_Auth_Manager');
return Metasync_Session_Helper::safe_start();
}
private function handle_whitelabel_session_logic()
{
Metasync_Connect_Manager::instance()->handle_whitelabel_session_logic();
}
private function handle_whitelabel_password_early()
{
Metasync_Connect_Manager::instance()->handle_whitelabel_password_early();
}
/**
* Get accordion sections configuration for General Settings
*
* @return array Accordion sections with field IDs, icons, and descriptions
*/
private function get_accordion_sections_config() {
return Metasync_Settings_Fields::instance()->get_accordion_sections_config();
}
/**
* Get accordion sections configuration for Advanced Settings Tab
*
* @return array Accordion sections configuration
*/
private function get_advanced_accordion_config() {
return Metasync_Settings_Fields::instance()->get_advanced_accordion_config();
}
/**
* Render accordion sections for Advanced Settings Tab
*/
public function render_advanced_accordion() {
Metasync_Settings_Fields::instance()->render_advanced_accordion();
}
/**
* Render reset settings section for Advanced tab accordion
*/
/**
* Render CPU Monitor section for Performance accordion
*/
public function render_cpu_monitor_section() {
$cpu_monitor = new Metasync_CPU_Monitor();
$stats = Metasync_CPU_Monitor::get_stats();
$per_core_threshold = Metasync_CPU_Monitor::get_per_core_threshold();
$cores = Metasync_CPU_Monitor::get_cpu_core_count();
$effective_threshold = Metasync_CPU_Monitor::get_effective_threshold();
$detection_reliable = Metasync_CPU_Monitor::is_core_detection_reliable();
?>
core
Not detected
Automatically detected on this system.
Core detection is not available on this hosting environment.
Set the load average per CPU core (0.5–10.0). Default: 2.0
Calculated as: cores × per-core threshold
CPU Load Statistics
Total Deferrals
Max Load Observed
Average Load
render_reset_settings_section();
}
/**
* Render Google Index API section for Indexation Control page
*/
public function render_google_index_section() {
Metasync_Settings_Fields::instance()->render_google_index_section();
}
/**
* Render Bing Index (IndexNow) section
*
* @since 2.6.0
* @return void
*/
public function render_bing_index_section() {
Metasync_Settings_Fields::instance()->render_bing_index_section();
}
/**
* Render Plugin Access Roles section for Advanced tab accordion
*/
private function render_plugin_access_roles_section() {
Metasync_Settings_Fields::instance()->render_plugin_access_roles_section();
}
/**
* Check if the current user has access to the plugin based on role settings
* Wrapper method that delegates to the common Metasync::current_user_has_plugin_access()
* but also requires manage_options capability for admin area access
*
* @return bool True if user has access, false otherwise
*/
public function current_user_has_plugin_access() {
return Metasync::current_user_has_plugin_access();
}
/**
* Get default execution settings
*
* @return array Default execution settings
*/
private function get_default_execution_settings() {
return Metasync_Settings_Fields::instance()->get_default_execution_settings();
}
/**
* Get execution setting value
*
* @param string $key Setting key
* @param mixed $default Default value if setting doesn't exist
* @return mixed Setting value or default
*/
public function get_execution_setting($key, $default = null) {
return Metasync_Settings_Fields::instance()->get_execution_setting($key, $default);
}
/**
* Get all execution settings
*
* @return array All execution settings with defaults merged
*/
public function get_all_execution_settings() {
return Metasync_Settings_Fields::instance()->get_all_execution_settings();
}
/**
* Check if server allows changing memory limit
* Tests if ini_set('memory_limit') is allowed
*
* @return bool True if memory limit can be changed, false otherwise
*/
private function can_change_memory_limit() {
return Metasync_Settings_Fields::instance()->can_change_memory_limit();
}
/**
* Get PHP server limits for display
*
* @return array Server limits (execution_time, memory_limit, can_change_memory)
*/
private function get_server_limits() {
return Metasync_Settings_Fields::instance()->get_server_limits();
}
/**
* Apply memory limit from execution settings
* Only applies if server allows changing memory limit
*
* @return bool True if memory limit was applied, false otherwise
*/
public function apply_memory_limit() {
return Metasync_Settings_Fields::instance()->apply_memory_limit();
}
/**
* Parse memory limit string to MB
*
* @param string $memory_limit Memory limit string (e.g., "256M", "1G")
* @return int Memory limit in MB
*/
private function parse_memory_limit_to_mb($memory_limit) {
return Metasync_Settings_Fields::instance()->parse_memory_limit_to_mb($memory_limit);
}
/**
* Render Execution Settings section for Advanced tab accordion
*/
private function render_execution_settings_section() {
Metasync_Settings_Fields::instance()->render_execution_settings_section();
}
/**
* Get tooltip content for settings fields
*
* @return array Field ID => Tooltip text mapping
*/
private function get_field_tooltips() {
return Metasync_Settings_Fields::instance()->get_field_tooltips();
}
/**
* Get the section key for a given field ID
*
* @param string $field_id The settings field ID
* @return string|null Section key or null if not found
*/
private function get_field_section($field_id) {
return Metasync_Settings_Fields::instance()->get_field_section($field_id);
}
/**
* Render accordion sections for General Settings
*
* @param string $page The settings page slug
*/
public function render_accordion_sections($page) {
Metasync_Settings_Fields::instance()->render_accordion_sections($page);
}
/**
* Register and add settings
*/
public function settings_page_init()
{
Metasync_Settings_Registration::instance()->settings_page_init();
}
/**
* Sanitize each setting field as needed
*
* @param array $input Contains all settings fields as array keys
*/
public function sanitize($input)
{
return Metasync_Settings_Registration::instance()->sanitize($input);
}
public function metasync_settings_genkey_callback()
{
Metasync_Settings_Fields::instance()->metasync_settings_genkey_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function linkgraph_token_callback()
{
Metasync_Settings_Fields::instance()->linkgraph_token_callback();
}
private function time_elapsed_string($datetime, $full = false)
{
return Metasync_Settings_Fields::instance()->time_elapsed_string($datetime, $full);
}
/**
* Get the settings option array and print one of its values
*/
public function searchatlas_api_key_callback()
{
Metasync_Settings_Fields::instance()->searchatlas_api_key_callback();
}
/**
* Site Verification Tools
*
* Bing Site Verification
* Baidu Site Verification
* Alexa Site Verification
* Yandex Site Verification
* Google Site Verification
* Pinterest Site Verification
* Norton Safe Web Site Verification
*/
/**
* Get the settings option array and print one of its values
*/
public function bing_site_verification_callback()
{
Metasync_Settings_Fields::instance()->bing_site_verification_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function yandex_site_verification_callback()
{
Metasync_Settings_Fields::instance()->yandex_site_verification_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function google_site_verification_callback()
{
Metasync_Settings_Fields::instance()->google_site_verification_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function pinterest_site_verification_callback()
{
Metasync_Settings_Fields::instance()->pinterest_site_verification_callback();
}
/**
* Local SEO for business and person
*
*/
/**
* Get the settings option array and print one of its values
*/
public function local_seo_person_organization_callback()
{
Metasync_Settings_Fields::instance()->local_seo_person_organization_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_name_callback()
{
Metasync_Settings_Fields::instance()->local_seo_name_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_logo_callback()
{
Metasync_Settings_Fields::instance()->local_seo_logo_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_url_callback()
{
Metasync_Settings_Fields::instance()->local_seo_url_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_email_callback()
{
Metasync_Settings_Fields::instance()->local_seo_email_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_phone_callback()
{
Metasync_Settings_Fields::instance()->local_seo_phone_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_address_callback()
{
Metasync_Settings_Fields::instance()->local_seo_address_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_business_type_callback()
{
Metasync_Settings_Fields::instance()->local_seo_business_type_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_opening_hours_callback()
{
Metasync_Settings_Fields::instance()->local_seo_opening_hours_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_phone_numbers_callback()
{
Metasync_Settings_Fields::instance()->local_seo_phone_numbers_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_price_range_callback()
{
Metasync_Settings_Fields::instance()->local_seo_price_range_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_about_page_callback()
{
Metasync_Settings_Fields::instance()->local_seo_about_page_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_contact_page_callback()
{
Metasync_Settings_Fields::instance()->local_seo_contact_page_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_map_key_callback()
{
Metasync_Settings_Fields::instance()->local_seo_map_key_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function local_seo_geo_coordinates_callback()
{
Metasync_Settings_Fields::instance()->local_seo_geo_coordinates_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function header_snippets_callback()
{
Metasync_Settings_Fields::instance()->header_snippets_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function footer_snippets_callback()
{
Metasync_Settings_Fields::instance()->footer_snippets_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function common_robot_meta_tags_callback()
{
Metasync_Settings_Fields::instance()->common_robot_meta_tags_callback();
}
/**
* Backward compatibility alias for common_robot_mata_tags_callback
* @deprecated Use common_robot_meta_tags_callback() instead
*/
public function common_robot_mata_tags_callback()
{
Metasync_Settings_Fields::instance()->common_robot_mata_tags_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function advance_robot_meta_tags_callback()
{
Metasync_Settings_Fields::instance()->advance_robot_meta_tags_callback();
}
/**
* Backward compatibility alias for advance_robot_mata_tags_callback
* @deprecated Use advance_robot_meta_tags_callback() instead
*/
public function advance_robot_mata_tags_callback()
{
Metasync_Settings_Fields::instance()->advance_robot_mata_tags_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function global_twitter_card_type_callback()
{
Metasync_Settings_Fields::instance()->global_twitter_card_type_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function global_open_graph_meta_callback()
{
Metasync_Settings_Fields::instance()->global_open_graph_meta_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function global_facebook_meta_callback()
{
Metasync_Settings_Fields::instance()->global_facebook_meta_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function global_twitter_meta_callback()
{
Metasync_Settings_Fields::instance()->global_twitter_meta_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function og_image_dimensions_callback()
{
Metasync_Settings_Fields::instance()->og_image_dimensions_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function article_timestamps_callback()
{
Metasync_Settings_Fields::instance()->article_timestamps_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function article_author_callback()
{
Metasync_Settings_Fields::instance()->article_author_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function article_section_callback()
{
Metasync_Settings_Fields::instance()->article_section_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function article_tags_callback()
{
Metasync_Settings_Fields::instance()->article_tags_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function twitter_image_alt_callback()
{
Metasync_Settings_Fields::instance()->twitter_image_alt_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function facebook_page_url_callback()
{
Metasync_Settings_Fields::instance()->facebook_page_url_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function facebook_authorship_callback()
{
Metasync_Settings_Fields::instance()->facebook_authorship_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function facebook_admin_callback()
{
Metasync_Settings_Fields::instance()->facebook_admin_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function facebook_app_callback()
{
Metasync_Settings_Fields::instance()->facebook_app_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function facebook_secret_callback()
{
Metasync_Settings_Fields::instance()->facebook_secret_callback();
}
/**
* Get the settings option array and print one of its values
*/
public function twitter_username_callback()
{
Metasync_Settings_Fields::instance()->twitter_username_callback();
}
/**
* Get business types as choices in local business.
*
* @return array
*/
public static function get_business_types()
{
return Metasync_Settings_Fields::get_business_types();
}
/**
* Display a dashboard warning when using the plain permalink structure.
* @param $data An array of data passed.
*/
public function permalink_structure_dashboard_warning() {
$current_permalink_structure = get_option('permalink_structure');
# Get the plugin name using centralized method
$plugin_name = Metasync::get_effective_plugin_name();
# Check if the current permalink structure is set to "Plain"
if ($current_permalink_structure === '/%post_id%/' || $current_permalink_structure === '') {
printf(
'
Warning from %s
To ensure compatibility, please update your permalink structure to any option other than "Plain".
For any inquiries, contact support.
',
esc_html($plugin_name)
);
}
}
/**
* Show a one-time admin notice when a page builder is detected but the
* "Default Page Builder" setting has never been explicitly saved.
*/
public function display_page_builder_notice() {
// Only relevant on the plugin's own settings page — avoid repeating
// this notice across every admin screen.
if (!isset($_GET['page']) || $_GET['page'] !== self::$page_slug) {
return;
}
$configured = Metasync::get_option('general')['default_page_builder'] ?? '';
// Setting already saved — nothing to warn about
if (!empty($configured)) {
return;
}
// Check if user dismissed this notice
$dismissed = get_user_meta(get_current_user_id(), 'metasync_builder_notice_dismissed', true);
if ($dismissed) {
return;
}
// Handle dismiss action
if (isset($_GET['metasync_dismiss_builder_notice']) && wp_verify_nonce($_GET['_wpnonce'] ?? '', 'metasync_dismiss_builder')) {
update_user_meta(get_current_user_id(), 'metasync_builder_notice_dismissed', '1');
return;
}
require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-html-to-builder-converter.php';
$detected = Metasync_HTML_To_Builder_Converter::auto_detect_builder();
// No non-Gutenberg builder detected — no need to warn
if ($detected === 'gutenberg') {
return;
}
$builders = Metasync_HTML_To_Builder_Converter::get_available_builders();
$builder_label = $builders[$detected]['label'] ?? $detected;
$plugin_name = Metasync::get_effective_plugin_name();
$settings_url = admin_url('admin.php?page=' . self::$page_slug . '&tab=general#metasync-section-content_rendering');
$dismiss_url = wp_nonce_url(add_query_arg('metasync_dismiss_builder_notice', '1'), 'metasync_dismiss_builder');
printf(
'
%s — Page Builder Detected %s is active on this site. Content synced by Content Genius currently uses Gutenberg (WordPress Block Editor) format by default.
',
esc_html($plugin_name),
esc_html($builder_label),
esc_html($builder_label),
esc_url($settings_url),
esc_url($dismiss_url)
);
}
/**
* Display update warning banner if plugin update is available
* Checks WordPress update API to see if a newer version is available
*
* @since 1.0.0
*/
public function display_update_warning_banner() {
// Get the installed version from database
$installed_version = get_option('metasync_version', '0.0.0');
// Get plugin basename for WordPress update API check
// This is the plugin file path relative to plugins directory (e.g., 'metasync/metasync.php')
$plugin_file = plugin_basename(plugin_dir_path(dirname(__FILE__)) . 'metasync.php');
// Get WordPress update plugins transient (contains available updates)
$update_plugins = get_site_transient('update_plugins');
// Check if update information exists and if our plugin has an update available
if ($update_plugins && isset($update_plugins->response) && isset($update_plugins->response[$plugin_file])) {
$update_info = $update_plugins->response[$plugin_file];
$latest_version = isset($update_info->new_version) ? $update_info->new_version : '';
// Compare installed version with latest available version
if ($latest_version && version_compare($installed_version, $latest_version, '<')) {
// Get the plugin name using centralized method
$plugin_name = Metasync::get_effective_plugin_name();
// Show admin notice with plugin name included in the message
printf(
'
Warning from %s
A new version of %s is available. Please update to the latest version to ensure compatibility and access new features.
For any inquiries, contact support.
',
esc_html($plugin_name),
esc_html($plugin_name)
);
}
}
}
/*
Method to handle Ajax request from "Indexation Control" page
*/
public function meta_sync_save_seo_controls() {
Metasync_Settings_Registration::instance()->meta_sync_save_seo_controls();
}
/**
* AJAX handler for saving Performance (CPU Load) settings
*
* Saves the CPU load threshold and returns statistics
*/
public function ajax_save_performance_settings() {
# Check nonce for security and return early if invalid
if (!isset($_POST['meta_sync_nonce']) || !wp_verify_nonce($_POST['meta_sync_nonce'], 'meta_sync_general_setting_nonce')) {
wp_send_json_error(array('message' => 'Invalid nonce'));
return;
}
# Check user capabilities
if (!Metasync::current_user_has_plugin_access()) {
wp_send_json_error(array('message' => 'Insufficient permissions'));
return;
}
# Get current options
$current_options = Metasync::get_option();
if (!is_array($current_options)) {
$current_options = array();
}
# Initialize performance section if it doesn't exist
if (!isset($current_options['performance']) || !is_array($current_options['performance'])) {
$current_options['performance'] = array();
}
# Validate and sanitize CPU load threshold
if (isset($_POST['metasync_options']['performance']['cpu_load_per_core_threshold'])) {
$threshold = floatval($_POST['metasync_options']['performance']['cpu_load_per_core_threshold']);
# Clamp value between 0.5 and 10.0
$threshold = max(0.5, min(10.0, $threshold));
$current_options['performance']['cpu_load_per_core_threshold'] = $threshold;
} else {
# Ensure default value exists
if (!isset($current_options['performance']['cpu_load_per_core_threshold'])) {
$current_options['performance']['cpu_load_per_core_threshold'] = Metasync_CPU_Monitor::DEFAULT_PER_CORE;
}
}
# Save the updated options
$result = Metasync::set_option($current_options);
if ($result) {
# Get current statistics to return
$stats = Metasync_CPU_Monitor::get_stats();
$cores = Metasync_CPU_Monitor::get_cpu_core_count();
$effective_threshold = Metasync_CPU_Monitor::get_effective_threshold();
wp_send_json_success(array(
'message' => 'Performance settings saved successfully!',
'cpu_load_per_core_threshold' => $current_options['performance']['cpu_load_per_core_threshold'],
'effective_threshold' => $effective_threshold,
'cores' => $cores,
'stats' => $stats
));
} else {
wp_send_json_error(array('message' => 'Failed to save Performance settings'));
}
}
/**
* Schedule transient cleanup cron job
* Runs daily to clean up expired transients and reduce database load
*/
public function schedule_transient_cleanup_cron()
{
// Clear any existing scheduled event first
$this->unschedule_transient_cleanup_cron();
// Schedule new cron job daily
if (!wp_next_scheduled('metasync_cleanup_transients')) {
$scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_cleanup_transients');
if (!$scheduled) {
error_log('MetaSync: Failed to schedule transient cleanup cron job');
}
}
}
/**
* Unschedule transient cleanup cron job
*/
public function unschedule_transient_cleanup_cron()
{
$timestamp = wp_next_scheduled('metasync_cleanup_transients');
if ($timestamp) {
wp_unschedule_event($timestamp, 'metasync_cleanup_transients');
error_log('MetaSync: Transient cleanup cron job unscheduled');
}
}
/**
* Maybe schedule transient cleanup cron job
* Called on init hook - always schedules for database maintenance
*/
public function maybe_schedule_transient_cleanup_cron()
{
if (!wp_next_scheduled('metasync_cleanup_transients')) {
$this->schedule_transient_cleanup_cron();
}
}
/**
* Schedule hidden post manager cron job (runs every 7 days)
* Called on init hook - always schedules for template checking
*/
public function maybe_schedule_hidden_post_check()
{
if (!wp_next_scheduled('metasync_hidden_post_check')) {
$scheduled = wp_schedule_event(time(), 'metasync_weekly', 'metasync_hidden_post_check');
if ($scheduled) {
error_log('MetaSync: Hidden post manager cron job scheduled successfully (runs every 7 days)');
} else {
error_log('MetaSync: Failed to schedule hidden post manager cron job');
}
}
}
/**
* Schedule OTTO 404 exclusion recheck cron job (runs daily)
* Rechecks URLs auto-excluded due to 404 after 7 days; removes from exclusion if now available
*/
public function maybe_schedule_otto_recheck_404_cron()
{
if (!wp_next_scheduled('metasync_otto_recheck_404_exclusions')) {
$scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_otto_recheck_404_exclusions');
if ($scheduled) {
error_log('MetaSync: OTTO 404 recheck cron job scheduled successfully (runs daily)');
} else {
error_log('MetaSync: Failed to schedule OTTO 404 recheck cron job');
}
}
}
/**
* Execute transient cleanup cron job
* Cleans up expired transients and plugin-specific transients to reduce database load
*/
public function execute_transient_cleanup()
{
Metasync_Admin_Ajax::instance()->execute_transient_cleanup();
}
// -------------------------------------------------------------------------
// DB CLEANUP — cron scheduling, execution, render, AJAX
// -------------------------------------------------------------------------
/**
* Returns saved DB cleanup settings with defaults merged in.
*/
private function get_db_cleanup_settings() {
$defaults = array(
'enabled' => false,
'clean_post_revisions' => true,
'clean_trashed_posts' => true,
'clean_trashed_comments' => true,
'clean_spam_comments' => true,
'clean_expired_transients' => true,
'clean_orphaned_postmeta' => true,
'last_run_at' => 0,
'last_run_stats' => array(),
);
$saved = get_option('metasync_db_cleanup_settings', array());
return array_merge($defaults, $saved);
}
/**
* Schedules the weekly DB cleanup cron if the feature is enabled and not yet scheduled.
*/
public function maybe_schedule_db_cleanup_cron() {
$settings = $this->get_db_cleanup_settings();
if (!empty($settings['enabled'])) {
if (!wp_next_scheduled('metasync_db_cleanup')) {
wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup');
}
} else {
$this->unschedule_db_cleanup_cron();
}
}
/**
* Removes the DB cleanup cron event.
*/
public function unschedule_db_cleanup_cron() {
$timestamp = wp_next_scheduled('metasync_db_cleanup');
if ($timestamp) {
wp_unschedule_event($timestamp, 'metasync_db_cleanup');
}
}
/**
* Cron callback: runs each enabled cleanup task and records stats.
* Never calls wp_cache_flush() — only targeted DB deletes.
*/
public function execute_db_cleanup() {
global $wpdb;
$settings = $this->get_db_cleanup_settings();
$stats = array();
$start = microtime(true);
try {
// 1. Post revisions
if (!empty($settings['clean_post_revisions'])) {
$stats['post_revisions'] = (int) $wpdb->query(
"DELETE FROM {$wpdb->posts} WHERE post_type = 'revision'"
);
// Remove postmeta left behind by deleted revisions
$wpdb->query(
"DELETE pm FROM {$wpdb->postmeta} pm
LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.ID IS NULL"
);
}
// 2. Trashed posts + their postmeta
if (!empty($settings['clean_trashed_posts'])) {
// Collect IDs first to cleanly remove postmeta
$trashed_ids = $wpdb->get_col(
"SELECT ID FROM {$wpdb->posts} WHERE post_status = 'trash'"
);
if (!empty($trashed_ids)) {
$placeholders = implode(',', array_fill(0, count($trashed_ids), '%d'));
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->postmeta} WHERE post_id IN ($placeholders)",
$trashed_ids
)
);
$stats['trashed_posts'] = (int) $wpdb->query(
"DELETE FROM {$wpdb->posts} WHERE post_status = 'trash'"
);
} else {
$stats['trashed_posts'] = 0;
}
}
// 3. Trashed comments
if (!empty($settings['clean_trashed_comments'])) {
$stats['trashed_comments'] = (int) $wpdb->query(
"DELETE FROM {$wpdb->comments} WHERE comment_approved = 'trash'"
);
}
// 4. Spam comments
if (!empty($settings['clean_spam_comments'])) {
$stats['spam_comments'] = (int) $wpdb->query(
"DELETE FROM {$wpdb->comments} WHERE comment_approved = 'spam'"
);
}
// 5. Expired transients — direct SQL, no cache flush
if (!empty($settings['clean_expired_transients'])) {
// Delete timeout rows that have already expired
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '\_transient\_timeout\_%'
AND option_value + 0 < UNIX_TIMESTAMP()"
);
// Delete value rows whose timeout row no longer exists
$stats['expired_transients'] = (int) $wpdb->query(
"DELETE o FROM {$wpdb->options} o
LEFT JOIN {$wpdb->options} t
ON t.option_name = CONCAT('_transient_timeout_', SUBSTRING(o.option_name, 12))
WHERE o.option_name LIKE '\_transient\_%'
AND o.option_name NOT LIKE '\_transient\_timeout\_%'
AND t.option_id IS NULL"
);
}
// 6. Orphaned postmeta (post_id references a post that no longer exists)
if (!empty($settings['clean_orphaned_postmeta'])) {
$stats['orphaned_postmeta'] = (int) $wpdb->query(
"DELETE pm FROM {$wpdb->postmeta} pm
LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.ID IS NULL"
);
}
$stats['execution_ms'] = round((microtime(true) - $start) * 1000, 2);
// Persist last-run timestamp and stats
$settings['last_run_at'] = time();
$settings['last_run_stats'] = $stats;
update_option('metasync_db_cleanup_settings', $settings);
error_log('MetaSync: DB cleanup completed — ' . json_encode($stats));
} catch (Exception $e) {
error_log('MetaSync: DB cleanup failed — ' . $e->getMessage());
}
}
/**
* Renders the Database Cleanup accordion section in Advanced Settings.
*/
public function render_db_cleanup_section() {
$settings = $this->get_db_cleanup_settings();
$last_run = !empty($settings['last_run_at']) ? $settings['last_run_at'] : 0;
$stats = !empty($settings['last_run_stats']) ? $settings['last_run_stats'] : array();
$next_run = wp_next_scheduled('metasync_db_cleanup');
$task_labels = array(
'clean_post_revisions' => 'Post revisions',
'clean_trashed_posts' => 'Trashed posts',
'clean_trashed_comments' => 'Trashed comments',
'clean_spam_comments' => 'Spam comments',
'clean_expired_transients' => 'Expired transients',
'clean_orphaned_postmeta' => 'Orphaned post meta',
);
?>
Remove orphaned database rows that accumulate over time and slow down queries. Runs weekly via WP-Cron when enabled.
'Invalid security token. Please refresh the page and try again.'));
return;
}
if (!Metasync::current_user_has_plugin_access()) {
wp_send_json_error(array('message' => 'Insufficient permissions.'));
return;
}
$existing = $this->get_db_cleanup_settings();
$task_keys = array(
'clean_post_revisions',
'clean_trashed_posts',
'clean_trashed_comments',
'clean_spam_comments',
'clean_expired_transients',
'clean_orphaned_postmeta',
);
$new_settings = array(
'enabled' => !empty($_POST['enabled']),
'last_run_at' => $existing['last_run_at'],
'last_run_stats' => $existing['last_run_stats'],
);
foreach ($task_keys as $key) {
$new_settings[$key] = !empty($_POST[$key]);
}
update_option('metasync_db_cleanup_settings', $new_settings);
// Reschedule based on new enabled state
if (!empty($new_settings['enabled'])) {
if (!wp_next_scheduled('metasync_db_cleanup')) {
wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup');
}
$next_run = wp_next_scheduled('metasync_db_cleanup');
$next_run_label = $next_run
? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_run)
: '';
wp_send_json_success(array(
'message' => 'Settings saved. Weekly cleanup is enabled.',
'next_run_label' => $next_run_label,
));
} else {
$this->unschedule_db_cleanup_cron();
wp_send_json_success(array(
'message' => 'Settings saved. Weekly cleanup is disabled.',
));
}
}
/**
* AJAX: Manually trigger the DB cleanup and return stats for the UI.
* Persists current form state first so unsaved checkbox changes are respected.
*/
public function ajax_run_db_cleanup() {
if (!isset($_POST['db_cleanup_settings_nonce']) ||
!wp_verify_nonce($_POST['db_cleanup_settings_nonce'], 'metasync_db_cleanup_settings_nonce')) {
wp_send_json_error(array('message' => 'Invalid security token.'));
return;
}
if (!Metasync::current_user_has_plugin_access()) {
wp_send_json_error(array('message' => 'Insufficient permissions.'));
return;
}
// Save current form state before running so the cleanup uses what the user sees
$existing = $this->get_db_cleanup_settings();
$task_keys = array(
'clean_post_revisions',
'clean_trashed_posts',
'clean_trashed_comments',
'clean_spam_comments',
'clean_expired_transients',
'clean_orphaned_postmeta',
);
$to_save = array(
'enabled' => !empty($_POST['enabled']),
'last_run_at' => $existing['last_run_at'],
'last_run_stats' => $existing['last_run_stats'],
);
foreach ($task_keys as $key) {
$to_save[$key] = !empty($_POST[$key]);
}
update_option('metasync_db_cleanup_settings', $to_save);
// Reschedule cron to match the (possibly updated) enabled state
if (!empty($to_save['enabled'])) {
if (!wp_next_scheduled('metasync_db_cleanup')) {
wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup');
}
} else {
$this->unschedule_db_cleanup_cron();
}
$this->execute_db_cleanup();
$settings = $this->get_db_cleanup_settings();
$stats = $settings['last_run_stats'];
$timestamp_label = date_i18n(
get_option('date_format') . ' ' . get_option('time_format'),
$settings['last_run_at']
);
// Build stats badges HTML
$stat_labels = array(
'post_revisions' => 'Post revisions',
'trashed_posts' => 'Trashed posts',
'trashed_comments' => 'Trashed comments',
'spam_comments' => 'Spam comments',
'expired_transients' => 'Expired transients',
'orphaned_postmeta' => 'Orphaned post meta',
);
$stats_html = '';
foreach ($stat_labels as $key => $label) {
if (isset($stats[$key])) {
$stats_html .= ''
. esc_html($label) . ': ' . intval($stats[$key]) . ' ';
}
}
if (!empty($stats['execution_ms'])) {
$stats_html .= 'in '
. esc_html($stats['execution_ms']) . 'ms';
}
wp_send_json_success(array(
'message' => 'Cleanup completed successfully.',
'timestamp_label' => $timestamp_label,
'stats_html' => $stats_html,
));
}
/**
* Control plugin auto-updates based on user setting
*
* @param bool $update Whether to update
* @param object $item Update offer
* @return bool Whether to allow auto-update
*/
public function control_plugin_auto_updates($update, $item)
{
# Check if the item object has the slug property
if (!isset($item->slug)) {
return $update;
}
// Check if this is our plugin
if ($item->slug === 'metasync') {
$general_settings = Metasync::get_option('general') ?? [];
$enable_auto_updates = $general_settings['enable_auto_updates'] ?? false;
// Return the user's preference (true = allow auto-updates, false = prevent)
return $enable_auto_updates === 'true' || $enable_auto_updates === true;
}
// For other plugins, don't interfere with their auto-update settings
return $update;
}
/**
* AJAX handler to add excluded URL for OTTO
*/
public function ajax_otto_add_excluded_url()
{
Metasync_Otto_Cache_Manager::instance()->ajax_otto_add_excluded_url();
}
/**
* AJAX handler to delete excluded URL for OTTO
*/
public function ajax_otto_delete_excluded_url()
{
Metasync_Otto_Cache_Manager::instance()->ajax_otto_delete_excluded_url();
}
/**
* AJAX handler to recheck if an excluded URL is now available
* Used for "Recheck" action on Excluded URLs list
*/
public function ajax_otto_recheck_excluded_url()
{
Metasync_Otto_Cache_Manager::instance()->ajax_otto_recheck_excluded_url();
}
/**
* AJAX handler to get excluded URLs with pagination
*/
public function ajax_otto_get_excluded_urls()
{
Metasync_Otto_Cache_Manager::instance()->ajax_otto_get_excluded_urls();
}
/**
* AJAX handler for submitting issue reports to Sentry
*
* @since 2.5.10
* @return void Sends JSON response and exits
*/
public function ajax_submit_issue_report()
{
Metasync_Admin_Ajax::instance()->ajax_submit_issue_report();
}
/**
* Format duration seconds into human-readable label
*
* @since 2.5.11
* @param int $seconds Duration in seconds
* @return string Human-readable duration
*/
private function format_duration_label($seconds)
{
$labels = array(
3600 => '1 hour',
14400 => '4 hours',
28800 => '8 hours',
86400 => '24 hours',
172800 => '48 hours',
604800 => '7 days',
1209600 => '14 days',
2592000 => '30 days'
);
if (isset($labels[$seconds])) {
return $labels[$seconds];
}
# Calculate hours if not a standard duration
$hours = round($seconds / 3600);
return $hours . ' hours';
}
/**
* AJAX handler for password recovery
* Sends the whitelabel settings password to the configured recovery email
*/
public function ajax_recover_password()
{
Metasync_Admin_Ajax::instance()->ajax_recover_password();
}
/**
* AJAX handler for saving theme preference
* Saves the user's theme choice (light/dark) to WordPress options
*/
public function ajax_save_theme()
{
Metasync_Admin_Ajax::instance()->ajax_save_theme();
}
/**
* AJAX handler for tracking 1-click activation in GA4
*/
public function ajax_track_one_click_activation()
{
Metasync_Admin_Ajax::instance()->ajax_track_one_click_activation();
}
/**
* Handler for exporting whitelabel settings to a zip file
* Uses admin-post action for file downloads
*/
public function handle_export_whitelabel_settings()
{
Metasync_Admin_Ajax::instance()->handle_export_whitelabel_settings();
}
/**
* Add custom column to posts/pages list for HTML-converted pages
*
* @param array $columns Existing columns
* @return array Modified columns
*/
public function add_html_converted_column($columns)
{
// Add column after the title column
$new_columns = array();
foreach ($columns as $key => $value) {
$new_columns[$key] = $value;
if ($key === 'title') {
$new_columns['metasync_html_source'] = __('Source', 'metasync');
}
}
return $new_columns;
}
/**
* Render content for the HTML-converted column
*
* @param string $column_name Name of the column
* @param int $post_id Post ID
*/
public function render_html_converted_column($column_name, $post_id)
{
if ($column_name !== 'metasync_html_source') {
return;
}
// Check if this is an HTML-converted page
$has_raw_html = get_post_meta($post_id, '_metasync_raw_html_enabled', true);
$has_custom_css = get_post_meta($post_id, '_metasync_custom_css', true);
// If page has raw HTML or custom CSS from conversion, show badge
if ($has_raw_html || !empty($has_custom_css)) {
$label = $this->get_html_source_label();
$tooltip = sprintf(
__('This page was created using %s HTML-to-Builder converter', 'metasync'),
$label
);
echo sprintf(
'⚡%s',
esc_attr($tooltip),
esc_html($label)
);
}
}
/**
* Get the label for HTML-converted pages (respects whitelabel settings)
*
* @return string Label to display
*/
private function get_html_source_label()
{
$whitelabel_company = Metasync::get_whitelabel_company_name();
if (!empty($whitelabel_company)) {
return $whitelabel_company . ' AI';
}
return Metasync::get_effective_plugin_name() . ' AI';
}
/**
* Add source notice banner in the page editor
*
* @param WP_Post $post Current post object
*/
public function add_editor_source_notice($post)
{
if (!$post || !in_array($post->post_type, array('post', 'page'))) {
return;
}
// Don't show the "HTML-to-Builder converter" banner on LPS-synced / custom-HTML
// pages: those are raw-HTML store-and-serve pages, NOT actually converted to a
// page builder, so the banner mislabels them. It still shows for pages genuinely
// produced by the converter.
if (function_exists('metasync_is_custom_or_lps_page') && metasync_is_custom_or_lps_page($post->ID)) {
return;
}
$has_raw_html = get_post_meta($post->ID, '_metasync_raw_html_enabled', true);
$has_custom_css = get_post_meta($post->ID, '_metasync_custom_css', true);
if ($has_raw_html || !empty($has_custom_css)) {
$label = $this->get_html_source_label();
$message = sprintf(
__('This page was created using %s HTML-to-Builder converter. The design is preserved with custom CSS and inline styles.', 'metasync'),
'' . esc_html($label) . ''
);
echo sprintf(
'
⚡%s
%s
',
esc_html($label),
$message
);
}
}
/**
* Add source display in quick edit panel
*
* @param string $column_name Column name
* @param string $post_type Post type
*/
public function add_quick_edit_source_display($column_name, $post_type)
{
if ($column_name !== 'metasync_html_source') {
return;
}
if (!in_array($post_type, array('post', 'page'))) {
return;
}
?>
get_html_source_label();
$widget_title = sprintf(__('%s Pages', 'metasync'), $label);
wp_add_dashboard_widget(
'metasync_html_pages_widget',
$widget_title,
array($this, 'render_html_pages_dashboard_widget')
);
}
/**
* Render the dashboard widget content
*/
public function render_html_pages_dashboard_widget()
{
Metasync_Admin_Ajax::instance()->render_html_pages_dashboard_widget();
}
/**
* Bot Statistics page callback
* Displays bot detection statistics and logs
*/
public function create_admin_bot_statistics_page()
{
require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
}
/**
* AJAX handler for resetting bot statistics
*/
public function ajax_reset_bot_stats()
{
check_ajax_referer('metasync_reset_bot_stats', 'nonce');
if (!Metasync::current_user_has_plugin_access()) {
wp_send_json_error(['message' => 'Insufficient permissions.']);
}
require_once plugin_dir_path(dirname(__FILE__)) . 'otto/class-metasync-otto-bot-statistics-database.php';
$db = Metasync_Otto_Bot_Statistics_Database::get_instance();
$result = $db->reset_statistics();
if ($result) {
wp_send_json_success(['message' => 'Statistics reset successfully.']);
} else {
wp_send_json_error(['message' => 'Failed to reset statistics.']);
}
}
/**
* AJAX handler for sending URLs to Google Instant Indexing API
*
* @since 2.6.0
* @return void Sends JSON response and exits
*/
public function ajax_send_giapi()
{
check_ajax_referer('metasync_nonce', 'nonce');
if (!Metasync::current_user_has_plugin_access()) {
wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
}
$post_data = metasync_sanitize_input_array($_POST);
if (!isset($post_data['metasync_giapi_url'])) {
return;
}
// Parse URLs from textarea input (one per line)
$urls = array_values(array_filter(array_map('trim', explode("\n", sanitize_textarea_field(wp_unslash($post_data['metasync_giapi_url']))))));
if (empty($urls)) {
return;
}
if (!isset($post_data['metasync_giapi_action'])) {
return;
}
$action = sanitize_title($post_data['metasync_giapi_action']);
// Map form action values to google_index_direct action values
if ($action === 'remove') {
$action = 'delete';
}
header('Content-type: application/json');
$result_data = [];
foreach ($urls as $i => $url) {
$url = esc_url_raw($url);
if (empty($url)) {
continue;
}
if ($action === 'status') {
$result = google_index_direct()->get_url_status($url);
} else {
$result = google_index_direct()->index_url($url, $action);
}
$key = 'url-' . $i;
if (!empty($result['success'])) {
$result_data[$key] = $result['data'];
} else {
$result_data[$key] = (object) [
'error' => (object) [
'code' => isset($result['error']['code']) ? $result['error']['code'] : 400,
'message' => isset($result['error']['message']) ? $result['error']['message'] : 'Unknown error',
]
];
}
}
// For single URL, unwrap from the batch format (matches old behavior)
if (count($result_data) === 1) {
$result_data = reset($result_data);
}
wp_send_json($result_data);
wp_die();
}
/**
* AJAX handler for sending URLs to Bing via IndexNow API
*
* @since 2.6.0
* @return void Sends JSON response and exits
*/
public function ajax_send_bing_indexnow()
{
check_ajax_referer('metasync_nonce', 'nonce');
if (!Metasync::current_user_has_plugin_access()) {
wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
}
require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
$bing_instant_index = new Metasync_Bing_Instant_Index();
$bing_instant_index->send();
}
/**
* Save instant indexing settings (Google and Bing)
*
* @since 2.6.0
* @return void
*/
public function save_instant_indexing_settings()
{
// Check if this is a settings submission
if (!isset($_POST['submit'])) {
return;
}
// This handler runs on admin_init, which also fires inside
// admin-ajax.php before its login gate, so a bare isset() check let
// any request (including logged-out ones) rewrite the auto-submit
// post types. Require the nonce and plugin access before writing.
if (!isset($_POST['metasync_instant_indexing_nonce'])
|| !wp_verify_nonce(sanitize_key(wp_unslash($_POST['metasync_instant_indexing_nonce'])), 'metasync_instant_indexing_settings')
|| !Metasync::current_user_has_plugin_access()) {
return;
}
// Save post types for Google Instant Indexing auto-submit
if (isset($_POST['metasync_post_types'])) {
$post_data = metasync_sanitize_input_array($_POST);
$post_types = is_array($post_data['metasync_post_types']) ? array_map('sanitize_title', $post_data['metasync_post_types']) : [];
$settings = get_option('metasync_options_instant_indexing', ['post_types' => []]);
$settings['post_types'] = array_values($post_types);
update_option('metasync_options_instant_indexing', $settings);
}
// Note: Bing Instant Indexing settings are saved via AJAX in save_bing_inline_settings_ajax()
}
/**
* Save Bing instant indexing settings from inline form (Indexation Control page)
*
* @since 2.6.0
* @return bool True on success, false on failure
*/
private function save_bing_inline_settings_ajax() {
return Metasync_Settings_Registration::instance()->save_bing_inline_settings_ajax();
}
/**
* Add instant indexing action links to post/page rows
*
* @since 2.6.0
* @param array $actions Current actions
* @param WP_Post $post Current post object
* @return array Modified actions
*/
public function add_instant_indexing_post_actions($actions, $post)
{
// Add Google Instant Indexing links
$options = get_option('metasync_options_instant_indexing', ['json_key' => '', 'post_types' => []]);
$post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
if (in_array($post->post_type, $post_types) && $post->post_status == 'publish') {
$link = get_permalink($post);
// Get menu slug (support white label)
$general_options = Metasync::get_option('general') ?? [];
$menu_slug = !empty($general_options['white_label_plugin_menu_slug']) ? $general_options['white_label_plugin_menu_slug'] : 'searchatlas';
$page_slug = $menu_slug . '-google-console';
$actions['index-update'] = 'Update Google Index';
$actions['index-status'] = 'Status Google Index';
}
// Add Bing Instant Indexing links
require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
$bing_instant_index = new Metasync_Bing_Instant_Index();
$actions = $bing_instant_index->bing_instant_index_post_link($actions, $post);
return $actions;
}
/**
* Auto-submit post to instant indexing services when published
*
* @since 2.6.0
* @param int $post_id Post ID
* @param WP_Post $post Post object
* @param bool $update Whether this is an update
* @return void
*/
public function auto_submit_to_instant_indexing($post_id, $post, $update)
{
// Skip revisions, autosaves, and non-published posts
if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) {
return;
}
if ($post->post_status !== 'publish') {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
// Auto-submit to Google Instant Indexing
$seo_controls = Metasync::get_option('seo_controls');
if (!empty($seo_controls['enable_googleinstantindex']) && $seo_controls['enable_googleinstantindex'] === 'true') {
$options = get_option('metasync_options_instant_indexing', ['post_types' => []]);
$post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
if (in_array($post->post_type, $post_types)) {
// save_post is the single owner of auto-submit and also fires
// on REST-created posts, where nothing else may have loaded
// the Google Index helpers yet — load them on demand instead
// of silently skipping.
if (!function_exists('google_index_direct')) {
$google_index_path = plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
if (file_exists($google_index_path)) {
require_once $google_index_path;
}
}
if (function_exists('google_index_direct')) {
$service_info = google_index_direct()->get_service_account_info();
if (!isset($service_info['error'])) {
google_index_direct()->index_post($post_id, $post->post_type, 'update');
}
}
}
}
// Auto-submit to Bing Instant Indexing
require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
$bing_instant_index = new Metasync_Bing_Instant_Index();
$bing_instant_index->auto_submit_on_publish($post_id, $post, $update);
}
}