| @@ -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 | |
| @@ -20,15 +24,8 @@ | ||
| 20 | 24 | */ |
| 21 | 25 | private const DEFAULT_SNIPPETS_PER_PAGE = 100; |
| 22 | 26 | |
| 23 | 27 | /** |
| 24 | - * Manage menu asset service. | |
| 25 | - * | |
| 26 | - * @var Manage_Menu_Assets | |
| 27 | - */ | |
| 28 | - private Manage_Menu_Assets $assets; | |
| 29 | - | |
| 30 | - /** | |
| 31 | 28 | * Manage menu Screen Options. |
| 32 | 29 | * |
| 33 | 30 | * @var Manage_Menu_Screen_Options |
| 34 | 31 | */ |
| @@ -52,10 +49,10 @@ | ||
| 52 | 49 | $this->screen_options = new Manage_Menu_Screen_Options(); |
| 53 | 50 | new Manage_Menu_Bulk_Download(); |
| 54 | 51 | |
| 55 | 52 | add_action( 'admin_menu', array( $this, 'register_upgrade_menu' ), 500 ); |
| 53 | + add_filter( 'heartbeat_received', [ $this, 'refresh_run_once_nonce' ] ); | |
| 56 | 54 | add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_menu_css' ) ); |
| 57 | - add_action( 'wp_ajax_update_code_snippet', array( $this, 'ajax_callback' ) ); | |
| 58 | 55 | add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_menu_css' ] ); |
| 59 | 56 | } |
| 60 | 57 | |
| 61 | 58 | /** |
| @@ -166,8 +163,9 @@ | ||
| 166 | 163 | $classmap = [ |
| 167 | 164 | 'snippets' => 'manage', |
| 168 | 165 | 'add-snippet' => 'edit', |
| 169 | 166 | 'edit-snippet' => 'edit', |
| 167 | + 'code-snippets-insights' => 'insights', | |
| 170 | 168 | 'import-code-snippets' => 'import', |
| 171 | 169 | 'snippets-settings' => 'settings', |
| 172 | 170 | ]; |
| 173 | 171 | $menus = code_snippets()->admin->menus; |
| @@ -185,16 +183,180 @@ | ||
| 185 | 183 | add_action( 'load-' . $hook, [ $class, 'load' ] ); |
| 186 | 184 | } |
| 187 | 185 | |
| 188 | 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 | + /** | |
| 189 | 337 | * Executed when the admin page is loaded. |
| 190 | 338 | */ |
| 191 | 339 | public function load() { |
| 340 | + $this->maybe_reset_demos(); | |
| 341 | + | |
| 192 | 342 | parent::load(); |
| 193 | 343 | |
| 344 | + $this->handle_run_once(); | |
| 194 | 345 | $this->screen_options->load(); |
| 195 | 346 | |
| 196 | - $contextual_help = new Contextual_Help( 'edit' ); | |
| 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' ); | |
| 197 | 359 | $contextual_help->load(); |
| 198 | 360 | } |
| 199 | 361 | |
| 200 | 362 | /** |