PluginProbe
Code Snippets / 4.0.0-beta.2
Code Snippets v4.0.0-beta.2
4.0.0-beta.2 3.10.2 3.10.1 3.10.0 3.10.0-beta.2 3.10.0-beta.1 4.0.0-beta.1 3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 All 65 releases
code-snippets / php / Admin / Menus / Manage / Manage_Menu.php

Manage_Menu.php in Code Snippets 4.0.0-beta.2, at php/Admin/Menus/Manage/Manage_Menu.php

410 lines 12.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Code_Snippets\Admin\Menus\Manage;
4
5 use Code_Snippets\Admin\Contextual_Help;
6 use Code_Snippets\Admin\Menus\Admin_Menu;
7 use Code_Snippets\Controller\Cloud_Search_Controller;
8 use Code_Snippets\Integration\Evaluate_Functions;
9 use Code_Snippets\REST_API\Preferences\Demos_Seen_REST_Controller;
10 use function Code_Snippets\activate_snippet;
11 use function Code_Snippets\code_snippets;
12 use function Code_Snippets\get_snippet;
13 use function Code_Snippets\Settings\get_setting;
14 use const Code_Snippets\PLUGIN_FILE;
15 use const Code_Snippets\PLUGIN_VERSION;
16
17 /**
18 * Provides the manage snippets admin menu.
19 */
20 class Manage_Menu extends Admin_Menu {
21
22 /**
23 * Default number of snippets shown per page in the manage table.
24 */
25 private const DEFAULT_SNIPPETS_PER_PAGE = 100;
26
27 /**
28 * Manage menu Screen Options.
29 *
30 * @var Manage_Menu_Screen_Options
31 */
32 private Manage_Menu_Screen_Options $screen_options;
33
34 /**
35 * Class constructor.
36 */
37 public function __construct() {
38 parent::__construct(
39 'manage',
40 _x( 'All Snippets', 'menu label', 'code-snippets' ),
41 __( 'Snippets', 'code-snippets' )
42 );
43
44 if ( code_snippets()->is_compact_menu() ) {
45 add_action( 'admin_menu', array( $this, 'register_compact_menu' ), 2 );
46 add_action( 'network_admin_menu', array( $this, 'register_compact_menu' ), 2 );
47 }
48
49 $this->screen_options = new Manage_Menu_Screen_Options();
50 new Manage_Menu_Bulk_Download();
51
52 add_action( 'admin_menu', array( $this, 'register_upgrade_menu' ), 500 );
53 add_filter( 'heartbeat_received', [ $this, 'refresh_run_once_nonce' ] );
54 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_menu_css' ) );
55 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_menu_css' ] );
56 }
57
58 /**
59 * Register the top-level 'Snippets' menu and associated 'Manage' subpage
60 */
61 public function register() {
62 add_menu_page(
63 __( 'Snippets', 'code-snippets' ),
64 _x( 'Snippets', 'top-level menu label', 'code-snippets' ),
65 code_snippets()->get_cap(),
66 code_snippets()->get_menu_slug(),
67 [ $this, 'render' ],
68 'none', // Added through CSS as a mask to prevent loading 'blinking'.
69 apply_filters( 'code_snippets/admin/menu_position', is_network_admin() ? 21 : 67 )
70 );
71
72 parent::register();
73 }
74
75 /**
76 * Render the snippets table interface.
77 *
78 * @return void
79 */
80 public function render() {
81 echo '<div id="manage-snippets-container" class="wrap"></div>';
82 }
83
84 /**
85 * Register the 'upgrade' menu item.
86 *
87 * @return void
88 */
89 public function register_upgrade_menu() {
90 if ( code_snippets()->licensing->is_licensed() || get_setting( 'general', 'hide_upgrade_menu' ) ) {
91 return;
92 }
93
94 $menu_title = sprintf(
95 '<span class="button button-primary code-snippets-upgrade-button">%s %s</span>',
96 _x( 'Go Pro', 'top-level menu label', 'code-snippets' ),
97 '<span class="dashicons dashicons-external" aria-hidden="true"></span>'
98 );
99
100 $hook = add_submenu_page(
101 code_snippets()->get_menu_slug(),
102 __( 'Upgrade to Pro', 'code-snippets' ),
103 $menu_title,
104 code_snippets()->get_cap(),
105 'code_snippets_upgrade',
106 '__return_empty_string',
107 100
108 );
109
110 add_action( "load-$hook", [ $this, 'load_upgrade_menu' ] );
111 }
112
113 /**
114 * Print CSS required for the admin menu icon.
115 *
116 * @return void
117 */
118 public function enqueue_menu_css() {
119 wp_enqueue_style(
120 'code-snippets-menu',
121 plugins_url( 'dist/menu.css', PLUGIN_FILE ),
122 [],
123 PLUGIN_VERSION
124 );
125 }
126
127 /**
128 * Redirect the user upon opening the upgrade menu.
129 *
130 * @return void
131 */
132 public function load_upgrade_menu() {
133 wp_safe_redirect( 'https://snipco.de/JE2f' );
134 exit;
135 }
136
137 /**
138 * Retrieve every hookname registered by this menu, including the compact
139 * Tools submenu hookname when compact mode is active.
140 *
141 * @return string[]
142 */
143 public function get_hooknames(): array {
144 $hooknames = parent::get_hooknames();
145
146 if ( code_snippets()->is_compact_menu() ) {
147 $hooknames[] = get_plugin_page_hookname( $this->base_slug, 'tools.php' );
148 }
149
150 return $hooknames;
151 }
152
153 /**
154 * Add menu pages for the compact menu
155 */
156 public function register_compact_menu() {
157 if ( ! code_snippets()->is_compact_menu() ) {
158 return;
159 }
160
161 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Value is matched to known classes.
162 $sub = code_snippets()->get_menu_slug( isset( $_GET['sub'] ) ? sanitize_key( $_GET['sub'] ) : 'snippets' );
163 $classmap = [
164 'snippets' => 'manage',
165 'add-snippet' => 'edit',
166 'edit-snippet' => 'edit',
167 'code-snippets-insights' => 'insights',
168 'import-code-snippets' => 'import',
169 'snippets-settings' => 'settings',
170 ];
171 $menus = code_snippets()->admin->menus;
172 $class = isset( $classmap[ $sub ], $menus[ $classmap[ $sub ] ] ) ? $menus[ $classmap[ $sub ] ] : $this;
173
174 $hook = add_submenu_page(
175 'tools.php',
176 __( 'Snippets', 'code-snippets' ),
177 _x( 'Snippets', 'tools submenu label', 'code-snippets' ),
178 code_snippets()->get_cap(),
179 code_snippets()->get_menu_slug(),
180 [ $class, 'render' ]
181 );
182
183 add_action( 'load-' . $hook, [ $class, 'load' ] );
184 }
185
186 /**
187 * Query parameter that clears the record of which demos have been watched.
188 */
189 public const DEMO_RESET_PARAM = 'demo-reset';
190
191 /**
192 * Nonce action guarding the watched-demo reset.
193 */
194 public const DEMO_RESET_NONCE = 'code_snippets_demo_reset';
195
196 /**
197 * Build the address that puts the walkthrough tabs back to their "New" state.
198 *
199 * @return string
200 */
201 public static function get_demo_reset_url(): string {
202 // Built raw rather than with wp_nonce_url(), which escapes the separator
203 // for markup: this address is meant to be pasted into the address bar.
204 return add_query_arg(
205 [
206 self::DEMO_RESET_PARAM => '1',
207 '_wpnonce' => wp_create_nonce( self::DEMO_RESET_NONCE ),
208 ],
209 code_snippets()->get_menu_url()
210 );
211 }
212
213 /**
214 * Clear the watched-demo record when asked through the query string.
215 *
216 * This is deliberately not exposed in the settings screen: it exists to put
217 * the walkthrough tabs back to their "New" state for a screenshot or a
218 * walkthrough of the walkthroughs. Build the address with
219 * {@see self::get_demo_reset_url()}, which signs it.
220 *
221 * @return void
222 */
223 private function maybe_reset_demos(): void {
224 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified immediately below.
225 if ( ! isset( $_GET[ self::DEMO_RESET_PARAM ] ) ) {
226 return;
227 }
228
229 $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
230
231 if ( ! wp_verify_nonce( $nonce, self::DEMO_RESET_NONCE ) || ! code_snippets()->current_user_can() ) {
232 return;
233 }
234
235 Demos_Seen_REST_Controller::reset_demos_seen();
236
237 // Redirect so a refresh does not repeat the reset, and the address bar
238 // is left clean.
239 wp_safe_redirect( remove_query_arg( [ self::DEMO_RESET_PARAM, '_wpnonce' ] ) );
240 exit;
241 }
242
243 /**
244 * Nonce action guarding the run-once request.
245 */
246 public const RUN_ONCE_NONCE = 'code_snippets_run_once';
247
248 /**
249 * Run a single-use snippet, when asked by the snippets list.
250 *
251 * Activating the snippet is all that is required: single-use snippets are
252 * executed and then deactivated again on the next page load, so redirecting
253 * afterward both runs the code and returns the snippet to its resting
254 * state. This mirrors what the list table did before the snippets list
255 * moved to the REST API, at which point the button was left pointing at a
256 * URL that nothing handled.
257 *
258 * @return void
259 */
260 private function handle_run_once(): void {
261 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified immediately below.
262 $action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : '';
263
264 if ( 'run-once' !== $action ) {
265 return;
266 }
267
268 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified immediately below.
269 $nonce = isset( $_REQUEST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ) : '';
270
271 if ( ! wp_verify_nonce( $nonce, self::RUN_ONCE_NONCE ) || ! code_snippets()->current_user_can() ) {
272 return;
273 }
274
275 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified above.
276 $snippet_id = isset( $_REQUEST['snippet'] ) ? absint( wp_unslash( $_REQUEST['snippet'] ) ) : 0;
277
278 if ( ! $snippet_id ) {
279 return;
280 }
281
282 // The network context comes from the current screen, never the request,
283 // so a subsite administrator cannot target a network snippet.
284 $network = is_network_admin();
285 $snippet = get_snippet( $snippet_id, $network );
286
287 // Only single-use snippets are run this way. Activating anything else
288 // would leave it permanently on while the notice claimed it ran once.
289 if ( ! $snippet || 0 === $snippet->id || 'single-use' !== $snippet->scope ) {
290 wp_safe_redirect( remove_query_arg( [ 'action', 'snippet', 'network', '_wpnonce', 'result' ] ) );
291 exit;
292 }
293
294 // Safe mode skips execution, so activating here would leave the snippet on
295 // without ever running it, behind a false success notice. Report it instead.
296 if ( Evaluate_Functions::is_safe_mode_active() ) {
297 wp_safe_redirect(
298 add_query_arg(
299 [ 'result' => 'run-once-safe-mode' ],
300 remove_query_arg( [ 'action', 'snippet', 'network', '_wpnonce', 'result' ] )
301 )
302 );
303 exit;
304 }
305
306 // An already-active snippet has effectively run, so treat it as success.
307 $result = $snippet->active ? $snippet : activate_snippet( $snippet_id, $network );
308
309 wp_safe_redirect(
310 add_query_arg(
311 [ 'result' => is_string( $result ) ? 'run-once-failed' : 'executed' ],
312 remove_query_arg( [ 'action', 'snippet', 'network', '_wpnonce', 'result' ] )
313 )
314 );
315 exit;
316 }
317
318 /**
319 * Send a fresh Run Once nonce with each Heartbeat, so a page left open past
320 * the nonce lifetime can still run a snippet.
321 *
322 * @param mixed $response Heartbeat response.
323 *
324 * @return array<string, mixed>
325 */
326 public function refresh_run_once_nonce( $response ): array {
327 $response = is_array( $response ) ? $response : [];
328
329 if ( code_snippets()->current_user_can() ) {
330 $response['code_snippets_run_once_nonce'] = wp_create_nonce( self::RUN_ONCE_NONCE );
331 }
332
333 return $response;
334 }
335
336 /**
337 * Executed when the admin page is loaded.
338 */
339 public function load() {
340 $this->maybe_reset_demos();
341
342 parent::load();
343
344 $this->handle_run_once();
345 $this->screen_options->load();
346
347 if ( $this->screen_options->is_upsell_view() ) {
348 $subpage = $this->screen_options->get_current_subpage();
349
350 if ( in_array( $subpage, [ 'cloud-library', 'blueprints', 'ai-agent' ], true ) ) {
351 $contextual_help = new Contextual_Help( $subpage );
352 $contextual_help->load();
353 }
354
355 return;
356 }
357
358 $contextual_help = new Contextual_Help( 'manage' );
359 $contextual_help->load();
360 }
361
362 /**
363 * Enqueue scripts and stylesheets for the admin page.
364 */
365 public function enqueue_assets() {
366 $assets = new Manage_Menu_Assets( $this->screen_options );
367 $assets->enqueue( self::$script_deps, self::$style_deps );
368 }
369
370 /**
371 * Get the number of snippets to show per page in the cloud search.
372 *
373 * The value defaults to the user's local snippets-per-page preference but can
374 * be overridden independently via the `code_snippets/cloud_search/per_page` filter.
375 * The result is clamped to the REST API's per_page maximum of 100.
376 *
377 * @return int
378 */
379 public static function get_cloud_search_per_page(): int {
380 $per_page = intval( apply_filters( 'code_snippets/cloud_search/per_page', self::get_snippets_per_page() ) );
381 return min( Cloud_Search_Controller::MAX_RESULTS_PER_PAGE, max( 1, $per_page ) );
382 }
383
384 /**
385 * Get the default number of snippets to show per page.
386 *
387 * @return int
388 */
389 public static function get_default_snippets_per_page(): int {
390 $default = apply_filters( 'code_snippets/snippets_per_page_default', self::DEFAULT_SNIPPETS_PER_PAGE );
391 return max( 1, intval( $default ) );
392 }
393
394 /**
395 * Get the number of snippets to show per page.
396 *
397 * @return int
398 */
399 public static function get_snippets_per_page(): int {
400 $per_page = intval( get_user_option( 'snippets_per_page' ) );
401
402 return intval(
403 apply_filters(
404 'snippets_per_page',
405 $per_page > 0 ? $per_page : self::get_default_snippets_per_page()
406 )
407 );
408 }
409 }
410