Action value => translatable label. */ function aitepc_get_expiry_actions() { return array( 'draft' => __( 'Set to Draft', 'ait-easy-post-customization' ), 'trash' => __( 'Move to Trash', 'ait-easy-post-customization' ), 'delete' => __( 'Delete Permanently', 'ait-easy-post-customization' ), 'redirect' => __( 'Redirect to URL', 'ait-easy-post-customization' ), ); } /** * Register the default expiry action setting via Settings API. * Hooks on admin_init, registered to the existing aitepc_expiry_group * so it is saved by the Assign Metabox tab form. */ function aitepc_register_expiry_action_settings() { register_setting( 'aitepc_expiry_group', 'aitepc_default_expiry_action', array( 'sanitize_callback' => 'aitepc_sanitize_expiry_action', 'default' => 'draft', ) ); add_settings_section( 'aitepc_expiry_action_section', __( 'Default Expiry Action', 'ait-easy-post-customization' ), 'aitepc_expiry_action_section_cb', 'aitepc-expiry-date-settings' ); add_settings_field( 'aitepc_default_expiry_action_field', __( 'When a post expires', 'ait-easy-post-customization' ), 'aitepc_default_expiry_action_cb', 'aitepc-expiry-date-settings', 'aitepc_expiry_action_section' ); } add_action( 'admin_init', 'aitepc_register_expiry_action_settings' ); /** * Section description callback. */ function aitepc_expiry_action_section_cb() { echo '

' . esc_html__( 'Choose what happens to a post when its expiry date passes. Individual posts can override this setting from the post editor.', 'ait-easy-post-customization' ) . '

'; } /** * Field callback: renders the default action '; foreach ( $actions as $value => $label ) { printf( '', esc_attr( $value ), selected( $current, $value, false ), esc_html( $label ) ); } echo ''; echo '

' . esc_html__( 'Warning: "Delete Permanently" cannot be undone and all post data will be lost.', 'ait-easy-post-customization' ) . '

'; } /** * Sanitize an expiry action value — must be one of the allowed keys. * * @param string $input Raw input. * @return string Sanitized action, falls back to 'draft'. */ function aitepc_sanitize_expiry_action( $input ) { $allowed = array_keys( aitepc_get_expiry_actions() ); $input = sanitize_key( (string) $input ); return in_array( $input, $allowed, true ) ? $input : 'draft'; } /** * Process the configured expiry action for a single post. * * Called from the daily cron handler instead of the hardcoded wp_update_post(). * Per-post meta takes priority over the site-wide default option. * * @param int $post_id Post ID to process. */ function aitepc_process_expiry_action( $post_id ) { $post_id = absint( $post_id ); if ( ! $post_id ) { return; } // Per-post setting overrides the site default. $action = get_post_meta( $post_id, '_aitepc_expiry_action', true ); if ( empty( $action ) ) { $action = get_option( 'aitepc_default_expiry_action', 'draft' ); } // Validate against the allowlist — never trust stored values blindly. if ( ! in_array( $action, array_keys( aitepc_get_expiry_actions() ), true ) ) { $action = 'draft'; } /** * Fires before a post expiry action is processed. * * @param int $post_id Post ID. * @param string $action Validated expiry action. */ do_action( 'aitepc_before_expiry_action', $post_id, $action ); switch ( $action ) { case 'trash': wp_trash_post( $post_id ); break; case 'delete': wp_delete_post( $post_id, true ); break; case 'redirect': $redirect_url = get_post_meta( $post_id, '_aitepc_redirect_url', true ); $redirect_url = esc_url_raw( (string) $redirect_url ); // Draft the post first, then persist the redirect destination. wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft', ) ); if ( ! empty( $redirect_url ) ) { update_post_meta( $post_id, '_aitepc_expired_redirect', $redirect_url ); } break; case 'draft': default: wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft', ) ); break; } /** * Fires after a post expiry action has been processed. * * @param int $post_id Post ID. * @param string $action Expiry action that was applied. */ do_action( 'aitepc_after_expiry_action', $post_id, $action ); } /** * Intercept 404s to redirect expired posts that had the 'redirect' action. * * When a post is expired via the redirect action it is set to draft and * its destination URL is stored in `_aitepc_expired_redirect`. This hook * fires before WordPress renders the 404 template, finds any draft post * matching the requested slug, and issues a 301 redirect. * * Hooked at priority 1 so it runs before other pre_handle_404 handlers. * * @param bool $preempt Short-circuit flag from WordPress. * @param WP_Query $wp_query Main query object. * @return bool */ function aitepc_pre_handle_404( $preempt, $wp_query ) { if ( ! $wp_query->is_main_query() ) { return $preempt; } $slug = $wp_query->get( 'name' ); if ( empty( $slug ) ) { return $preempt; } $post_types = get_option( 'aitepc_expiry_date_post_types', array( 'post' ) ); if ( ! is_array( $post_types ) || empty( $post_types ) ) { return $preempt; } $posts = get_posts( array( 'name' => sanitize_title( $slug ), 'post_status' => 'draft', 'post_type' => $post_types, 'numberposts' => 1, 'meta_key' => '_aitepc_expired_redirect', 'meta_compare' => 'EXISTS', 'fields' => 'ids', 'no_found_rows' => true, ) ); if ( ! empty( $posts ) ) { $redirect_url = get_post_meta( $posts[0], '_aitepc_expired_redirect', true ); if ( ! empty( $redirect_url ) ) { wp_redirect( esc_url_raw( $redirect_url ), 301 ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect exit; } } return $preempt; } add_filter( 'pre_handle_404', 'aitepc_pre_handle_404', 1, 2 );