PluginProbe
Code Snippets / trunk
Code Snippets vtrunk
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
← All changes | php/Admin/Menus/Manage/Manage_Menu.php +167 -1 3.10.1 → trunk View file →
@@ -4,9 +4,13 @@
4 4
5 5 use Code_Snippets\Admin\Contextual_Help;
6 6 use Code_Snippets\Admin\Menus\Admin_Menu;
7 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;
8 11 use function Code_Snippets\code_snippets;
12 +use function Code_Snippets\get_snippet;
9 13 use function Code_Snippets\Settings\get_setting;
10 14 use const Code_Snippets\PLUGIN_FILE;
11 15 use const Code_Snippets\PLUGIN_VERSION;
12 16
@@ -45,8 +49,9 @@
45 49 $this->screen_options = new Manage_Menu_Screen_Options();
46 50 new Manage_Menu_Bulk_Download();
47 51
48 52 add_action( 'admin_menu', array( $this, 'register_upgrade_menu' ), 500 );
53 + add_filter( 'heartbeat_received', [ $this, 'refresh_run_once_nonce' ] );
49 54 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_menu_css' ) );
50 55 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_menu_css' ] );
51 56 }
52 57
@@ -158,8 +163,9 @@
158 163 $classmap = [
159 164 'snippets' => 'manage',
160 165 'add-snippet' => 'edit',
161 166 'edit-snippet' => 'edit',
167 + 'code-snippets-insights' => 'insights',
162 168 'import-code-snippets' => 'import',
163 169 'snippets-settings' => 'settings',
164 170 ];
165 171 $menus = code_snippets()->admin->menus;
@@ -177,20 +183,180 @@
177 183 add_action( 'load-' . $hook, [ $class, 'load' ] );
178 184 }
179 185
180 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 + /**
181 337 * Executed when the admin page is loaded.
182 338 */
183 339 public function load() {
340 + $this->maybe_reset_demos();
341 +
184 342 parent::load();
185 343
344 + $this->handle_run_once();
186 345 $this->screen_options->load();
187 346
188 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 +
189 355 return;
190 356 }
191 357
192 - $contextual_help = new Contextual_Help( 'edit' );
358 + $contextual_help = new Contextual_Help( 'manage' );
193 359 $contextual_help->load();
194 360 }
195 361
196 362 /**