*/
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' );
}
}