prefix . 'sxseo_redirects';
}
public static function log_table() {
global $wpdb;
return $wpdb->prefix . 'sxseo_404';
}
public static function install() {
global $wpdb;
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$cs = $wpdb->get_charset_collate();
dbDelta( "CREATE TABLE " . self::table() . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
source varchar(500) NOT NULL,
source_hash char(32) NOT NULL,
target text NOT NULL,
type smallint(6) NOT NULL DEFAULT 301,
auto tinyint(1) NOT NULL DEFAULT 0,
hits int(11) NOT NULL DEFAULT 0,
last_hit datetime NULL,
created datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY source_hash (source_hash)
) $cs;" );
dbDelta( "CREATE TABLE " . self::log_table() . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
path varchar(500) NOT NULL,
path_hash char(32) NOT NULL,
hits int(11) NOT NULL DEFAULT 1,
referrer varchar(500) NOT NULL DEFAULT '',
first_seen datetime NOT NULL,
last_seen datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY path_hash (path_hash),
KEY last_seen (last_seen)
) $cs;" );
}
public static function hook() {
add_action( 'template_redirect', array( __CLASS__, 'maybe_redirect' ), 1 );
add_action( 'template_redirect', array( __CLASS__, 'handle_404' ), 99 );
add_action( 'template_redirect', array( __CLASS__, 'attachment_pages' ), 2 );
add_action( 'post_updated', array( __CLASS__, 'on_post_updated' ), 10, 3 );
add_action( 'edit_terms', array( __CLASS__, 'capture_term' ), 10, 2 );
add_action( 'edited_term', array( __CLASS__, 'on_term_edited' ), 10, 3 );
}
// ----------------------------------------------------------------- storage
public static function add( $source, $target, $type = 301, $auto = 0 ) {
global $wpdb;
$path = self::path_from( $source );
if ( '' === $path || '' === trim( (string) $target ) ) {
return false;
}
$target = self::norm_target( $target );
if ( self::path_from( $target ) === $path ) {
return false; // would loop
}
$wpdb->replace(
self::table(),
array(
'source' => $path,
'source_hash' => md5( $path ),
'target' => $target,
'type' => in_array( (int) $type, array( 301, 302, 307, 308, 410, 451 ), true ) ? (int) $type : 301,
'auto' => $auto ? 1 : 0,
'created' => current_time( 'mysql', true ),
)
);
// Anything that used to point at the new source must now follow the chain, never loop.
$wpdb->delete( self::table(), array( 'source_hash' => md5( self::path_from( $target ) ) ) );
wp_cache_delete( 'sxseo_r_' . md5( $path ), 'sxseo' );
return true;
}
/** Accepts a full URL or a path and returns the normalised site-relative path ('' = home). */
public static function path_from( $url ) {
$p = (string) wp_parse_url( (string) $url, PHP_URL_PATH );
$base = (string) wp_parse_url( home_url(), PHP_URL_PATH );
if ( '' !== $base && 0 === strpos( $p, $base ) ) {
$p = substr( $p, strlen( $base ) );
}
return sxseo_norm_path( $p );
}
private static function norm_target( $t ) {
$t = trim( (string) $t );
if ( preg_match( '#^https?://#i', $t ) ) {
return esc_url_raw( $t );
}
return home_url( '/' . ltrim( $t, '/' ) );
}
public static function lookup( $path ) {
global $wpdb;
$hash = md5( $path );
$hit = wp_cache_get( 'sxseo_r_' . $hash, 'sxseo' );
if ( false === $hit ) {
$hit = $wpdb->get_row( $wpdb->prepare( 'SELECT id,target,type FROM ' . self::table() . ' WHERE source_hash = %s', $hash ) ); // phpcs:ignore WordPress.DB
wp_cache_set( 'sxseo_r_' . $hash, $hit ? $hit : 0, 'sxseo', 300 );
}
return $hit ? $hit : null;
}
// ----------------------------------------------------------------- serving
public static function maybe_redirect() {
if ( is_admin() || wp_doing_ajax() || wp_doing_cron() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
return;
}
$method = isset( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : 'GET';
if ( 'GET' !== $method && 'HEAD' !== $method ) {
return;
}
$path = sxseo_request_path();
$r = self::lookup( $path );
if ( ! $r ) {
return;
}
global $wpdb;
$wpdb->query( $wpdb->prepare( 'UPDATE ' . self::table() . ' SET hits = hits + 1, last_hit = %s WHERE id = %d', current_time( 'mysql', true ), $r->id ) ); // phpcs:ignore WordPress.DB
$type = (int) $r->type;
if ( 410 === $type || 451 === $type ) {
status_header( $type );
nocache_headers();
header( 'X-Robots-Tag: noindex' );
echo '
' . esc_html( 410 === $type ? 'Gone' : 'Unavailable' ) . '' . esc_html( 410 === $type ? 'This page has been permanently removed.' : 'Unavailable for legal reasons.' ) . '
';
exit;
}
wp_redirect( $r->target, $type, 'Sentrixa SEO' ); // phpcs:ignore WordPress.Security.SafeRedirect
exit;
}
/** Attachment pages are thin duplicate content: send them to the file's post or the file itself. */
public static function attachment_pages() {
if ( ! is_attachment() || ! sxseo_opt( 'attachment_redirect' ) ) {
return;
}
$att = get_queried_object();
$target = ( $att && $att->post_parent ) ? get_permalink( $att->post_parent ) : wp_get_attachment_url( $att->ID );
if ( $target ) {
wp_redirect( $target, 301, 'Sentrixa SEO' ); // phpcs:ignore WordPress.Security.SafeRedirect
exit;
}
}
public static function handle_404() {
if ( ! is_404() ) {
return;
}
$path = sxseo_request_path();
if ( '' === $path || preg_match( '#^/(wp-|xmlrpc|favicon|apple-touch|robots\.txt|sitemap|llms\.txt)#', $path ) ) {
return;
}
if ( sxseo_opt( 'smart_404' ) ) {
$target = self::guess( $path );
if ( $target ) {
self::add( $path, $target, 301, 1 );
wp_redirect( $target, 301, 'Sentrixa SEO' ); // phpcs:ignore WordPress.Security.SafeRedirect
exit;
}
}
if ( sxseo_opt( 'log_404' ) && ! preg_match( '#\.(css|js|map|png|jpe?g|gif|svg|ico|webp|woff2?|ttf)$#i', $path ) ) {
self::log( $path );
}
}
/**
* Old structures keep their value: /2019/04/some-post/, /blog/some-post/, /category/news/some-post/ all end in
* a slug that still exists. If exactly one live page owns that last segment, that is where the visitor belongs.
*/
private static function guess( $path ) {
global $wpdb;
$slug = sanitize_title( basename( $path ) );
$slug = preg_replace( '/\.(html?|php|aspx?)$/', '', $slug );
if ( '' === $slug || sxseo_len( $slug ) < 4 ) {
return '';
}
$types = sxseo_post_types();
if ( ! $types ) {
return '';
}
$in = implode( ',', array_fill( 0, count( $types ), '%s' ) );
$rows = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_status = 'publish' AND post_type IN ($in) LIMIT 2", array_merge( array( $slug ), $types ) ) ); // phpcs:ignore WordPress.DB
if ( 1 === count( $rows ) ) {
$link = get_permalink( (int) $rows[0] );
return self::path_from( $link ) === $path ? '' : $link;
}
return '';
}
private static function log( $path ) {
global $wpdb;
$now = current_time( 'mysql', true );
$ref = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '';
$wpdb->query( $wpdb->prepare( 'INSERT INTO ' . self::log_table() . ' (path, path_hash, hits, referrer, first_seen, last_seen) VALUES (%s,%s,1,%s,%s,%s) ON DUPLICATE KEY UPDATE hits = hits + 1, last_seen = VALUES(last_seen), referrer = IF(VALUES(referrer) <> "", VALUES(referrer), referrer)', substr( $path, 0, 500 ), md5( $path ), substr( $ref, 0, 500 ), $now, $now ) ); // phpcs:ignore WordPress.DB
if ( 1 === wp_rand( 1, 200 ) ) { // keep the table bounded
$wpdb->query( 'DELETE FROM ' . self::log_table() . " WHERE last_seen < (UTC_TIMESTAMP() - INTERVAL 120 DAY)" ); // phpcs:ignore WordPress.DB
}
}
// ------------------------------------------------------- automatic redirects
public static function on_post_updated( $post_id, $after, $before ) {
if ( ! sxseo_opt( 'auto_redirects' ) || 'publish' !== $before->post_status || 'publish' !== $after->post_status ) {
return;
}
if ( ! in_array( $after->post_type, sxseo_post_types(), true ) ) {
return;
}
if ( $before->post_name === $after->post_name && $before->post_parent === $after->post_parent ) {
return;
}
$old = get_permalink( $before );
$new = get_permalink( $after );
if ( $old && $new && self::path_from( $old ) !== self::path_from( $new ) ) {
self::add( $old, $new, 301, 1 );
SXSEO_Sitemap::submit( array( $new ) );
}
}
public static function capture_term( $term_id, $taxonomy ) {
$link = get_term_link( (int) $term_id, $taxonomy );
if ( ! is_wp_error( $link ) ) {
self::$old_term[ $term_id ] = $link;
}
}
public static function on_term_edited( $term_id, $tt_id, $taxonomy ) {
if ( ! sxseo_opt( 'auto_redirects' ) || empty( self::$old_term[ $term_id ] ) ) {
return;
}
$new = get_term_link( (int) $term_id, $taxonomy );
if ( ! is_wp_error( $new ) && self::path_from( $new ) !== self::path_from( self::$old_term[ $term_id ] ) ) {
self::add( self::$old_term[ $term_id ], $new, 301, 1 );
}
}
}