debug_enabled = (defined('WP_DEBUG') && WP_DEBUG); add_action('plugins_loaded', array($this, 'check_compatibility'), 5); } /** * The running plugin instance (plugin-file variables are not reliable * globals under every loader, e.g. WP-CLI). */ public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } public function check_compatibility() { if (version_compare(get_bloginfo('version'), '5.0', '<')) { add_action('admin_notices', array($this, 'wordpress_version_notice')); return; } if (version_compare(PHP_VERSION, '7.4', '<')) { add_action('admin_notices', array($this, 'php_version_notice')); return; } if (!function_exists('get_post_types') || !function_exists('get_posts') || !function_exists('wp_create_nonce')) { add_action('admin_notices', array($this, 'missing_functions_notice')); return; } $this->plugin_ready = true; // Cache invalidation must run in EVERY context: the block editor saves // through the REST API, and posts also change via WP-CLI, cron and the // front end, none of which are is_admin(). add_action('save_post', array($this, 'clear_navigation_cache')); add_action('deleted_post', array($this, 'clear_navigation_cache')); add_action('trashed_post', array($this, 'clear_navigation_cache')); add_action('untrashed_post', array($this, 'clear_navigation_cache')); // Simple Custom Post Order writes menu_order directly (no save_post), // so hook its AJAX handlers before they run. add_action('wp_ajax_update-menu-order', array($this, 'bump_cache_generation'), 1); add_action('wp_ajax_scpo_reset_order', array($this, 'bump_cache_generation'), 1); add_action('wp_ajax_scpo_set_position', array($this, 'bump_cache_generation'), 1); // Plugin (de)activation can register or remove post types. add_action('activated_plugin', array($this, 'bump_cache_generation')); add_action('deactivated_plugin', array($this, 'bump_cache_generation')); if (is_admin()) { add_action('init', array($this, 'init'), 20); add_action('wp_ajax_twf_admin_posts_nav_get_posts', array($this, 'ajax_get_nav_posts_with_rate_limit')); add_action('wp_ajax_twf_admin_posts_nav_update_sort', array($this, 'ajax_update_sort_preference')); } } public function init() { if (!$this->plugin_ready) { return; } if (!is_admin() || (!current_user_can('edit_posts') && !current_user_can('edit_pages'))) { return; } $this->supported_post_types = $this->get_supported_post_types_cached(); if (empty($this->supported_post_types)) { return; } add_action('edit_form_after_title', array($this, 'add_navigation_buttons')); add_action('enqueue_block_editor_assets', array($this, 'enqueue_gutenberg_assets')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); if ($this->debug_enabled) { add_action('admin_notices', array($this, 'show_supported_types_notice')); } } /* --------------------------------------------------------------------- * Sorting * ------------------------------------------------------------------ */ /** * The sort methods offered to users, keyed by the value stored in user meta. */ private function get_sort_options() { return array( 'date' => __('Date Published', 'admin-posts-navigation'), 'alphabetical' => __('Alphabetical', 'admin-posts-navigation'), 'post_id' => __('Post ID', 'admin-posts-navigation'), 'menu_order' => __('Sort Order', 'admin-posts-navigation'), ); } private function is_valid_sort_method($sort_method) { return is_string($sort_method) && array_key_exists($sort_method, $this->get_sort_options()); } /** * Whether a post type carries a meaningful manual order: it exposes the * "Order" attribute, or Simple Custom Post Order manages it. */ private function post_type_uses_menu_order($post_type) { if (post_type_supports($post_type, 'page-attributes')) { return true; } $scpo_options = get_option('scporder_options'); if (is_array($scpo_options) && !empty($scpo_options['objects']) && is_array($scpo_options['objects'])) { return in_array($post_type, $scpo_options['objects'], true); } return false; } /** * Default sort method for a post type when the user has not chosen one. */ private function get_default_sort($post_type) { $default = $this->post_type_uses_menu_order($post_type) ? 'menu_order' : 'date'; /** * Filter the default sort method for a post type. * * @param string $default One of: date, alphabetical, post_id, menu_order. * @param string $post_type Post type name. */ $default = apply_filters('twf_admin_posts_navigation_default_sort', $default, $post_type); return $this->is_valid_sort_method($default) ? $default : 'date'; } /** * Get user's sort preference for a specific post type */ private function get_sort_preference($post_type) { $user_id = get_current_user_id(); $preference_key = 'twf_admin_posts_nav_sort_' . $post_type; $preference = get_user_meta($user_id, $preference_key, true); if (!$this->is_valid_sort_method($preference)) { $preference = $this->get_default_sort($post_type); } return $preference; } /** * Query orderby for a sort method, in WP_Query array form so every sort * has a deterministic tiebreaker. */ private function get_orderby_for_sort($sort_method) { switch ($sort_method) { case 'alphabetical': return array('title' => 'ASC', 'ID' => 'ASC'); case 'post_id': return array('ID' => 'ASC'); case 'menu_order': return array('menu_order' => 'ASC', 'title' => 'ASC', 'ID' => 'ASC'); case 'date': default: return array('date' => 'DESC', 'ID' => 'DESC'); } } /** * AJAX handler for updating sort preference */ public function ajax_update_sort_preference() { $user_id = get_current_user_id(); // Security: Check user permissions first if (!current_user_can('edit_posts') && !current_user_can('edit_pages')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'admin-posts-navigation')), 403); } // Security: Use check_ajax_referer for AJAX nonce verification check_ajax_referer('twf_admin_posts_navigation_sort_' . $user_id, 'nonce'); $post_type = isset($_POST['post_type']) ? sanitize_key(wp_unslash($_POST['post_type'])) : ''; $sort_method = isset($_POST['sort_method']) ? sanitize_key(wp_unslash($_POST['sort_method'])) : ''; if (!$post_type || !post_type_exists($post_type) || !in_array($post_type, $this->supported_post_types, true)) { wp_send_json_error(array('message' => __('Unsupported post type.', 'admin-posts-navigation')), 400); } if (!$this->is_valid_sort_method($sort_method)) { wp_send_json_error(array('message' => __('Invalid parameters.', 'admin-posts-navigation')), 400); } $preference_key = 'twf_admin_posts_nav_sort_' . $post_type; update_user_meta($user_id, $preference_key, $sort_method); wp_send_json_success(array('sort_method' => $sort_method)); } /* --------------------------------------------------------------------- * Logging / notices * ------------------------------------------------------------------ */ /** * Debug logging function that respects WordPress debugging settings */ private function debug_log($message, $context = array()) { if (!$this->debug_enabled) { return; } $log_message = 'TWF Admin Posts Navigation: ' . $message; if (!empty($context)) { $log_message .= ' Context: ' . wp_json_encode($context); } // Only use WordPress debug logging - no error_log() if (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG && function_exists('wp_debug_log')) { wp_debug_log($log_message); } } /** * Show admin notice for errors (only to administrators) */ private function show_admin_error($message) { add_action('admin_notices', function() use ($message) { if (current_user_can('manage_options')) { printf( '

%s: %s

', esc_html__('Admin Posts Navigation', 'admin-posts-navigation'), esc_html($message) ); } }); } public function wordpress_version_notice() { echo '

'; printf( '%s: %s', esc_html__('Admin Posts Navigation', 'admin-posts-navigation'), esc_html__('This plugin requires WordPress 5.0 or higher.', 'admin-posts-navigation') ); echo '

'; } public function php_version_notice() { echo '

'; printf( '%s: %s', esc_html__('Admin Posts Navigation', 'admin-posts-navigation'), esc_html__('This plugin requires PHP 7.4 or higher.', 'admin-posts-navigation') ); echo '

'; } public function missing_functions_notice() { echo '

'; printf( '%s: %s', esc_html__('Admin Posts Navigation', 'admin-posts-navigation'), esc_html__('Required WordPress functions are missing. Please check your WordPress installation.', 'admin-posts-navigation') ); echo '

'; } /* --------------------------------------------------------------------- * Supported post types * ------------------------------------------------------------------ */ private function get_supported_types_cache_key() { return $this->cache_key_prefix . 'types_' . $this->get_cache_generation() . '_' . get_current_user_id(); } private function get_supported_post_types_cached() { $cache_key = $this->get_supported_types_cache_key(); // Use WordPress Object Cache API $cached_types = wp_cache_get($cache_key, $this->cache_group); if ($cached_types !== false && is_array($cached_types)) { return $cached_types; } // Fallback to transients if object cache is not persistent $cached_types = get_transient($cache_key); if ($cached_types !== false && is_array($cached_types)) { wp_cache_set($cache_key, $cached_types, $this->cache_group, 300); return $cached_types; } try { $supported_types = $this->get_supported_post_types(); wp_cache_set($cache_key, $supported_types, $this->cache_group, 300); set_transient($cache_key, $supported_types, $this->cache_expiration); $this->debug_log('Generated fresh supported post types', array('types' => $supported_types)); return $supported_types; } catch (Exception $e) { $this->debug_log('Error getting supported post types: ' . $e->getMessage()); $this->show_admin_error(__('Unable to detect supported post types. Using defaults.', 'admin-posts-navigation')); return array('post', 'page'); } } private function get_supported_post_types() { try { $all_post_types = get_post_types(array('public' => true), 'objects'); if (!is_array($all_post_types)) { throw new Exception('get_post_types() did not return an array'); } $supported_types = array(); $excluded_types = apply_filters('twf_admin_posts_navigation_excluded_types', array( 'attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset', 'oembed_cache', 'user_request', 'wp_block', 'wp_template', 'wp_template_part', 'wp_global_styles', 'wp_navigation' )); foreach ($all_post_types as $post_type) { if (!$post_type || !is_object($post_type) || !isset($post_type->name)) { continue; } if (!in_array($post_type->name, $excluded_types, true) && isset($post_type->cap->edit_posts) && current_user_can($post_type->cap->edit_posts)) { $supported_types[] = sanitize_key($post_type->name); } } // Ensure core post types are included if they exist and user has permission if (post_type_exists('post') && current_user_can('edit_posts') && !in_array('post', $supported_types, true)) { $supported_types[] = 'post'; } if (post_type_exists('page') && current_user_can('edit_pages') && !in_array('page', $supported_types, true)) { $supported_types[] = 'page'; } return array_values(array_unique($supported_types)); } catch (Exception $e) { $this->debug_log('Failed to get supported post types: ' . $e->getMessage()); throw new Exception('Failed to get supported post types'); } } /** * Whether navigation should be offered for this post object at all. */ private function is_navigable_post($post) { if (!$post || !is_object($post) || !isset($post->post_type) || !isset($post->ID)) { return false; } if (!in_array($post->post_type, $this->supported_post_types, true)) { return false; } // A brand-new post has no place in the sequence yet. if (isset($post->post_status) && 'auto-draft' === $post->post_status) { return false; } return current_user_can('edit_post', $post->ID); } /* --------------------------------------------------------------------- * Classic Editor output * ------------------------------------------------------------------ */ public function add_navigation_buttons() { global $post; if (!$this->is_navigable_post($post)) { return; } try { $nav_data = $this->get_navigation_data_cached($post); if (!$nav_data || (!$nav_data['prev'] && !$nav_data['next'])) { return; } $post_type_obj = get_post_type_object($post->post_type); $post_type_label = $post_type_obj && isset($post_type_obj->labels->singular_name) ? $post_type_obj->labels->singular_name : ucfirst($post->post_type); $current_sort = $this->get_sort_preference($post->post_type); echo '
'; // Header with dropdown echo '
'; // translators: %s is the post type name (e.g., "Posts", "Pages", "Products") echo '

' . esc_html(sprintf(__('Navigate %s', 'admin-posts-navigation'), $post_type_label)) . '

'; echo ''; echo '
'; // Navigation buttons echo '
'; $this->render_classic_button($nav_data['prev'], 'prev'); $this->render_classic_button($nav_data['next'], 'next'); echo '
'; if (isset($nav_data['current_position']) && isset($nav_data['total_posts'])) { echo '
'; printf( '%s', esc_html(sprintf( // translators: %1$d is current position number, %2$d is total posts, %3$s is post type name __('Position: %1$d of %2$d %3$s', 'admin-posts-navigation'), $nav_data['current_position'], $nav_data['total_posts'], strtolower($post_type_obj && isset($post_type_obj->labels->name) ? $post_type_obj->labels->name : $post->post_type) )) ); echo '
'; } echo '
'; } catch (Exception $e) { $this->debug_log('Error in add_navigation_buttons: ' . $e->getMessage(), array('post_id' => $post->ID)); } } private function render_classic_button($target, $direction) { if (!$target || !is_object($target)) { return; } $edit_link = get_edit_post_link($target->ID); if (!$edit_link) { return; } $title = wp_trim_words($target->post_title, 5); if ('prev' === $direction) { $label = '← ' . __('Previous', 'admin-posts-navigation') . ': ' . $title; } else { $label = __('Next', 'admin-posts-navigation') . ': ' . $title . ' →'; } printf( '%s', esc_url($edit_link), esc_attr($direction), esc_attr($target->post_title), absint($target->ID), esc_html($label) ); } /* --------------------------------------------------------------------- * Assets * ------------------------------------------------------------------ */ public function enqueue_gutenberg_assets() { global $post; if (!$this->is_navigable_post($post)) { return; } wp_enqueue_script( 'twf-admin-posts-navigation-gutenberg', plugin_dir_url(__FILE__) . 'assets/gutenberg.js', array('wp-element', 'wp-components', 'wp-data', 'wp-plugins', 'wp-editor', 'wp-edit-post'), $this->version, true ); $post_type_obj = get_post_type_object($post->post_type); $current_sort = $this->get_sort_preference($post->post_type); wp_localize_script('twf-admin-posts-navigation-gutenberg', 'twfAdminPostsNavigation', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('twf_admin_posts_navigation_' . get_current_user_id() . '_' . $post->ID), 'sortNonce' => wp_create_nonce('twf_admin_posts_navigation_sort_' . get_current_user_id()), 'postId' => absint($post->ID), 'postType' => sanitize_key($post->post_type), 'postTypeLabel' => $post_type_obj && isset($post_type_obj->labels->singular_name) ? $post_type_obj->labels->singular_name : ucfirst($post->post_type), 'postTypeLabelPlural' => $post_type_obj && isset($post_type_obj->labels->name) ? $post_type_obj->labels->name : ucfirst($post->post_type) . 's', 'currentSort' => $current_sort, 'sortOptions' => $this->get_sort_options(), 'i18n' => array( 'sortBy' => __('Sort By', 'admin-posts-navigation'), 'loading' => __('Loading navigation...', 'admin-posts-navigation'), 'updatingSort' => __('Updating sort order...', 'admin-posts-navigation'), 'previous' => __('Previous', 'admin-posts-navigation'), 'next' => __('Next', 'admin-posts-navigation'), 'untitled' => __('(no title)', 'admin-posts-navigation'), /* translators: %1$d is current position number, %2$d is total posts, %3$s is post type name */ 'position' => __('Position: %1$d of %2$d %3$s', 'admin-posts-navigation'), /* translators: %s is the plural post type name */ 'noAdjacent' => __('No adjacent %s found.', 'admin-posts-navigation'), /* translators: %1$d is total posts, %2$s is the plural post type name */ 'total' => __('Total: %1$d %2$s', 'admin-posts-navigation'), 'loadError' => __('Navigation data could not be loaded.', 'admin-posts-navigation'), 'timeoutError' => __('Request timed out.', 'admin-posts-navigation'), 'sortError' => __('Failed to update sort preference.', 'admin-posts-navigation'), 'configError' => __('Configuration error.', 'admin-posts-navigation'), ), )); } public function enqueue_admin_assets($hook) { if (!in_array($hook, array('post.php', 'post-new.php'), true)) { return; } $screen = function_exists('get_current_screen') ? get_current_screen() : null; if ($screen && isset($screen->post_type) && $screen->post_type && !in_array($screen->post_type, $this->supported_post_types, true)) { return; } wp_enqueue_style( 'twf-admin-posts-navigation-styles', plugin_dir_url(__FILE__) . 'assets/admin-styles.css', array(), $this->version ); // Inline script for the Classic Editor dropdown wp_add_inline_script('jquery', ' jQuery(document).ready(function($) { $(document).on("change", ".twf-admin-posts-nav-sort-dropdown", function() { var $dropdown = $(this); var postType = $dropdown.data("post-type"); var sortMethod = $dropdown.val(); var nonce = $dropdown.data("nonce"); if (!postType || !sortMethod || !nonce) return; $dropdown.prop("disabled", true); $.post(ajaxurl, { action: "twf_admin_posts_nav_update_sort", post_type: postType, sort_method: sortMethod, nonce: nonce }) .done(function(response) { if (response && response.success) { location.reload(); } else { window.alert(' . wp_json_encode(__('Failed to update sort preference.', 'admin-posts-navigation')) . '); $dropdown.prop("disabled", false); } }) .fail(function() { window.alert(' . wp_json_encode(__('Failed to update sort preference.', 'admin-posts-navigation')) . '); $dropdown.prop("disabled", false); }); }); }); '); } /* --------------------------------------------------------------------- * AJAX: navigation data * ------------------------------------------------------------------ */ public function ajax_get_nav_posts_with_rate_limit() { $post_id = 0; try { $user_id = get_current_user_id(); // Security: Check user permissions first if (!current_user_can('edit_posts') && !current_user_can('edit_pages')) { wp_send_json_error(array('message' => __('You do not have permission to perform this action.', 'admin-posts-navigation')), 403); } $rate_limit_key = 'twf_admin_posts_navigation_rate_limit_' . $user_id . '_' . wp_hash(wp_get_session_token()); $requests = wp_cache_get($rate_limit_key, 'twf_admin_posts_navigation_rate'); if ($requests === false) { $requests = get_transient($rate_limit_key); } if ($requests && $requests >= 30) { wp_send_json_error(array('message' => __('Rate limit exceeded. Please wait before making another request.', 'admin-posts-navigation')), 429); } $new_count = $requests ? $requests + 1 : 1; wp_cache_set($rate_limit_key, $new_count, 'twf_admin_posts_navigation_rate', 60); set_transient($rate_limit_key, $new_count, 60); $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0; if (!$post_id) { wp_send_json_error(array('message' => __('Invalid post ID.', 'admin-posts-navigation')), 400); } // Security: Use check_ajax_referer for AJAX nonce verification check_ajax_referer('twf_admin_posts_navigation_' . $user_id . '_' . $post_id, 'nonce'); $post = get_post($post_id); if (!$post || !is_object($post) || !isset($post->post_type)) { wp_send_json_error(array('message' => __('Invalid post.', 'admin-posts-navigation')), 404); } if (!in_array($post->post_type, $this->supported_post_types, true)) { wp_send_json_error(array('message' => __('Unsupported post type.', 'admin-posts-navigation')), 400); } // Security: Check user can edit this specific post if (!current_user_can('edit_post', $post_id)) { wp_send_json_error(array('message' => __('You do not have permission to edit this post.', 'admin-posts-navigation')), 403); } $nav_data = $this->get_navigation_data_cached($post); $response = array( 'prev' => null, 'next' => null, 'current_position' => isset($nav_data['current_position']) ? absint($nav_data['current_position']) : 0, 'total_posts' => isset($nav_data['total_posts']) ? absint($nav_data['total_posts']) : 0, 'sort_method' => $this->get_sort_preference($post->post_type), ); foreach (array('prev', 'next') as $direction) { if ($nav_data && isset($nav_data[$direction]) && is_object($nav_data[$direction])) { $edit_link = get_edit_post_link($nav_data[$direction]->ID, 'raw'); if ($edit_link) { $response[$direction] = array( 'id' => absint($nav_data[$direction]->ID), 'title' => sanitize_text_field($nav_data[$direction]->post_title), 'edit_link' => esc_url_raw($edit_link), ); } } } wp_send_json_success($response); } catch (Exception $e) { $this->debug_log('AJAX error: ' . $e->getMessage(), array('post_id' => $post_id)); wp_send_json_error(array('message' => __('Navigation data could not be loaded.', 'admin-posts-navigation')), 500); } } /* --------------------------------------------------------------------- * Navigation data * ------------------------------------------------------------------ */ private function get_navigation_data_cached($current_post) { if (!$current_post || !is_object($current_post) || !isset($current_post->ID)) { return null; } $current_sort = $this->get_sort_preference($current_post->post_type); // The neighbour set depends on the user (capabilities), the post, the // sort method and the cache generation. $cache_key = $this->cache_key_prefix . 'nav_' . $this->get_cache_generation() . '_' . get_current_user_id() . '_' . $current_post->ID . '_' . $current_post->post_type . '_' . $current_sort; $cached_data = wp_cache_get($cache_key, $this->cache_group); if ($cached_data !== false && is_array($cached_data)) { return $cached_data; } $cached_data = get_transient($cache_key); if ($cached_data !== false && is_array($cached_data)) { wp_cache_set($cache_key, $cached_data, $this->cache_group, 900); return $cached_data; } try { $nav_data = $this->get_navigation_data($current_post, $current_sort); if ($nav_data) { wp_cache_set($cache_key, $nav_data, $this->cache_group, 900); set_transient($cache_key, $nav_data, 1800); $this->debug_log('Generated fresh navigation data', array('post_id' => $current_post->ID)); } return $nav_data; } catch (Exception $e) { $this->debug_log('Error getting navigation data: ' . $e->getMessage(), array('post_id' => $current_post->ID)); return null; } } private function get_navigation_data($current_post, $sort_method) { try { if (!$current_post || !is_object($current_post) || !isset($current_post->ID) || !isset($current_post->post_type)) { throw new Exception('Invalid post object'); } $post_type = sanitize_key($current_post->post_type); $orderby = $this->get_orderby_for_sort($sort_method); /** * Filter the query ordering. * * Receives WP_Query array form (column => direction). Returning a * string is still supported: it is used together with the * `twf_admin_posts_navigation_order` filter as before. * * @param array|string $orderby Orderby (array form by default). * @param string $post_type Post type name. */ $orderby = apply_filters('twf_admin_posts_navigation_orderby', $orderby, $post_type); $legacy_order = ('date' === $sort_method) ? 'DESC' : 'ASC'; $order = apply_filters('twf_admin_posts_navigation_order', $legacy_order, $post_type); $args = array( 'post_type' => $post_type, 'post_status' => array('publish', 'draft', 'pending', 'private', 'future'), /** * Filter the maximum number of posts considered for navigation. * -1 (default) means all posts of the type. Only IDs are fetched. */ 'posts_per_page' => (int) apply_filters('twf_admin_posts_navigation_max_posts', -1, $post_type), 'fields' => 'ids', 'suppress_filters' => false, 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, 'cache_results' => true, ); if (is_array($orderby)) { $args['orderby'] = array(); foreach ($orderby as $column => $direction) { $args['orderby'][sanitize_key($column)] = ('DESC' === strtoupper((string) $direction)) ? 'DESC' : 'ASC'; } } else { $args['orderby'] = sanitize_text_field((string) $orderby); $args['order'] = ('DESC' === strtoupper((string) $order)) ? 'DESC' : 'ASC'; } // Users who cannot edit others' posts only ever navigate their own. $post_type_obj = get_post_type_object($post_type); if ($post_type_obj && isset($post_type_obj->cap->edit_others_posts) && !current_user_can($post_type_obj->cap->edit_others_posts)) { $args['author'] = get_current_user_id(); } $args = apply_filters('twf_admin_posts_navigation_query_args', $args, $current_post); $all_posts = get_posts($args); if (!is_array($all_posts)) { throw new Exception('get_posts() did not return an array'); } $all_posts = array_map('intval', $all_posts); $current_index = array_search((int) $current_post->ID, $all_posts, true); $prev_post = null; $next_post = null; $current_position = 0; $total_posts = count($all_posts); if ($current_index !== false) { $current_position = $current_index + 1; $prev_post = $this->find_editable_neighbor($all_posts, $current_index, -1); $next_post = $this->find_editable_neighbor($all_posts, $current_index, 1); } return array( 'prev' => $prev_post, 'next' => $next_post, 'current_position' => $current_position, 'total_posts' => $total_posts, ); } catch (Exception $e) { $this->debug_log('Navigation data generation failed: ' . $e->getMessage()); throw new Exception('Navigation data generation failed'); } } /** * Walk from the current index in the given direction until a post the * current user may edit is found. */ private function find_editable_neighbor(array $ids, $current_index, $step) { $count = count($ids); $steps = 0; for ($i = $current_index + $step; $i >= 0 && $i < $count; $i += $step) { if (++$steps > $this->neighbor_search_limit) { break; } if (!current_user_can('edit_post', $ids[$i])) { continue; } $post = get_post($ids[$i]); if ($post) { return $post; } } return null; } /* --------------------------------------------------------------------- * Cache invalidation * ------------------------------------------------------------------ */ private function get_cache_generation() { $generation = absint(get_option(self::CACHE_GEN_OPTION, 1)); return $generation > 0 ? $generation : 1; } /** * Invalidate every cached navigation and post-type list at once. */ public function bump_cache_generation() { update_option(self::CACHE_GEN_OPTION, $this->get_cache_generation() + 1, false); } /** * save_post / deleted_post / trashed_post / untrashed_post callback. */ public function clear_navigation_cache($post_id = null) { if (!$post_id) { return; } // Revisions, autosaves and brand-new auto-drafts never affect the sequence. if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) { return; } $post = get_post($post_id); if ($post && isset($post->post_status) && 'auto-draft' === $post->post_status) { return; } $this->bump_cache_generation(); } /* --------------------------------------------------------------------- * Misc * ------------------------------------------------------------------ */ public function show_supported_types_notice() { $screen = get_current_screen(); if (!$screen || !in_array($screen->id, array('edit-post', 'edit-page'), true) || !current_user_can('manage_options')) { return; } $notice_key = 'twf_admin_posts_navigation_notice_' . get_current_user_id() . '_' . wp_hash(wp_get_session_token()); if (get_transient($notice_key)) { return; } set_transient($notice_key, true, 3600); echo '
'; printf( '

%s: %s

', esc_html__('Admin Posts Navigation', 'admin-posts-navigation'), esc_html(sprintf( // translators: %1$d is the number of post types, %2$s is the list of post type names __('Active for %1$d post types: %2$s', 'admin-posts-navigation'), count($this->supported_post_types), implode(', ', array_map('ucfirst', $this->supported_post_types)) )) ); echo '
'; } public function get_supported_types() { return $this->plugin_ready ? $this->supported_post_types : array(); } } // Initialize the plugin $twf_admin_posts_navigation = TWF_AdminPostsNavigation::get_instance(); // Activation hook: start from a fresh cache generation. function twf_admin_posts_navigation_activation_hook() { update_option(TWF_AdminPostsNavigation::CACHE_GEN_OPTION, time(), false); } register_activation_hook(__FILE__, 'twf_admin_posts_navigation_activation_hook'); // Deactivation hook: invalidate caches. function twf_admin_posts_navigation_deactivation_hook() { update_option(TWF_AdminPostsNavigation::CACHE_GEN_OPTION, time(), false); } register_deactivation_hook(__FILE__, 'twf_admin_posts_navigation_deactivation_hook'); // Uninstall hook function twf_admin_posts_navigation_uninstall_hook() { delete_option(TWF_AdminPostsNavigation::CACHE_GEN_OPTION); delete_option('twf_admin_posts_navigation_activated'); // legacy (< 1.5.0) // Clean up user sort preferences using WordPress functions only $users = get_users(array('fields' => 'ID')); if (!empty($users)) { foreach ($users as $user_id) { $user_meta = get_user_meta($user_id); if (is_array($user_meta)) { foreach ($user_meta as $meta_key => $meta_value) { if (strpos($meta_key, 'twf_admin_posts_nav_sort_') === 0) { delete_user_meta($user_id, $meta_key); } } } } } } register_uninstall_hook(__FILE__, 'twf_admin_posts_navigation_uninstall_hook'); // Plugin action links function twf_admin_posts_navigation_plugin_action_links($links) { try { $supported_types = TWF_AdminPostsNavigation::get_instance()->get_supported_types(); if (is_array($supported_types) && !empty($supported_types)) { $settings_link = '' . esc_html(sprintf( // translators: %s is a list of supported post types __('Supports: %s', 'admin-posts-navigation'), implode(', ', array_slice($supported_types, 0, 3)) . (count($supported_types) > 3 ? '... (+' . (count($supported_types) - 3) . ' more)' : '') )) . ''; array_unshift($links, $settings_link); } } catch (Exception $e) { if (defined('WP_DEBUG') && WP_DEBUG && defined('WP_DEBUG_LOG') && WP_DEBUG_LOG && function_exists('wp_debug_log')) { wp_debug_log('TWF Admin Posts Navigation: Plugin action links error - ' . $e->getMessage()); } } return $links; } add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'twf_admin_posts_navigation_plugin_action_links');