*/ namespace RankMath\Redirections; use RankMath\Helper; use RankMath\Traits\Hooker; use MyThemeShop\Helpers\Param; defined( 'ABSPATH' ) || exit; /** * Watcher class. */ class Watcher { use Hooker; /** * Hold updated posts permalinks. * * @var array */ private $updated_posts = []; /** * Hook methods for invalidation on necessary events. */ public function __construct() { // Post. // Only monitor if permalinks enabled. if ( get_option( 'permalink_structure' ) ) { if ( Helper::get_settings( 'general.redirections_post_redirect' ) ) { $this->action( 'pre_post_update', 'pre_post_update' ); $this->action( 'post_updated', 'handle_post_update', 10, 3 ); } else { $this->action( 'wp_trash_post', 'display_suggestion' ); } } $this->action( 'deleted_post', 'invalidate_post' ); // Term. $this->action( 'edited_terms', 'invalidate_term', 10, 2 ); $this->action( 'delete_term', 'invalidate_term', 10, 2 ); // User. $this->action( 'delete_user', 'invalidate_author' ); $this->action( 'profile_update', 'invalidate_author' ); } /** * Remember the previous post permalink * * @param integer $post_id Post ID. */ public function pre_post_update( $post_id ) { $this->updated_posts[ $post_id ] = get_permalink( $post_id ); } /** * Handles redirection when post is updated. * * @param integer $post_id Post ID. * @param WP_Post $post Post object after update. * @param WP_Post $before Post object before update. */ public function handle_post_update( $post_id, $post, $before ) { if ( ! in_array( $post->post_type, Helper::get_accessible_post_types(), true ) ) { return; } // Transitioning state of post. $transition = "{$before->post_status}_to_{$post->post_status}"; // Both state permalink. $before_permalink = isset( $this->updated_posts[ $post_id ] ) ? $this->updated_posts[ $post_id ] : false; $after_permalink = get_permalink( $post_id ); // Check for permalink change. if ( 'publish_to_publish' === $transition && $this->has_permalink_changed( $before_permalink, $after_permalink ) ) { $redirection_id = $this->create_redirection( $before_permalink, $after_permalink, 301, $post ); Helper::add_notification( sprintf( // translators: %1$s: post type label, %2$s: edit redirection URL. __( 'SEO Notice: you just changed the slug of a %1$s and Rank Math has automatically created a redirection. You can edit the redirection by clicking here.', 'rank-math' ), Helper::get_post_type_label( $post->post_type, true ), $this->get_edit_redirection_url( $redirection_id ) ), [ 'type' => 'warning', 'classes' => 'is-dismissible', ] ); // Update the meta value as well. if ( 'edit-post' === Param::post( 'screen' ) ) { update_post_meta( $post_id, 'rank_math_permalink', $post->post_name ); } $this->do_action( 'redirection/post_updated', $redirection_id ); return; } } /** * Create redirection * * @param string $from_url Redirecting from url for cache. * @param string $url_to Destination url. * @param int $header_code Response header code. * @param WP_Post $object Post object. * @return int Redirection id. */ private function create_redirection( $from_url, $url_to, $header_code, $object ) { // Early Bail! if ( empty( $from_url ) || empty( $url_to ) ) { return; } // Check for any existing redirection. // If found update that record. $redirection = $this->has_existing_redirection( $object->ID ); if ( false === $redirection ) { $redirection = Redirection::from([ 'url_to' => $url_to, 'header_code' => $header_code, ]); } $redirection->set_nocache( true ); $redirection->add_source( $from_url, 'exact' ); $redirection->save(); // Perform Cache. Cache::purge_by_object_id( $object->ID, 'post' ); if ( $from_url ) { $from_url = parse_url( $from_url, PHP_URL_PATH ); $from_url = Redirection::strip_subdirectory( $from_url ); Cache::add([ 'from_url' => $from_url, 'redirection_id' => $redirection->get_id(), 'object_id' => $object->ID, ]); } return $redirection->get_id(); } /** * Check for any existing redirection. * * @param int $post_id Post ID. * * @return boolean|int */ private function has_existing_redirection( $post_id ) { $cache = Cache::get_by_object_id( $post_id, 'post' ); if ( ! $cache ) { return false; } return Redirection::create( $cache->redirection_id ); } /** * Changed if permalinks are different and the before wasn't the site url (we don't want to redirect the site URL) * * @param WP_Post $before Post object before update. * @param WP_Post $after Post object after update. * @return boolean */ private function has_permalink_changed( $before, $after ) { $before = parse_url( $before, PHP_URL_PATH ); $after = parse_url( $after, PHP_URL_PATH ); // Are the URLs the same? if ( $before === $after ) { return false; } // Check it's not redirecting from the root. if ( $this->get_site_path() === $before || '/' === $before ) { return false; } return true; } /** * Gets edit redirection URL. * * @param int $redirection_id Redirection ID. * @return string */ private function get_edit_redirection_url( $redirection_id ) { return Helper::get_admin_url( 'redirections', [ 'redirection' => $redirection_id, 'security' => wp_create_nonce( 'redirection_list_action' ), ]); } /** * Get site path. * * @return string */ private function get_site_path() { $path = parse_url( get_site_url(), PHP_URL_PATH ); if ( $path ) { return rtrim( $path, '/' ) . '/'; } return '/'; } /** * Display notice after a post has been deleted * * @param int $post_id Deleted post ID. */ public function display_suggestion( $post_id ) { $post = get_post( $post_id ); if ( $this->can_display_suggestion( $post ) ) { $url = get_permalink( $post_id ); $admin_url = Helper::get_admin_url( 'redirections', [ 'url' => trim( set_url_scheme( $url, 'relative' ), '/' ) ] ); /* translators: 1. url to new screen, 2. old trashed post permalink */ $message = sprintf( wp_kses_post( __( 'SEO Notice: A previously published post has been moved to trash. You may redirect it %2$s to new url.', 'rank-math' ) ), $admin_url, $url ); Helper::add_notification( $message, [ 'type' => 'warning' ] ); } } /** * Can display ay_suggestion * * @param WP_Post $post Current post. * @return bool */ private function can_display_suggestion( $post ) { if ( 'publish' !== $post->post_status ) { return false; } return Helper::is_post_type_accessible( $post->post_type ); } /** * Invalidate redirection cache for the post. * Don't invalidate for revisions. * * @param int $post_id Post ID to invalidate type for. */ public function invalidate_post( $post_id ) { if ( wp_is_post_revision( $post_id ) ) { return; } Cache::purge_by_object_id( $post_id, 'post' ); } /** * Invalidate redirection cache for taxonomies. * * @param int|WP_Term $term Term ID or Term object. */ public function invalidate_term( $term ) { if ( is_a( $term, 'WP_Term' ) ) { $term = $term->term_id; } Cache::purge_by_object_id( $term, 'term' ); } /** * Invalidate redirection cache for authors. * * @param int $user_id User ID. */ public function invalidate_author( $user_id ) { Cache::purge_by_object_id( $user_id, 'user' ); } }