| @@ -4,8 +4,10 @@ | ||
| 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; | |
| 8 | 10 | use function Code_Snippets\activate_snippet; |
| 9 | 11 | use function Code_Snippets\code_snippets; |
| 10 | 12 | use function Code_Snippets\get_snippet; |
| 11 | 13 | use function Code_Snippets\Settings\get_setting; |
| @@ -47,8 +49,9 @@ | ||
| 47 | 49 | $this->screen_options = new Manage_Menu_Screen_Options(); |
| 48 | 50 | new Manage_Menu_Bulk_Download(); |
| 49 | 51 | |
| 50 | 52 | add_action( 'admin_menu', array( $this, 'register_upgrade_menu' ), 500 ); |
| 53 | + add_filter( 'heartbeat_received', [ $this, 'refresh_run_once_nonce' ] ); | |
| 51 | 54 | add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_menu_css' ) ); |
| 52 | 55 | add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_menu_css' ] ); |
| 53 | 56 | } |
| 54 | 57 | |
| @@ -160,8 +163,9 @@ | ||
| 160 | 163 | $classmap = [ |
| 161 | 164 | 'snippets' => 'manage', |
| 162 | 165 | 'add-snippet' => 'edit', |
| 163 | 166 | 'edit-snippet' => 'edit', |
| 167 | + 'code-snippets-insights' => 'insights', | |
| 164 | 168 | 'import-code-snippets' => 'import', |
| 165 | 169 | 'snippets-settings' => 'settings', |
| 166 | 170 | ]; |
| 167 | 171 | $menus = code_snippets()->admin->menus; |
| @@ -179,18 +183,75 @@ | ||
| 179 | 183 | add_action( 'load-' . $hook, [ $class, 'load' ] ); |
| 180 | 184 | } |
| 181 | 185 | |
| 182 | 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 | + /** | |
| 183 | 244 | * Nonce action guarding the run-once request. |
| 184 | 245 | */ |
| 185 | 246 | public const RUN_ONCE_NONCE = 'code_snippets_run_once'; |
| 186 | 247 | |
| 187 | 248 | /** |
| 188 | - * Run a single-use snippet, when asked to by the snippets list. | |
| 249 | + * Run a single-use snippet, when asked by the snippets list. | |
| 189 | 250 | * |
| 190 | 251 | * Activating the snippet is all that is required: single-use snippets are |
| 191 | 252 | * executed and then deactivated again on the next page load, so redirecting |
| 192 | - * afterwards both runs the code and returns the snippet to its resting | |
| 253 | + * afterward both runs the code and returns the snippet to its resting | |
| 193 | 254 | * state. This mirrors what the list table did before the snippets list |
| 194 | 255 | * moved to the REST API, at which point the button was left pointing at a |
| 195 | 256 | * URL that nothing handled. |
| 196 | 257 | * |
| @@ -231,9 +292,9 @@ | ||
| 231 | 292 | } |
| 232 | 293 | |
| 233 | 294 | // Safe mode skips execution, so activating here would leave the snippet on |
| 234 | 295 | // without ever running it, behind a false success notice. Report it instead. |
| 235 | - if ( \Code_Snippets\Integration\Evaluate_Functions::is_safe_mode_active() ) { | |
| 296 | + if ( Evaluate_Functions::is_safe_mode_active() ) { | |
| 236 | 297 | wp_safe_redirect( |
| 237 | 298 | add_query_arg( |
| 238 | 299 | [ 'result' => 'run-once-safe-mode' ], |
| 239 | 300 | remove_query_arg( [ 'action', 'snippet', 'network', '_wpnonce', 'result' ] ) |
| @@ -254,11 +315,31 @@ | ||
| 254 | 315 | exit; |
| 255 | 316 | } |
| 256 | 317 | |
| 257 | 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 | + /** | |
| 258 | 337 | * Executed when the admin page is loaded. |
| 259 | 338 | */ |
| 260 | 339 | public function load() { |
| 340 | + $this->maybe_reset_demos(); | |
| 341 | + | |
| 261 | 342 | parent::load(); |
| 262 | 343 | |
| 263 | 344 | $this->handle_run_once(); |
| 264 | 345 | $this->screen_options->load(); |
| @@ -263,12 +344,19 @@ | ||
| 263 | 344 | $this->handle_run_once(); |
| 264 | 345 | $this->screen_options->load(); |
| 265 | 346 | |
| 266 | 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 | + | |
| 267 | 355 | return; |
| 268 | 356 | } |
| 269 | 357 | |
| 270 | - $contextual_help = new Contextual_Help( 'edit' ); | |
| 358 | + $contextual_help = new Contextual_Help( 'manage' ); | |
| 271 | 359 | $contextual_help->load(); |
| 272 | 360 | } |
| 273 | 361 | |
| 274 | 362 | /** |