*/ private static $processedPosts = []; /** * Get singleton instance * @return ABJ_404_Solution_SlugChangeHandler */ public static function getInstance() { if (self::$instance == null) { self::$instance = new ABJ_404_Solution_SlugChangeHandler(); } return self::$instance; } /** * Initialize the handler and register WordPress hooks * @return void */ static function init() { $me = abj_service('slug_change_handler'); add_action('save_post', array($me, 'save_postHandler'), 10, 3); add_action('transition_post_status', array($me, 'postStatusTransitionHandler'), 10, 3); add_action('before_delete_post', array($me, 'beforeDeletePostHandler'), 10, 2); } /** We'll just make sure the permalink gets updated in case it's changed. * @param int $post_id The post ID. * @param \WP_Post $post The post object. * @param bool $update Whether this is an existing post being updated or not. * @return void */ function save_postHandler($post_id, $post, $update) { $abj404logging = abj_service('logging'); // Prevent duplicate processing within same request // WordPress fires save_post multiple times per save operation if (isset(self::$processedPosts[$post_id])) { $abj404logging->debugMessage(__CLASS__ . "/" . __FUNCTION__ . ": Already processed post ID " . $post_id . " in this request (skipped)."); return; } // Defensive: WordPress hook may pass unexpected types at runtime. if (!is_object($post) || !property_exists($post, 'post_name')) { $abj404logging->debugMessage(__CLASS__ . "/" . __FUNCTION__ . ": Invalid post object or missing post_name property for post ID " . $post_id . "."); return; } if (!$update) { $abj404logging->debugMessage(__CLASS__ . "/" . __FUNCTION__ . ": Non-update skipped for post ID " . $post_id . "."); return; } // Check if we should create a redirect (respects per-post override from editor) $abj404logic = abj_service('plugin_logic'); $options = $abj404logic->getOptions(); // Check for per-post override from Quick Edit, Classic Editor, or Gutenberg if (class_exists('ABJ_404_Solution_PostEditorIntegration')) { $shouldCreate = ABJ_404_Solution_PostEditorIntegration::shouldCreateRedirect($post_id, $options); } else { // Fallback to global setting if PostEditorIntegration not loaded $shouldCreate = @$options['auto_slugs'] == '1'; } if (!$shouldCreate) { $abj404logging->debugMessage(__CLASS__ . "/" . __FUNCTION__ . ": Auto slug redirects off " . "or disabled for this post (skipped) (post ID " . $post_id . ")."); return; } // Use post_status from $post object instead of database query /** @var string|false $postStatus */ $postStatus = property_exists($post, 'post_status') ? $post->post_status : get_post_status($post_id); if (!in_array($postStatus, array('publish', 'published'))) { $abj404logging->debugMessage(__CLASS__ . "/" . __FUNCTION__ . ": Post status: " . $postStatus . " (skipped) (post ID " . $post_id . ")."); return; } // get the old slug $abj404dao = abj_service('data_access'); $oldURL = $abj404dao->getPermalinkFromCache($post_id); if ($oldURL == null || $oldURL == "") { $abj404logging->debugMessage("Couldn't find old slug for updated page. ID " . $post_id . ", old URL: " . $oldURL . ", post name: " . $post->post_name . ", update: " . $update); return; } $newURL = get_permalink($post); // Defensive: get_permalink may return WP_Error via filters in some environments. if (is_wp_error($newURL)) { $abj404logging->debugMessage("Could not get permalink for post (WP_Error). ID: " . $post_id . ", error: " . $newURL->get_error_message()); return; } if ($newURL === false || $newURL === '') { $abj404logging->debugMessage("Could not get permalink for post (invalid return). ID: " . $post_id); return; } // Safely parse the old URL $oldURLParsed = parse_url($oldURL); if ($oldURLParsed === false) { $abj404logging->debugMessage("Could not parse old URL (malformed). ID: " . $post_id . ", URL: " . $oldURL); return; } if (!isset($oldURLParsed['path']) || $oldURLParsed['path'] === '') { $abj404logging->debugMessage("Old URL has no path component. ID: " . $post_id . ", URL: " . $oldURL); return; } $oldSlug = $oldURLParsed['path']; if ($oldURL == $newURL) { $abj404logging->debugMessage("Save post listener: Old and new URL are the same. (Ignored) " . "ID: " . $post_id . ", old URL: " . $oldURL . ", old slug: " . $oldSlug . ", new slug: " . $post->post_name . ", update: " . $update); return; } // Mark as processed before creating redirect to prevent duplicates self::$processedPosts[$post_id] = true; // create a redirect from the old to the new. $abj404dao->setupRedirect($oldSlug, (string)ABJ404_STATUS_AUTO, (string)ABJ404_TYPE_POST, (string)$post_id, (isset($options['default_redirect']) && is_scalar($options['default_redirect'])) ? (string)$options['default_redirect'] : '301', 0, 'slug change'); $abj404logging->infoMessage("Added automatic redirect after slug change from " . $oldURL . ' to ' . $newURL . " for post ID " . $post_id); } /** * Fires when a published post is moved to trash. * Creates a redirect from the old permalink to the homepage. * * @param string $new_status New post status. * @param string $old_status Old post status. * @param \WP_Post $post Post object. * @return void */ function postStatusTransitionHandler($new_status, $old_status, $post) { // Only care about published posts being trashed. if ($old_status !== 'publish' || $new_status !== 'trash') { return; } if (!is_object($post) || !property_exists($post, 'ID')) { return; } $post_id = (int)$post->ID; // Check option $abj404logic = abj_service('plugin_logic'); $options = $abj404logic->getOptions(); if (!isset($options['auto_trash_redirect']) || $options['auto_trash_redirect'] != '1') { return; } // Prevent duplicate processing within same request if (isset(self::$processedPosts[$post_id])) { return; } $abj404dao = abj_service('data_access'); $oldURL = $abj404dao->getPermalinkFromCache($post_id); if ($oldURL == null || $oldURL == '') { return; } $oldURLParsed = parse_url($oldURL); if ($oldURLParsed === false || !isset($oldURLParsed['path']) || $oldURLParsed['path'] === '') { return; } $oldSlug = $oldURLParsed['path']; $redirectCode = (isset($options['default_redirect']) && is_scalar($options['default_redirect'])) ? (string)$options['default_redirect'] : '301'; self::$processedPosts[$post_id] = true; $abj404dao->setupRedirect($oldSlug, (string)ABJ404_STATUS_AUTO, (string)ABJ404_TYPE_HOME, '0', $redirectCode, 0, 'post trashed'); abj_service('logging')->infoMessage( "Added automatic redirect to homepage after post trashed. ID: " . $post_id . ", old URL: " . $oldURL); } /** * Fires just before a published post is permanently deleted. * Creates a redirect from the old permalink to the homepage. * * @param int $post_id Post ID. * @param \WP_Post $post Post object. * @return void */ function beforeDeletePostHandler($post_id, $post) { if (!is_object($post) || !property_exists($post, 'post_status')) { return; } // Only published posts (not already-trashed posts being force-deleted). $postStatus = (string)$post->post_status; if (!in_array($postStatus, array('publish', 'published'), true)) { return; } // Check option $abj404logic = abj_service('plugin_logic'); $options = $abj404logic->getOptions(); if (!isset($options['auto_trash_redirect']) || $options['auto_trash_redirect'] != '1') { return; } $post_id = (int)$post_id; // Prevent duplicate processing within same request if (isset(self::$processedPosts[$post_id])) { return; } $abj404dao = abj_service('data_access'); $oldURL = $abj404dao->getPermalinkFromCache($post_id); if ($oldURL == null || $oldURL == '') { return; } $oldURLParsed = parse_url($oldURL); if ($oldURLParsed === false || !isset($oldURLParsed['path']) || $oldURLParsed['path'] === '') { return; } $oldSlug = $oldURLParsed['path']; $redirectCode = (isset($options['default_redirect']) && is_scalar($options['default_redirect'])) ? (string)$options['default_redirect'] : '301'; self::$processedPosts[$post_id] = true; $abj404dao->setupRedirect($oldSlug, (string)ABJ404_STATUS_AUTO, (string)ABJ404_TYPE_HOME, '0', $redirectCode, 0, 'post deleted'); abj_service('logging')->infoMessage( "Added automatic redirect to homepage after post deleted. ID: " . $post_id . ", old URL: " . $oldURL); } }