| @@ -3,9 +3,8 @@ | ||
| 3 | 3 | namespace Code_Snippets\Admin\Menus; |
| 4 | 4 | |
| 5 | 5 | use Code_Snippets\Admin\Contextual_Help; |
| 6 | 6 | use Code_Snippets\Model\Snippet; |
| 7 | -use WP_Screen; | |
| 8 | 7 | use function Code_Snippets\code_snippets; |
| 9 | 8 | use function Code_Snippets\get_all_snippet_tags; |
| 10 | 9 | use function Code_Snippets\get_snippet; |
| 11 | 10 | use function Code_Snippets\Settings\get_setting; |
| @@ -48,10 +47,8 @@ | ||
| 48 | 47 | _x( 'Edit Snippet', 'menu label', 'code-snippets' ), |
| 49 | 48 | __( 'Edit Snippet', 'code-snippets' ) |
| 50 | 49 | ); |
| 51 | 50 | |
| 52 | - add_action( 'current_screen', array( $this, 'maybe_hide_menu_item' ) ); | |
| 53 | - | |
| 54 | 51 | $this->remove_debug_bar_codemirror(); |
| 55 | 52 | } |
| 56 | 53 | |
| 57 | 54 | /** |
| @@ -59,10 +56,21 @@ | ||
| 59 | 56 | * |
| 60 | 57 | * @return void |
| 61 | 58 | */ |
| 62 | 59 | public function register() { |
| 63 | - parent::register(); | |
| 60 | + // The page itself is always registered outside the menu, so nothing that | |
| 61 | + // reads the menu during `admin_menu` — menu editors and role managers | |
| 62 | + // among them — ever sees it. Registering it in the menu and removing it | |
| 63 | + // afterward is why a page that never renders in the menu could still end | |
| 64 | + // up in someone's saved menu. | |
| 65 | + $this->register_without_menu_item(); | |
| 64 | 66 | |
| 67 | + $snippet_id = $this->get_requested_snippet_id(); | |
| 68 | + | |
| 69 | + if ( $snippet_id ) { | |
| 70 | + $this->add_current_snippet_menu_item( $snippet_id ); | |
| 71 | + } | |
| 72 | + | |
| 65 | 73 | // Create New Snippet menu. |
| 66 | 74 | $this->add_menu( |
| 67 | 75 | code_snippets()->get_menu_slug( 'add' ), |
| 68 | 76 | _x( 'Add New', 'menu label', 'code-snippets' ), |
| @@ -70,38 +78,101 @@ | ||
| 70 | 78 | ); |
| 71 | 79 | } |
| 72 | 80 | |
| 73 | 81 | /** |
| 74 | - * Retrieve every hookname registered by this menu, including the separate | |
| 75 | - * "Add New" page, so screen-based checks recognise both editor views. | |
| 82 | + * The snippet this request is editing, if any. | |
| 76 | 83 | * |
| 77 | - * @return string[] | |
| 84 | + * Read from the request rather than the current screen, because this runs | |
| 85 | + * during `admin_menu`, before the screen is known. | |
| 86 | + * | |
| 87 | + * @return int Snippet ID, or 0 when not editing a specific snippet. | |
| 78 | 88 | */ |
| 79 | - public function get_hooknames(): array { | |
| 80 | - return [ | |
| 81 | - $this->get_hookname(), | |
| 82 | - get_plugin_page_hookname( code_snippets()->get_menu_slug( 'add' ), $this->base_slug ), | |
| 83 | - ]; | |
| 89 | + private function get_requested_snippet_id(): int { | |
| 90 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing parameter. | |
| 91 | + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; | |
| 92 | + | |
| 93 | + if ( $page !== $this->slug ) { | |
| 94 | + return 0; | |
| 95 | + } | |
| 96 | + | |
| 97 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing parameter. | |
| 98 | + return isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; | |
| 84 | 99 | } |
| 85 | 100 | |
| 86 | 101 | /** |
| 87 | - * Hide the static Edit Snippet menu item unless a specific snippet is being edited. | |
| 102 | + * Register the edit page without placing it in the admin menu. | |
| 88 | 103 | * |
| 89 | - * @param WP_Screen $screen Current admin screen. | |
| 104 | + * @return void | |
| 105 | + */ | |
| 106 | + private function register_without_menu_item(): void { | |
| 107 | + $hook = add_submenu_page( | |
| 108 | + '', | |
| 109 | + $this->title, | |
| 110 | + $this->label, | |
| 111 | + code_snippets()->get_cap(), | |
| 112 | + $this->slug, | |
| 113 | + array( $this, 'render' ) | |
| 114 | + ); | |
| 115 | + | |
| 116 | + if ( $hook ) { | |
| 117 | + add_action( 'load-' . $hook, array( $this, 'load' ) ); | |
| 118 | + } | |
| 119 | + } | |
| 120 | + | |
| 121 | + /** | |
| 122 | + * Mark the snippet being edited in the menu. | |
| 90 | 123 | * |
| 124 | + * Added as a plain link rather than a second registration of the page: a | |
| 125 | + * submenu slug doubles as the identifier WordPress checks permissions | |
| 126 | + * against, so carrying the ID in the slug would make `page=edit-snippet` | |
| 127 | + * match nothing and the screen would refuse to load. | |
| 128 | + * | |
| 129 | + * @param int $snippet_id Snippet being edited. | |
| 130 | + * | |
| 91 | 131 | * @return void |
| 92 | 132 | */ |
| 93 | - public function maybe_hide_menu_item( WP_Screen $screen ) { | |
| 94 | - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin menu context. | |
| 95 | - $current_id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; | |
| 96 | - $edit_hook = get_plugin_page_hookname( $this->slug, $this->base_slug ); | |
| 97 | - $edit_hook .= $screen->in_admin( 'network' ) ? '-network' : ''; | |
| 133 | + private function add_current_snippet_menu_item( int $snippet_id ): void { | |
| 134 | + add_submenu_page( | |
| 135 | + $this->base_slug, | |
| 136 | + $this->title, | |
| 137 | + $this->label, | |
| 138 | + code_snippets()->get_cap(), | |
| 139 | + add_query_arg( | |
| 140 | + [ | |
| 141 | + 'page' => $this->slug, | |
| 142 | + 'id' => $snippet_id, | |
| 143 | + ], | |
| 144 | + 'admin.php' | |
| 145 | + ), | |
| 146 | + '', | |
| 147 | + 1 | |
| 148 | + ); | |
| 149 | + } | |
| 98 | 150 | |
| 99 | - if ( ( $screen->id === $edit_hook || $screen->base === $edit_hook ) && 0 < $current_id ) { | |
| 100 | - return; | |
| 101 | - } | |
| 151 | + /** | |
| 152 | + * Retrieve the hookname of the edit page. | |
| 153 | + * | |
| 154 | + * The page is registered without a parent, so WordPress files it under | |
| 155 | + * "admin_page_" rather than under the Snippets menu; deriving it from the | |
| 156 | + * menu slug would name a screen that does not exist. | |
| 157 | + * | |
| 158 | + * @return string | |
| 159 | + */ | |
| 160 | + public function get_hookname(): string { | |
| 161 | + return get_plugin_page_hookname( $this->slug, '' ); | |
| 162 | + } | |
| 102 | 163 | |
| 103 | - remove_submenu_page( $this->base_slug, $this->slug ); | |
| 164 | + /** | |
| 165 | + * Retrieve every hookname registered by this menu, including the separate | |
| 166 | + * "Add New" page, so screen-based checks recognize both editor views. | |
| 167 | + * | |
| 168 | + * @return string[] | |
| 169 | + */ | |
| 170 | + public function get_hooknames(): array { | |
| 171 | + return [ | |
| 172 | + $this->get_hookname(), | |
| 173 | + get_plugin_page_hookname( code_snippets()->get_menu_slug( 'add' ), $this->base_slug ), | |
| 174 | + ]; | |
| 104 | 175 | } |
| 105 | 176 | |
| 106 | 177 | /** |
| 107 | 178 | * Executed when the menu is loaded. |
| @@ -123,11 +194,10 @@ | ||
| 123 | 194 | * |
| 124 | 195 | * @return void |
| 125 | 196 | */ |
| 126 | 197 | protected function ensure_correct_page() { |
| 127 | - $screen = get_current_screen(); | |
| 128 | - $edit_hook = get_plugin_page_hookname( $this->slug, $this->base_slug ); | |
| 129 | - $edit_hook .= $screen->in_admin( 'network' ) ? '-network' : ''; | |
| 198 | + $screen = get_current_screen(); | |
| 199 | + $edit_hook = $this->get_hookname() . ( $screen->in_admin( 'network' ) ? '-network' : '' ); | |
| 130 | 200 | |
| 131 | 201 | // Disallow visiting the edit snippet page without a valid ID. |
| 132 | 202 | if ( |
| 133 | 203 | $screen->base === $edit_hook |