| @@ -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; |
| @@ -21,14 +20,14 @@ | ||
| 21 | 20 | |
| 22 | 21 | /** |
| 23 | 22 | * Handle for JavaScript asset file. |
| 24 | 23 | */ |
| 25 | - public const JS_HANDLE = 'code-snippets-edit-menu'; | |
| 24 | + private const JS_HANDLE = 'code-snippets-edit-menu'; | |
| 26 | 25 | |
| 27 | 26 | /** |
| 28 | 27 | * Handle for CSS asset file. |
| 29 | 28 | */ |
| 30 | - public const CSS_HANDLE = 'code-snippets-edit'; | |
| 29 | + private const CSS_HANDLE = 'code-snippets-edit'; | |
| 31 | 30 | |
| 32 | 31 | /** |
| 33 | 32 | * The snippet object currently being edited |
| 34 | 33 | * |
| @@ -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,28 +78,104 @@ | ||
| 70 | 78 | ); |
| 71 | 79 | } |
| 72 | 80 | |
| 73 | 81 | /** |
| 74 | - * Hide the static Edit Snippet menu item unless a specific snippet is being edited. | |
| 82 | + * The snippet this request is editing, if any. | |
| 75 | 83 | * |
| 76 | - * @param WP_Screen $screen Current admin screen. | |
| 84 | + * Read from the request rather than the current screen, because this runs | |
| 85 | + * during `admin_menu`, before the screen is known. | |
| 77 | 86 | * |
| 87 | + * @return int Snippet ID, or 0 when not editing a specific snippet. | |
| 88 | + */ | |
| 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; | |
| 99 | + } | |
| 100 | + | |
| 101 | + /** | |
| 102 | + * Register the edit page without placing it in the admin menu. | |
| 103 | + * | |
| 78 | 104 | * @return void |
| 79 | 105 | */ |
| 80 | - public function maybe_hide_menu_item( WP_Screen $screen ) { | |
| 81 | - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only admin menu context. | |
| 82 | - $current_id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; | |
| 83 | - $edit_hook = get_plugin_page_hookname( $this->slug, $this->base_slug ); | |
| 84 | - $edit_hook .= $screen->in_admin( 'network' ) ? '-network' : ''; | |
| 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 | + ); | |
| 85 | 115 | |
| 86 | - if ( ( $screen->id === $edit_hook || $screen->base === $edit_hook ) && 0 < $current_id ) { | |
| 87 | - return; | |
| 116 | + if ( $hook ) { | |
| 117 | + add_action( 'load-' . $hook, array( $this, 'load' ) ); | |
| 88 | 118 | } |
| 119 | + } | |
| 89 | 120 | |
| 90 | - remove_submenu_page( $this->base_slug, $this->slug ); | |
| 121 | + /** | |
| 122 | + * Mark the snippet being edited in the menu. | |
| 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 | + * | |
| 131 | + * @return void | |
| 132 | + */ | |
| 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 | + ); | |
| 91 | 149 | } |
| 92 | 150 | |
| 93 | 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 | + } | |
| 163 | + | |
| 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 | + ]; | |
| 175 | + } | |
| 176 | + | |
| 177 | + /** | |
| 94 | 178 | * Executed when the menu is loaded. |
| 95 | 179 | * |
| 96 | 180 | * @return void |
| 97 | 181 | */ |
| @@ -110,11 +194,10 @@ | ||
| 110 | 194 | * |
| 111 | 195 | * @return void |
| 112 | 196 | */ |
| 113 | 197 | protected function ensure_correct_page() { |
| 114 | - $screen = get_current_screen(); | |
| 115 | - $edit_hook = get_plugin_page_hookname( $this->slug, $this->base_slug ); | |
| 116 | - $edit_hook .= $screen->in_admin( 'network' ) ? '-network' : ''; | |
| 198 | + $screen = get_current_screen(); | |
| 199 | + $edit_hook = $this->get_hookname() . ( $screen->in_admin( 'network' ) ? '-network' : '' ); | |
| 117 | 200 | |
| 118 | 201 | // Disallow visiting the edit snippet page without a valid ID. |
| 119 | 202 | if ( |
| 120 | 203 | $screen->base === $edit_hook |
| @@ -191,9 +274,9 @@ | ||
| 191 | 274 | |
| 192 | 275 | wp_enqueue_script( |
| 193 | 276 | self::JS_HANDLE, |
| 194 | 277 | plugins_url( 'dist/edit.js', PLUGIN_FILE ), |
| 195 | - [ 'code-snippets-code-editor' ] + self::$script_deps, | |
| 278 | + array_merge( [ 'code-snippets-code-editor' ], self::$script_deps ), | |
| 196 | 279 | PLUGIN_VERSION, |
| 197 | 280 | [ 'in_footer' => true ] |
| 198 | 281 | ); |
| 199 | 282 | |