*/ private static $processedExclusionPosts = []; /** @return self */ public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } /** * Initialize all editor integrations */ /** @return void */ public static function init() { $me = self::getInstance(); // Quick Edit integration add_filter('manage_posts_columns', array($me, 'addRedirectColumn'), 10, 2); add_filter('manage_pages_columns', array($me, 'addRedirectColumn')); add_action('manage_posts_custom_column', array($me, 'renderRedirectColumn'), 10, 2); add_action('manage_pages_custom_column', array($me, 'renderRedirectColumn'), 10, 2); add_action('quick_edit_custom_box', array($me, 'renderQuickEditCheckbox'), 10, 2); add_action('admin_enqueue_scripts', array($me, 'enqueueQuickEditScript')); // Classic Editor meta box add_action('add_meta_boxes', array($me, 'addMetaBox')); add_action('save_post', array($me, 'saveExclusionMeta')); // Gutenberg sidebar panel add_action('init', array($me, 'registerPostMeta')); add_action('enqueue_block_editor_assets', array($me, 'enqueueGutenbergScript')); // Term meta for exclusion on category/tag edit screens add_action('init', array($me, 'registerTermMeta')); } /** * Get the default value for the redirect checkbox based on global settings */ /** @return bool */ public function getDefaultRedirectSetting() { $options = abj_service('plugin_logic')->getOptions(); return @$options['auto_slugs'] == '1'; } // ==================== Quick Edit Integration ==================== /** * Add a hidden column to store redirect default for Quick Edit JavaScript */ /** * @param array $columns * @param string|null $post_type * @return array */ public function addRedirectColumn($columns, $post_type = null) { // Add our column (it will be hidden via CSS) $columns['abj404_redirect'] = ''; return $columns; } /** * Render the hidden column data (used by Quick Edit JavaScript) */ /** * @param string $column * @param int $post_id * @return void */ public function renderRedirectColumn($column, $post_id) { if ($column === 'abj404_redirect') { $default = $this->getDefaultRedirectSetting() ? '1' : '0'; // Output data attribute for JavaScript to read echo ''; } } /** * Render the Quick Edit checkbox */ /** * @param string $column_name * @param string $post_type * @return void */ public function renderQuickEditCheckbox($column_name, $post_type) { if ($column_name !== 'abj404_redirect') { return; } // Only show for post types that can have slugs if (!post_type_supports($post_type, 'slug') && !in_array($post_type, array('post', 'page'))) { return; } static $nonce_printed = false; if (!$nonce_printed) { wp_nonce_field('abj404_quick_edit', 'abj404_quick_edit_nonce'); $nonce_printed = true; } ?>
true), 'names'); foreach ($post_types as $post_type) { add_meta_box( 'abj404_redirect_meta_box', __('404 Solution', '404-solution'), array($this, 'renderMetaBox'), $post_type, 'side', 'default', array( // Prevent this meta box from appearing in Gutenberg // We have a custom Gutenberg integration that shows the checkbox // only when the slug is modified '__block_editor_compatible_meta_box' => false, '__back_compat_meta_box' => true ) ); } } /** * Render the Classic Editor meta box content */ /** * @param \WP_Post $post * @return void */ public function renderMetaBox($post) { // Only show for published posts (new posts have no old URL to redirect from) if ($post->post_status !== 'publish') { echo '

' . esc_html__('Redirect options are available after the post is published.', '404-solution') . '

'; return; } $default = $this->getDefaultRedirectSetting(); $checked = $default ? 'checked' : ''; $excludeMeta = get_post_meta($post->ID, '_abj404_exclude', true); $excludeChecked = ($excludeMeta === '1') ? 'checked' : ''; wp_nonce_field('abj404_meta_box', 'abj404_meta_box_nonce'); ?>


true, 'single' => true, 'type' => 'string', 'default' => '', 'auth_callback' => function() { return current_user_can('edit_posts'); }, 'sanitize_callback' => function($value) { return $value === '1' ? '1' : ($value === '0' ? '0' : ''); } )); register_post_meta('', '_abj404_exclude', array( 'show_in_rest' => true, 'single' => true, 'type' => 'string', 'default' => '', 'auth_callback' => function() { return current_user_can('edit_posts'); }, 'sanitize_callback' => array(__CLASS__, 'sanitizeExclusionMeta'), )); } /** * Sanitize the exclusion meta value: only '1' is truthy, everything else becomes ''. * * @param mixed $value * @return string */ public static function sanitizeExclusionMeta($value) { return $value === '1' ? '1' : ''; } /** * Enqueue Gutenberg sidebar script */ /** @return void */ public function enqueueGutenbergScript() { // Only load on post edit screens. // get_current_screen() lives in wp-admin/includes/screen.php; guard adjacent // because this hook may fire from contexts where wp-admin includes aren't loaded. if (!function_exists('get_current_screen')) { return; } $screen = get_current_screen(); if (!$screen || $screen->base !== 'post') { return; } wp_enqueue_script( 'abj404-gutenberg-redirect', plugin_dir_url(__FILE__) . 'js/gutenberg-redirect.js', array('wp-plugins', 'wp-edit-post', 'wp-element', 'wp-components', 'wp-data', 'wp-i18n'), ABJ404_VERSION, true ); // Pass default setting and translations to JavaScript wp_localize_script('abj404-gutenberg-redirect', 'abj404GutenbergRedirect', array( 'defaultEnabled' => $this->getDefaultRedirectSetting(), 'i18n' => array( 'checkboxLabel' => __('Create redirect from old URL to new URL', '404-solution'), 'slugChangedNotice' => __('Slug changed:', '404-solution'), 'excludeLabel' => __('Exclude from 404 redirect suggestions', '404-solution'), 'excludeHelp' => __('When checked, this post will not be suggested as a redirect target for 404 errors.', '404-solution'), ) )); } // ==================== Post Exclusion Save ==================== /** * Save _abj404_exclude post meta on post save. * * @param int $post_id * @return void */ public function saveExclusionMeta($post_id) { // Prevent duplicate processing within same request (WordPress fires save_post 2-4 times per save). if (isset(self::$processedExclusionPosts[$post_id])) { return; } if (!isset($_POST['abj404_meta_box_nonce'])) { return; } if (!wp_verify_nonce($_POST['abj404_meta_box_nonce'], 'abj404_meta_box')) { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (!current_user_can('edit_post', $post_id)) { return; } $value = isset($_POST['abj404_exclude']) && $_POST['abj404_exclude'] === '1' ? '1' : ''; update_post_meta($post_id, '_abj404_exclude', $value); self::$processedExclusionPosts[$post_id] = true; } // ==================== Term Meta Integration ==================== /** * Register term meta and hook edit/save for all public taxonomies. * * @return void */ public function registerTermMeta() { $taxonomies = get_taxonomies(array('public' => true), 'names'); foreach ($taxonomies as $taxonomy) { register_term_meta($taxonomy, '_abj404_exclude', array( 'show_in_rest' => true, 'single' => true, 'type' => 'string', 'default' => '', 'sanitize_callback' => array(__CLASS__, 'sanitizeExclusionMeta'), )); add_action("{$taxonomy}_edit_form_fields", array($this, 'renderTermExclusionField'), 10, 2); add_action("edited_{$taxonomy}", array($this, 'saveTermExclusionMeta')); add_action("created_{$taxonomy}", array($this, 'saveTermExclusionMeta')); } } /** * Render the exclusion checkbox on the term edit form. * * @param \WP_Term $term * @param string $taxonomy * @return void */ public function renderTermExclusionField($term, $taxonomy = '') { $value = get_term_meta($term->term_id, '_abj404_exclude', true); $checked = ($value === '1') ? 'checked' : ''; wp_nonce_field('abj404_term_exclude', 'abj404_term_exclude_nonce'); ?>

$options Plugin options * @return bool Whether to create redirect */ public static function shouldCreateRedirect($post_id, $options) { // Check Quick Edit / Classic Editor POST data if (isset($_POST['abj404_create_redirect'])) { // Verify nonce for Quick Edit if (isset($_POST['abj404_quick_edit_nonce']) && wp_verify_nonce($_POST['abj404_quick_edit_nonce'], 'abj404_quick_edit')) { return $_POST['abj404_create_redirect'] === '1'; } // Verify nonce for Classic Editor if (isset($_POST['abj404_meta_box_nonce']) && wp_verify_nonce($_POST['abj404_meta_box_nonce'], 'abj404_meta_box')) { return $_POST['abj404_create_redirect'] === '1'; } } // Check for unchecked checkbox in Quick Edit (checkbox not in POST = unchecked) if (isset($_POST['abj404_quick_edit_nonce']) && wp_verify_nonce($_POST['abj404_quick_edit_nonce'], 'abj404_quick_edit') && !isset($_POST['abj404_create_redirect'])) { return false; } // Check for unchecked checkbox in Classic Editor (checkbox not in POST = unchecked) if (isset($_POST['abj404_meta_box_nonce']) && wp_verify_nonce($_POST['abj404_meta_box_nonce'], 'abj404_meta_box') && !isset($_POST['abj404_create_redirect'])) { return false; } // Check Gutenberg post meta $meta = get_post_meta($post_id, '_abj404_create_redirect', true); if ($meta !== '' && $meta !== null) { // Clear the meta after reading (one-time use per edit session) delete_post_meta($post_id, '_abj404_create_redirect'); return $meta === '1'; } // Fall back to global setting return @$options['auto_slugs'] == '1'; } }