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 ); } } }