db = new Redirect_Manager_DB(); $this->settings = new Redirect_Manager_Settings( $this->db, $this ); add_action( 'template_redirect', [ $this, 'maybe_redirect' ], 1 ); add_action( 'init', [ $this, 'maybe_register_redirected_status' ], 20 ); add_action( 'admin_init', [ $this, 'register_post_workflow_hooks' ] ); add_action( 'add_meta_boxes', [ $this, 'add_post_editor_panel' ] ); add_action( 'save_post', [ $this, 'save_post_editor_redirect' ], 20, 2 ); add_action( 'transition_post_status', [ $this, 'handle_restored_redirected_post' ], 20, 3 ); } /** * Register list table row actions when enabled. * * @return void */ public function register_post_workflow_hooks(): void { if ( ! is_admin() || ! $this->is_post_redirect_tools_enabled() || ! $this->get_option( 'show_post_table_action' ) ) { return; } add_filter( 'post_row_actions', [ $this, 'add_post_redirect_row_action' ], 10, 2 ); add_filter( 'page_row_actions', [ $this, 'add_post_redirect_row_action' ], 10, 2 ); } /** * Add a redirect row action to eligible published posts. * * @param array $actions Existing row actions. * @param \WP_Post $post Post object. * * @return array */ public function add_post_redirect_row_action( array $actions, $post ): array { if ( ! $post instanceof \WP_Post || ! $this->is_post_eligible_for_redirect( $post ) ) { return $actions; } if ( ! current_user_can( 'manage_options' ) ) { if ( ! $this->get_option( 'show_post_editor_panel' ) ) { return $actions; } $redirect_url = get_edit_post_link( $post->ID, 'raw' ); } else { $redirect_url = $this->get_redirect_manager_prefill_url( $post ); } $actions['adminoptim_redirect_post'] = sprintf( '%s', esc_url( $redirect_url ), esc_html__( 'Redirect this post', 'admin-optimizer' ) ); return $actions; } /** * Add redirect controls to the post editor. * * @return void */ public function add_post_editor_panel(): void { if ( ! $this->is_post_redirect_tools_enabled() || ! $this->get_option( 'show_post_editor_panel' ) ) { return; } foreach ( $this->get_enabled_post_types() as $post_type ) { add_meta_box( 'adminoptim_redirect_manager', __( 'Redirect Manager', 'admin-optimizer' ), [ $this, 'render_post_editor_panel' ], $post_type, 'side', 'default' ); } } /** * Render post editor redirect controls. * * @param \WP_Post $post Post object. * * @return void */ public function render_post_editor_panel( $post ): void { if ( ! $post instanceof \WP_Post || ! $this->is_post_eligible_for_redirect( $post ) ) { echo '
' . esc_html__( 'Publish this post to create a redirect from its current URL.', 'admin-optimizer' ) . '
'; return; } $redirect_id = (int) get_post_meta( $post->ID, self::POST_REDIRECT_META_KEY, true ); $redirect = $redirect_id > 0 ? $this->db->get_by_id( $redirect_id ) : null; wp_nonce_field( 'adminoptim_redirect_post_' . $post->ID, 'adminoptim_redirect_post_nonce' ); ?>
get_option( 'auto_unpublish_on_redirect', 1 ) ) { echo '
' . esc_html__( 'The source post will be moved out of Published status after the redirect is created.', 'admin-optimizer' ) . '
'; } } /** * Save redirect controls from the post editor. * * @param int $post_id Post ID. * @param \WP_Post $post Post object. * * @return void */ public function save_post_editor_redirect( int $post_id, $post ): void { if ( ! $post instanceof \WP_Post || empty( $_POST['adminoptim_redirect_post_nonce'] ) ) { return; } if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['adminoptim_redirect_post_nonce'] ) ), 'adminoptim_redirect_post_' . $post_id ) ) { return; } if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) || ! $this->is_post_eligible_for_redirect( $post ) ) { return; } $target_url = isset( $_POST['adminoptim_redirect_target'] ) ? esc_url_raw( wp_unslash( $_POST['adminoptim_redirect_target'] ) ) : ''; if ( '' === $target_url ) { return; } $type = isset( $_POST['adminoptim_redirect_type'] ) ? (int) $_POST['adminoptim_redirect_type'] : 301; $this->create_or_update_post_redirect( $post, $target_url, $type, true ); } /** * Register the Redirected post status when requested. * * @return void */ public function maybe_register_redirected_status(): void { if ( ! $this->get_option( 'create_redirected_status' ) && 'redirected' !== $this->get_option( 'unpublish_status', 'redirected' ) ) { return; } register_post_status( 'redirected', [ 'label' => __( 'Redirected', 'admin-optimizer' ), 'protected' => true, '_builtin' => false, 'show_in_admin_status_list' => true, 'show_in_admin_all_list' => true, // translators: %s: number of redirected posts. 'label_count' => _n_noop( 'Redirected (%s)', 'Redirected (%s)', 'admin-optimizer' ), ] ); if ( $this->get_option( 'create_redirected_status' ) ) { // Pro hooks in here to also register this status as a Custom Post Status module term. do_action( 'adminoptim_redirect_manager_ensure_redirected_status' ); } } /** * Apply restore behavior when a redirected source post is republished. * * @param string $new_status New status. * @param string $old_status Old status. * @param \WP_Post $post Post object. * * @return void */ public function handle_restored_redirected_post( string $new_status, string $old_status, $post ): void { if ( 'publish' !== $new_status || $new_status === $old_status || ! $post instanceof \WP_Post ) { return; } $redirect_id = (int) get_post_meta( $post->ID, self::POST_REDIRECT_META_KEY, true ); if ( $redirect_id <= 0 ) { return; } $behavior = $this->get_option( 'restore_behavior', 'disable' ); if ( 'delete' === $behavior ) { $this->db->delete( $redirect_id ); delete_post_meta( $post->ID, self::POST_REDIRECT_META_KEY ); } elseif ( 'disable' === $behavior ) { $this->db->toggle_enabled( $redirect_id, 0 ); } } /** * Check current request against stored redirects and redirect if a match is found * * Runs early on template_redirect (priority 1) so it fires before theme routing. * * @return void */ public function maybe_redirect() { if ( is_admin() ) { return; } $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $source = Redirect_Manager_DB::normalize_source( $request_uri ); $redirect = $this->db->find_by_source( $source ); if ( $redirect ) { $destination = $this->build_redirect_destination( $redirect ); $validated = Redirect_Manager_DB::validate_target_url( $destination ); if ( is_wp_error( $validated ) ) { return; } $is_external = Redirect_Manager_DB::is_external_target( $validated ); if ( $is_external && empty( $redirect->allow_external ) ) { $this->log_blocked_external_redirect( $redirect ); return; } $this->db->increment_hits( (int) $redirect->id ); if ( $is_external ) { // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- intentional: Redirect Manager allows admin-configured redirects to external URLs; wp_safe_redirect() would defeat that. $validated is admin-set target_url, already checked by validate_target_url(), and only reached when allow_external is explicitly enabled. wp_redirect( $validated, (int) $redirect->redirect_type, 'Admin Optimizer Redirect Manager' ); exit; } wp_safe_redirect( $validated, (int) $redirect->redirect_type, 'Admin Optimizer Redirect Manager' ); exit; } } /** * Build the final redirect destination, appending the incoming query string when enabled. * * @param object $redirect Redirect record. * * @return string */ private function build_redirect_destination( $redirect ): string { $destination = $redirect->target_url; $query_string = isset( $_SERVER['QUERY_STRING'] ) ? wp_unslash( $_SERVER['QUERY_STRING'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized if ( ! empty( $redirect->preserve_query ) && '' !== $query_string ) { $separator = false === strpos( $destination, '?' ) ? '?' : '&'; $destination .= $separator . $query_string; } return $destination; } /** * Log an external redirect blocked because this rule was not explicitly authorized. * * @param object $redirect Redirect record. * * @return void */ private function log_blocked_external_redirect( $redirect ): void { do_action( 'adminoptim_redirect_manager_blocked_external_redirect', [ 'redirect_id' => (int) $redirect->id, 'source_url' => (string) $redirect->source_url, 'target_host' => Redirect_Manager_DB::get_normalized_target_host( (string) $redirect->target_url ), 'reason' => 'external target not authorized', ] ); } /** * Create or update a redirect connected to a post. * * @param \WP_Post $post Source post. * @param string $target_url Target URL. * @param int $type Redirect type. * @param bool $unpublish Whether to unpublish after saving. * * @return int */ public function create_or_update_post_redirect( $post, string $target_url, int $type = 301, bool $unpublish = true ): int { if ( ! $post instanceof \WP_Post || '' === $target_url ) { return 0; } $type = in_array( $type, self::REDIRECT_TYPES, true ) ? $type : 301; $source_url = Redirect_Manager_DB::normalize_source( (string) wp_parse_url( get_permalink( $post ), PHP_URL_PATH ) ); $validated = Redirect_Manager_DB::validate_target_url( $target_url ); if ( is_wp_error( $validated ) || Redirect_Manager_DB::is_external_target( $validated ) ) { return 0; } $target_url = $validated; $target_path = Redirect_Manager_DB::normalize_source( (string) wp_parse_url( $target_url, PHP_URL_PATH ) ); if ( $source_url === $target_path ) { return 0; } $redirect_id = (int) get_post_meta( $post->ID, self::POST_REDIRECT_META_KEY, true ); if ( $redirect_id > 0 && $this->db->get_by_id( $redirect_id ) ) { if ( $this->db->source_exists( $source_url, $redirect_id ) ) { return 0; } $this->db->update( $redirect_id, [ 'source_url' => $source_url, 'target_url' => $target_url, 'redirect_type' => $type, 'allow_external' => 0, ] ); } else { if ( $this->db->source_exists( $source_url ) ) { return 0; } $redirect_id = $this->db->insert( [ 'source_url' => $source_url, 'target_url' => $target_url, 'redirect_type' => $type, 'allow_external' => 0, ] ); update_post_meta( $post->ID, self::POST_REDIRECT_META_KEY, $redirect_id ); } if ( $redirect_id > 0 && $unpublish ) { $this->maybe_unpublish_redirected_post( $post ); } return $redirect_id; } /** * Unpublish a source post if the auto-unpublish option is enabled. * * @param \WP_Post $post Source post. * * @return void */ public function maybe_unpublish_redirected_post( $post ): void { if ( ! $post instanceof \WP_Post || 'publish' !== $post->post_status ) { return; } if ( ! $this->get_option( 'auto_unpublish_on_redirect', 1 ) ) { return; } $this->unpublish_redirected_post( $post ); } /** * Move a source post out of published status. * * @param \WP_Post $post Source post. * * @return void */ private function unpublish_redirected_post( $post ): void { $status = $this->get_option( 'unpublish_status', 'redirected' ); if ( 'redirected' === $status && ! get_post_status_object( 'redirected' ) ) { $status = 'draft'; } wp_update_post( [ 'ID' => $post->ID, 'post_status' => $status, ] ); } /** * Build a Redirect Manager URL prefilled from a post. * * @param \WP_Post $post Post object. * * @return string */ private function get_redirect_manager_prefill_url( $post ): string { return self::get_module_admin_url( [ 'tab' => 'rules', 'source_url' => Redirect_Manager_DB::normalize_source( (string) wp_parse_url( get_permalink( $post ), PHP_URL_PATH ) ), 'source_post' => $post->ID, ] ); } /** * Check post redirect workflow eligibility. * * @param \WP_Post $post Post object. * * @return bool */ private function is_post_eligible_for_redirect( $post ): bool { if ( ! $post instanceof \WP_Post || 'publish' !== $post->post_status || ! $this->is_enabled_post_type( $post->post_type ) ) { return false; } if ( current_user_can( 'manage_options' ) ) { return true; } return ! empty( $this->get_option( 'allow_editors_create_redirects' ) ) && current_user_can( 'edit_post', $post->ID ); } /** * Check whether post redirect tools are enabled. * * @return bool */ private function is_post_redirect_tools_enabled(): bool { return ! empty( $this->get_option( 'enable_post_redirect_tools' ) ); } /** * Check whether a post type is enabled for post redirects. * * @param string $post_type Post type. * * @return bool */ private function is_enabled_post_type( string $post_type ): bool { return in_array( $post_type, $this->get_enabled_post_types(), true ); } /** * Get post types enabled for post redirects. * * @return array */ private function get_enabled_post_types(): array { $post_types = $this->get_option( 'post_redirect_post_types', [ 'post', 'page' ] ); return is_array( $post_types ) ? array_map( 'sanitize_key', $post_types ) : [ 'post', 'page' ]; } /** * Read a stored option with default. * * @param string $key Option key. * @param mixed $default Default value. * * @return mixed */ private function get_option( string $key, $default = 0 ) { $options = get_option( self::OPTION_NAME, [] ); return is_array( $options ) && array_key_exists( $key, $options ) ? $options[ $key ] : $default; } }