PluginProbe
Stream – Activity Log & Audit Trail / 3.4.2
Stream – Activity Log & Audit Trail v3.4.2
4.4.0 4.3.0 4.2.2 4.2.1 trunk 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.1 3.1.1 3.10.0 3.2.0 3.2.1 3.2.2 3.2.3 All 50 releases
← All changes | classes/class-admin.php +250 -1130 trunk3.4.2 View file →
@@ -1,10 +1,5 @@
1 1 <?php
2 -/**
3 - * Centralized manager for WordPress backend functionality.
4 - *
5 - * @package WP_Stream
6 - */
7 2
8 3 namespace WP_Stream;
9 4
10 5 use DateTime;
@@ -9,77 +4,16 @@
9 4
10 5 use DateTime;
11 6 use DateTimeZone;
12 7 use DateInterval;
13 -use WP_CLI;
14 -use WP_Roles;
8 +use \WP_CLI;
9 +use \WP_Roles;
15 10
16 -/**
17 - * Class - Admin
18 - */
19 11 class Admin {
20 12
21 13 /**
22 - * The async deletion action for large sites.
14 + * Hold Plugin class
23 15 *
24 - * @const string
25 - */
26 - const ASYNC_DELETION_ACTION = 'stream_erase_large_records_action';
27 -
28 - /**
29 - * Recurring Action Scheduler action that drives the TTL-based auto-purge.
30 - *
31 - * @const string
32 - */
33 - const AUTO_PURGE_ACTION = 'stream_auto_purge_action';
34 -
35 - /**
36 - * Async batch worker scheduled by the recurring auto-purge action.
37 - *
38 - * @const string
39 - */
40 - const AUTO_PURGE_BATCH_ACTION = 'stream_auto_purge_batch_action';
41 -
42 - /**
43 - * Terminal action that runs the orphan-meta reaper once per chain.
44 - *
45 - * @const string
46 - */
47 - const AUTO_PURGE_REAPER_ACTION = 'stream_auto_purge_reaper_action';
48 -
49 - /**
50 - * Action Scheduler group string for all auto-purge actions.
51 - *
52 - * @const string
53 - */
54 - const AUTO_PURGE_GROUP = 'stream-auto-purge';
55 -
56 - /**
57 - * Option storing which scheduler backend last registered the recurring
58 - * auto-purge action ('action_scheduler' | 'wp_cron'), or 'disabled' when
59 - * the `wp_stream_enable_auto_purge` filter has torn scheduling down. Used
60 - * to detect a backend switch (or a disable/re-enable cycle) so the stale
61 - * recurring action is cleared exactly once, instead of probing for it on
62 - * every page load.
63 - *
64 - * @const string
65 - */
66 - const SCHEDULER_BACKEND_OPTION = 'wp_stream_scheduler_backend';
67 -
68 - /**
69 - * Option persisting the "large batched operation queued to WP-Cron"
70 - * warning between requests. The contexts that queue the warning (the
71 - * recurring purge under DOING_CRON, the reset handler just before its
72 - * redirect) never render their own output, so the message is stored here
73 - * and displayed on the next admin page load instead. Deleted on render.
74 - *
75 - * @const string
76 - */
77 - const LARGE_TABLE_CRON_NOTICE_OPTION = 'wp_stream_large_table_cron_notice';
78 -
79 - /**
80 - * Holds Instance of plugin object
81 - *
82 16 * @var Plugin
83 17 */
84 18 public $plugin;
85 19
@@ -160,13 +94,13 @@
160 94 */
161 95 public $view_cap = 'view_stream';
162 96
163 97 /**
164 - * Capability name for managing settings
98 + * Capability name for viewing settings
165 99 *
166 100 * @var string
167 101 */
168 - public $settings_cap = WP_STREAM_SETTINGS_CAPABILITY;
102 + public $settings_cap = 'manage_options';
169 103
170 104 /**
171 105 * Total amount of authors to pre-load
172 106 *
@@ -183,9 +117,9 @@
183 117
184 118 /**
185 119 * Class constructor.
186 120 *
187 - * @param Plugin $plugin Instance of plugin object.
121 + * @param Plugin $plugin The main Plugin class.
188 122 */
189 123 public function __construct( $plugin ) {
190 124 $this->plugin = $plugin;
191 125
@@ -199,9 +133,9 @@
199 133 // User and role caps.
200 134 add_filter( 'user_has_cap', array( $this, 'filter_user_caps' ), 10, 4 );
201 135 add_filter( 'role_has_cap', array( $this, 'filter_role_caps' ), 10, 3 );
202 136
203 - if ( $this->plugin->is_multisite_network_activated() && ! is_network_admin() ) {
137 + if ( is_multisite() && $plugin->is_network_activated() && ! is_network_admin() ) {
204 138 $options = (array) get_site_option( 'wp_stream_network', array() );
205 139 $option = isset( $options['general_site_access'] ) ? absint( $options['general_site_access'] ) : 1;
206 140
207 141 $this->disable_access = ( $option ) ? false : true;
@@ -215,11 +149,8 @@
215 149 // Admin notices.
216 150 add_action( 'admin_notices', array( $this, 'prepare_admin_notices' ) );
217 151 add_action( 'shutdown', array( $this, 'admin_notices' ) );
218 152
219 - // Feature request notice.
220 - add_action( 'admin_notices', array( $this, 'display_feature_request_notice' ) );
221 -
222 153 // Add admin body class.
223 154 add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
224 155
225 156 // Plugin action links.
@@ -251,41 +182,20 @@
251 182 'wp_ajax_reset',
252 183 )
253 184 );
254 185
255 - // Manual "Clean orphaned meta now" action (Settings → Advanced).
256 - add_action(
257 - 'wp_ajax_wp_stream_clean_orphan_meta',
258 - array( $this, 'wp_ajax_clean_orphan_meta' )
259 - );
186 + // Uninstall Streams and Deactivate plugin.
187 + $uninstall = $this->plugin->db->driver->purge_storage( $this->plugin );
260 188
261 - // Render confirmation notices keyed by the wp_stream_message query
262 - // arg set on post-action redirects (e.g. orphan_meta_cleanup_scheduled).
263 - add_action( 'admin_notices', array( $this, 'maybe_display_message' ) );
264 - add_action( 'network_admin_notices', array( $this, 'maybe_display_message' ) );
265 -
266 - // Render the persisted "large batched operation queued to WP-Cron"
267 - // warning on the next admin page load (see
268 - // maybe_warn_large_table_without_action_scheduler()).
269 - add_action( 'admin_notices', array( $this, 'display_large_table_cron_notice' ) );
270 - add_action( 'network_admin_notices', array( $this, 'display_large_table_cron_notice' ) );
271 -
272 - // Auto purge setup (Action Scheduler).
189 + // Auto purge setup.
273 190 add_action( 'wp_loaded', array( $this, 'purge_schedule_setup' ) );
274 191 add_action(
275 - self::AUTO_PURGE_ACTION,
276 - array( $this, 'purge_scheduled_action' )
192 + 'wp_stream_auto_purge',
193 + array(
194 + $this,
195 + 'purge_scheduled_action',
196 + )
277 197 );
278 - add_action(
279 - self::AUTO_PURGE_BATCH_ACTION,
280 - array( $this, 'auto_purge_batch' ),
281 - 10,
282 - 3
283 - );
284 - add_action(
285 - self::AUTO_PURGE_REAPER_ACTION,
286 - array( $this, 'auto_purge_reaper' )
287 - );
288 198
289 199 // Ajax users list.
290 200 add_action(
291 201 'wp_ajax_wp_stream_filters',
@@ -293,19 +203,8 @@
293 203 $this,
294 204 'ajax_filters',
295 205 )
296 206 );
297 -
298 - // Async action for erasing large log tables.
299 - add_action(
300 - self::ASYNC_DELETION_ACTION,
301 - array(
302 - $this,
303 - 'erase_large_records',
304 - ),
305 - 10,
306 - 4
307 - );
308 207 }
309 208
310 209 /**
311 210 * Load admin classes
@@ -315,14 +214,8 @@
315 214 public function init() {
316 215 $this->network = new Network( $this->plugin );
317 216 $this->live_update = new Live_Update( $this->plugin );
318 217 $this->export = new Export( $this->plugin );
319 -
320 - // Check if the host has configured the `REMOTE_ADDR` correctly.
321 - $client_ip = $this->plugin->get_client_ip_address();
322 - if ( empty( $client_ip ) && $this->is_stream_screen() ) {
323 - $this->notice( __( 'Stream plugin can\'t determine a reliable client IP address! Please update the hosting environment to set the $_SERVER[\'REMOTE_ADDR\'] variable or use the wp_stream_client_ip_address filter to specify the verified client IP address!', 'stream' ) );
324 - }
325 218 }
326 219
327 220 /**
328 221 * Output specific updates passed as URL parameters.
@@ -348,9 +241,9 @@
348 241 * @param bool $is_error If the message is error_level (true) or warning (false).
349 242 */
350 243 public function notice( $message, $is_error = true ) {
351 244 if ( defined( 'WP_CLI' ) && WP_CLI ) {
352 - $message = wp_strip_all_tags( $message );
245 + $message = strip_tags( $message );
353 246
354 247 if ( $is_error ) {
355 248 WP_CLI::warning( $message );
356 249 } else {
@@ -398,28 +291,8 @@
398 291 }
399 292 }
400 293
401 294 /**
402 - * Display a feature request notice.
403 - *
404 - * @return void
405 - */
406 - public function display_feature_request_notice() {
407 - $screen = get_current_screen();
408 -
409 - // Display the notice only on the Stream settings page.
410 - if ( empty( $this->screen_id['settings'] ) || $this->screen_id['settings'] !== $screen->id ) {
411 - return;
412 - }
413 -
414 - printf(
415 - '<div class="notice notice-info notice-stream-feature-request"><p>%1$s <a href="https://github.com/xwp/stream/issues/new/choose" target="_blank">%2$s <span class="dashicons dashicons-external"></span></a></p></div>',
416 - esc_html__( 'Have suggestions or found a bug?', 'stream' ),
417 - esc_html__( 'Click here to let us know!', 'stream' )
418 - );
419 - }
420 -
421 - /**
422 295 * Register menu page
423 296 *
424 297 * @action admin_menu
425 298 *
@@ -454,9 +327,9 @@
454 327 $main_menu_title,
455 328 $this->view_cap,
456 329 $this->records_page_slug,
457 330 array( $this, 'render_list_table' ),
458 - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIGZpbGw9IjAwMCI+Cgk8cGF0aCBkPSJNOTAzLjExNSA1MTUuNDEzYy00OS4zOTIgMC05MS40NzQgMzEuMzM3LTEwNy40NiA3NS4yMDNsLTEyNC40MTEtMS41MzJjLTExLjM3Ny0uMzQ2LTIyLjc1MS0uNjg5LTM0LjEyOS0uOTk4bC0uMjQxLjU3NC0yMi40MzYtLjI3OC0uMTUzLS45Mi0xNS4wNTYtODIuOTMzLTIwLjE0Ni0xMDguNDA1LTIwLjU0NC0xMDguMzM3TDUwMy45ODIgMGwtNTMuMTQxIDQyOS4wMTMtMTYuMjE0IDEzNy45MzUtMTIuMDE2IDEwNi45MjQtMTE3LjI4Ni0yODUuMjItMTguMzUzIDIwMi44MWMtNDIuNTYyIDEuNDU0LTg1LjEyNyAyLjkzNC0xMjcuNjg4IDQuNzM4LTUzLjA5NyAyLjI5Mi0xMDYuMTg3IDQuNDczLTE1OS4yODQgNy41MzZ2NDIuMDQyYzUzLjA5NyAzLjA2IDEwNi4xODcgNS4yNDcgMTU5LjI4NCA3LjUzMyA1My4wOTMgMi4yNDUgMTA2LjE4IDQuMTk0IDE1OS4yNzMgNS45MDNsMTQuMjQuNDY1IDE3LjM1MSA0OC4zOWMxOC44NDIgNTEuODc0IDM3LjU0MiAxMDMuODA2IDU2Ljc2NSAxNTUuNTQxTDQ2Ni41MiAxMDI0bDQxLjUxMi0zMDguMjkzIDE3LjYzMy0xMzYuNjg1IDEwLjc3NiA1MC4zMjkgNTQuODE1IDI0OC41NDQgNzIuNTE2LTIxNy4yMTdoMTI5LjI2MWMxMy40OTMgNDguMTIxIDU3LjY1NSA4My40MjkgMTEwLjA3NSA4My40MjkgNjMuMTYgMCAxMTQuMzUyLTUxLjIwNSAxMTQuMzUyLTExNC4zNDggMC02My4xMzktNTEuMTg5LTExNC4zNDUtMTE0LjM0OS0xMTQuMzQ1bC4wMDQtLjAwMVoiIC8+Cjwvc3ZnPgo=',
331 + 'div',
459 332 $main_menu_position
460 333 );
461 334
462 335 /**
@@ -506,55 +379,93 @@
506 379 * Enqueue scripts/styles for admin screen
507 380 *
508 381 * @action admin_enqueue_scripts
509 382 *
510 - * @param string $hook Current hook.
383 + * @param string $hook
511 384 *
512 385 * @return void
513 386 */
514 387 public function admin_enqueue_scripts( $hook ) {
515 - if ( in_array( $hook, $this->screen_id, true ) ) {
516 - $this->plugin->enqueue_asset(
517 - 'admin',
388 + wp_register_script( 'wp-stream-select2', $this->plugin->locations['url'] . 'ui/lib/select2/js/select2.full.min.js', array( 'jquery' ), '3.5.2', true );
389 + wp_register_style( 'wp-stream-select2', $this->plugin->locations['url'] . 'ui/lib/select2/css/select2.min.css', array(), '3.5.2' );
390 + wp_register_script( 'wp-stream-timeago', $this->plugin->locations['url'] . 'ui/lib/timeago/jquery.timeago.js', array(), '1.4.1', true );
391 +
392 + $locale = strtolower( substr( get_locale(), 0, 2 ) );
393 + $file_tmpl = 'ui/lib/timeago/locales/jquery.timeago.%s.js';
394 +
395 + if ( file_exists( $this->plugin->locations['dir'] . sprintf( $file_tmpl, $locale ) ) ) {
396 + wp_register_script( 'wp-stream-timeago-locale', $this->plugin->locations['url'] . sprintf( $file_tmpl, $locale ), array( 'wp-stream-timeago' ), '1' );
397 + } else {
398 + wp_register_script( 'wp-stream-timeago-locale', $this->plugin->locations['url'] . sprintf( $file_tmpl, 'en' ), array( 'wp-stream-timeago' ), '1' );
399 + }
400 +
401 + $min = wp_stream_min_suffix();
402 + wp_enqueue_style( 'wp-stream-admin', $this->plugin->locations['url'] . 'ui/css/admin.' . $min . 'css', array(), $this->plugin->get_version() );
403 +
404 + $script_screens = array( 'plugins.php' );
405 +
406 + if ( in_array( $hook, $this->screen_id, true ) || in_array( $hook, $script_screens, true ) ) {
407 + wp_enqueue_script( 'wp-stream-select2' );
408 + wp_enqueue_style( 'wp-stream-select2' );
409 +
410 + wp_enqueue_script( 'wp-stream-timeago' );
411 + wp_enqueue_script( 'wp-stream-timeago-locale' );
412 +
413 + wp_enqueue_script(
414 + 'wp-stream-admin',
415 + $this->plugin->locations['url'] . 'ui/js/admin.' . $min . 'js',
518 416 array(
519 - $this->plugin->with_select2(),
520 - $this->plugin->with_jquery_timeago(),
417 + 'jquery',
418 + 'wp-stream-select2',
521 419 ),
420 + $this->plugin->get_version()
421 + );
422 + wp_enqueue_script(
423 + 'wp-stream-admin-exclude',
424 + $this->plugin->locations['url'] . 'ui/js/exclude.' . $min . 'js',
522 425 array(
523 - 'i18n' => array(
524 - 'confirm_purge' => __( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
525 - 'confirm_defaults' => __( 'Are you sure you want to reset all site settings to default? This cannot be undone.', 'stream' ),
526 - ),
527 - 'locale' => strtolower( substr( get_locale(), 0, 2 ) ),
528 - 'gmt_offset' => get_option( 'gmt_offset' ),
529 - )
426 + 'jquery',
427 + 'wp-stream-select2',
428 + ),
429 + $this->plugin->get_version()
530 430 );
531 -
532 - $this->plugin->enqueue_asset(
533 - 'admin-exclude',
431 + wp_enqueue_script(
432 + 'wp-stream-live-updates',
433 + $this->plugin->locations['url'] . 'ui/js/live-updates.' . $min . 'js',
534 434 array(
535 - $this->plugin->with_select2(),
435 + 'jquery',
436 + 'heartbeat',
536 437 ),
438 + $this->plugin->get_version()
439 + );
440 +
441 + wp_localize_script(
442 + 'wp-stream-admin',
443 + 'wp_stream',
537 444 array(
538 - 'getActionsNonce' => wp_create_nonce( 'stream_get_actions' ),
445 + 'i18n' => array(
446 + 'confirm_purge' => esc_html__( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
447 + 'confirm_defaults' => esc_html__( 'Are you sure you want to reset all site settings to default? This cannot be undone.', 'stream' ),
448 + 'confirm_uninstall' => esc_html__( 'Are you sure you want to uninstall and deactivate Stream? This will delete all Stream tables from the database and cannot be undone.', 'stream' ),
449 + ),
450 + 'locale' => esc_js( $locale ),
451 + 'gmt_offset' => get_option( 'gmt_offset' ),
539 452 )
540 453 );
541 454
542 - $current_order = isset( $_GET['order'] ) ? sanitize_key( wp_unslash( $_GET['order'] ) ) : 'desc'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
543 - if ( ! in_array( $current_order, array( 'asc', 'desc' ), true ) ) {
544 - $current_order = 'desc';
545 - }
546 - $current_query = map_deep( wp_unslash( $_GET ), 'sanitize_text_field' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
547 -
548 - $this->plugin->enqueue_asset(
549 - 'live-updates',
550 - array( 'heartbeat' ),
455 + wp_localize_script(
456 + 'wp-stream-live-updates',
457 + 'wp_stream_live_updates',
551 458 array(
552 459 'current_screen' => $hook,
553 - 'current_page' => isset( $_GET['paged'] ) ? absint( wp_unslash( $_GET['paged'] ) ) : '1', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
554 - 'current_order' => $current_order,
555 - 'current_query' => wp_json_encode( $current_query ),
556 - 'current_query_count' => count( $current_query ),
460 + 'current_page' => isset( $_GET['paged'] ) ? esc_js( $_GET['paged'] ) : '1', // WPCS: CSRF ok.
461 + // input var okay, CSRF okay
462 + 'current_order' => isset( $_GET['order'] ) ? esc_js( $_GET['order'] ) : 'desc', // WPCS: CSRF ok.
463 + // input var okay, CSRF okay
464 + 'current_query' => wp_stream_json_encode( $_GET ), // WPCS: CSRF ok.
465 + // input var okay, CSRF okay
466 + 'current_query_count' => count( $_GET ), // WPCS: CSRF ok.
467 + // input var okay, CSRF okay
557 468 )
558 469 );
559 470 }
560 471
@@ -571,16 +482,17 @@
571 482 * @return int
572 483 */
573 484 $bulk_actions_threshold = apply_filters( 'wp_stream_bulk_actions_threshold', 100 );
574 485
575 - $this->plugin->enqueue_asset(
576 - 'global',
577 - array(),
486 + wp_enqueue_script( 'wp-stream-global', $this->plugin->locations['url'] . 'ui/js/global.' . $min . 'js', array( 'jquery' ), $this->plugin->get_version() );
487 + wp_localize_script(
488 + 'wp-stream-global',
489 + 'wp_stream_global',
578 490 array(
579 491 'bulk_actions' => array(
580 492 'i18n' => array(
581 - /* translators: %s: a number of items (e.g. "1,742") */
582 - 'confirm_action' => sprintf( __( 'Are you sure you want to perform bulk actions on over %s items? This process could take a while to complete.', 'stream' ), number_format( absint( $bulk_actions_threshold ) ) ),
493 + // translators: Placeholder refers to a number of items (e.g. "1,742")
494 + 'confirm_action' => sprintf( esc_html__( 'Are you sure you want to perform bulk actions on over %s items? This process could take a while to complete.', 'stream' ), number_format( absint( $bulk_actions_threshold ) ) ),
583 495 ),
584 496 'threshold' => absint( $bulk_actions_threshold ),
585 497 ),
586 498 'plugins_screen_url' => self_admin_url( 'plugins.php#stream' ),
@@ -593,23 +505,17 @@
593 505 *
594 506 * @return bool
595 507 */
596 508 public function is_stream_screen() {
597 - if ( ! is_admin() ) {
598 - return false;
509 + if ( is_admin() && false !== strpos( wp_stream_filter_input( INPUT_GET, 'page' ), $this->records_page_slug ) ) {
510 + return true;
599 511 }
600 512
601 - $page = wp_stream_filter_input( INPUT_GET, 'page' );
602 - if ( is_string( $page ) && false !== strpos( $page, $this->records_page_slug ) ) {
513 + $screen = get_current_screen();
514 + if ( is_admin() && Alerts::POST_TYPE === $screen->post_type ) {
603 515 return true;
604 516 }
605 517
606 - if ( is_admin() && function_exists( 'get_current_screen' ) ) {
607 - $screen = get_current_screen();
608 -
609 - return ( Alerts::POST_TYPE === $screen->post_type );
610 - }
611 -
612 518 return false;
613 519 }
614 520
615 521 /**
@@ -614,9 +520,9 @@
614 520
615 521 /**
616 522 * Add a specific body class to all Stream admin screens
617 523 *
618 - * @param string $classes CSS classes to output to body.
524 + * @param string $classes CSS classes to output to body
619 525 *
620 526 * @filter admin_body_class
621 527 *
622 528 * @return string
@@ -626,10 +532,10 @@
626 532
627 533 if ( $this->is_stream_screen() ) {
628 534 $stream_classes[] = $this->admin_body_class;
629 535
630 - if ( isset( $_GET['page'] ) ) { // // phpcs:ignore WordPress.Security.NonceVerification.Recommended
631 - $stream_classes[] = sanitize_key( $_GET['page'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
536 + if ( isset( $_GET['page'] ) ) { // CSRF okay
537 + $stream_classes[] = sanitize_key( $_GET['page'] ); // input var okay, CSRF okay
632 538 }
633 539 }
634 540
635 541 /**
@@ -643,14 +549,20 @@
643 549 return sprintf( '%s %s ', $classes, $stream_classes );
644 550 }
645 551
646 552 /**
647 - * Add menu styles for various WP Admin skins.
553 + * Add menu styles for various WP Admin skins
648 554 *
555 + * @uses \wp_add_inline_style()
556 + *
649 557 * @action admin_enqueue_scripts
650 558 */
651 559 public function admin_menu_css() {
652 - // Make sure we're working off a clean version.
560 + $min = wp_stream_min_suffix();
561 + wp_register_style( 'wp-stream-datepicker', $this->plugin->locations['url'] . 'ui/css/datepicker.' . $min . 'css', array(), $this->plugin->get_version() );
562 + wp_register_style( 'wp-stream-icons', $this->plugin->locations['url'] . 'ui/stream-icons/style.css', array(), $this->plugin->get_version() );
563 +
564 + // Make sure we're working off a clean version
653 565 if ( ! file_exists( ABSPATH . WPINC . '/version.php' ) ) {
654 566 return;
655 567 }
656 568 include ABSPATH . WPINC . '/version.php';
@@ -658,29 +570,61 @@
658 570 if ( ! isset( $wp_version ) ) {
659 571 return;
660 572 }
661 573
662 - $css = "
663 - body.{$this->admin_body_class} #wpbody-content .wrap h1:nth-child(1):before {
664 - content: '';
665 - display: inline-block;
666 - width: 24px;
667 - height: 24px;
668 - margin-right: 8px;
669 - vertical-align: text-bottom;
670 - background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIGZpbGw9ImN1cnJlbnRjb2xvciI+Cgk8cGF0aCBkPSJNOTAzLjExNSA1MTUuNDEzYy00OS4zOTIgMC05MS40NzQgMzEuMzM3LTEwNy40NiA3NS4yMDNsLTEyNC40MTEtMS41MzJjLTExLjM3Ny0uMzQ2LTIyLjc1MS0uNjg5LTM0LjEyOS0uOTk4bC0uMjQxLjU3NC0yMi40MzYtLjI3OC0uMTUzLS45Mi0xNS4wNTYtODIuOTMzLTIwLjE0Ni0xMDguNDA1LTIwLjU0NC0xMDguMzM3TDUwMy45ODIgMGwtNTMuMTQxIDQyOS4wMTMtMTYuMjE0IDEzNy45MzUtMTIuMDE2IDEwNi45MjQtMTE3LjI4Ni0yODUuMjItMTguMzUzIDIwMi44MWMtNDIuNTYyIDEuNDU0LTg1LjEyNyAyLjkzNC0xMjcuNjg4IDQuNzM4LTUzLjA5NyAyLjI5Mi0xMDYuMTg3IDQuNDczLTE1OS4yODQgNy41MzZ2NDIuMDQyYzUzLjA5NyAzLjA2IDEwNi4xODcgNS4yNDcgMTU5LjI4NCA3LjUzMyA1My4wOTMgMi4yNDUgMTA2LjE4IDQuMTk0IDE1OS4yNzMgNS45MDNsMTQuMjQuNDY1IDE3LjM1MSA0OC4zOWMxOC44NDIgNTEuODc0IDM3LjU0MiAxMDMuODA2IDU2Ljc2NSAxNTUuNTQxTDQ2Ni41MiAxMDI0bDQxLjUxMi0zMDguMjkzIDE3LjYzMy0xMzYuNjg1IDEwLjc3NiA1MC4zMjkgNTQuODE1IDI0OC41NDQgNzIuNTE2LTIxNy4yMTdoMTI5LjI2MWMxMy40OTMgNDguMTIxIDU3LjY1NSA4My40MjkgMTEwLjA3NSA4My40MjkgNjMuMTYgMCAxMTQuMzUyLTUxLjIwNSAxMTQuMzUyLTExNC4zNDggMC02My4xMzktNTEuMTg5LTExNC4zNDUtMTE0LjM0OS0xMTQuMzQ1bC4wMDQtLjAwMVoiIC8+Cjwvc3ZnPgo=');
671 - }
672 - #menu-posts-feedback .wp-menu-image:before {
673 - font-family: dashicons !important;
674 - content: '\\f175';
675 - }
676 - #adminmenu #menu-posts-feedback div.wp-menu-image {
677 - background: none !important;
678 - background-repeat: no-repeat;
679 - }
680 - ";
574 + $body_class = $this->admin_body_class;
575 + $records_page = $this->records_page_slug;
576 + $stream_url = $this->plugin->locations['url'];
681 577
682 - wp_add_inline_style( 'wp-admin', $css );
578 + if ( version_compare( $wp_version, '3.8-alpha', '>=' ) ) {
579 + wp_enqueue_style( 'wp-stream-icons' );
580 +
581 + $css = "
582 + #toplevel_page_{$records_page} .wp-menu-image:before {
583 + font-family: 'WP Stream' !important;
584 + content: '\\73' !important;
585 + }
586 + #toplevel_page_{$records_page} .wp-menu-image {
587 + background-repeat: no-repeat;
588 + }
589 + #menu-posts-feedback .wp-menu-image:before {
590 + font-family: dashicons !important;
591 + content: '\\f175';
592 + }
593 + #adminmenu #menu-posts-feedback div.wp-menu-image {
594 + background: none !important;
595 + background-repeat: no-repeat;
596 + }
597 + body.{$body_class} #wpbody-content .wrap h1:nth-child(1):before {
598 + font-family: 'WP Stream' !important;
599 + content: '\\73';
600 + padding: 0 8px 0 0;
601 + }
602 + ";
603 + } else {
604 + $css = "
605 + #toplevel_page_{$records_page} .wp-menu-image {
606 + background: url( {$stream_url}ui/stream-icons/menuicon-sprite.png ) 0 90% no-repeat;
607 + }
608 + /* Retina Stream Menu Icon */
609 + @media only screen and (-moz-min-device-pixel-ratio: 1.5),
610 + only screen and (-o-min-device-pixel-ratio: 3/2),
611 + only screen and (-webkit-min-device-pixel-ratio: 1.5),
612 + only screen and (min-device-pixel-ratio: 1.5) {
613 + #toplevel_page_{$records_page} .wp-menu-image {
614 + background: url( {$stream_url}ui/stream-icons/menuicon-sprite-2x.png ) 0 90% no-repeat;
615 + background-size:30px 64px;
616 + }
617 + }
618 + #toplevel_page_{$records_page}.current .wp-menu-image,
619 + #toplevel_page_{$records_page}.wp-has-current-submenu .wp-menu-image,
620 + #toplevel_page_{$records_page}:hover .wp-menu-image {
621 + background-position: top left;
622 + }
623 + ";
624 + }
625 +
626 + \wp_add_inline_style( 'wp-admin', $css );
683 627 }
684 628
685 629 /**
686 630 * Handle the reset AJAX request to reset logs.
@@ -695,14 +639,8 @@
695 639 esc_html__( "You don't have sufficient privileges to do this action.", 'stream' )
696 640 );
697 641 }
698 642
699 - // Ensure the database tables exist before attempting to clear records.
700 - // Install::check() short-circuits on DOING_AJAX, so call install()
701 - // directly. dbDelta is idempotent and safe to run when tables already
702 - // exist.
703 - $this->plugin->install->install( $this->plugin->get_version() );
704 -
705 643 $this->erase_stream_records();
706 644
707 645 if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
708 646 return true;
@@ -707,9 +645,9 @@
707 645 if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
708 646 return true;
709 647 }
710 648
711 - wp_safe_redirect(
649 + wp_redirect(
712 650 add_query_arg(
713 651 array(
714 652 'page' => is_network_admin() ? $this->network->network_settings_page_slug : $this->settings_page_slug,
715 653 'message' => 'data_erased',
@@ -720,906 +658,84 @@
720 658
721 659 exit;
722 660 }
723 661
724 - /**
725 - * Clears stream records from the database.
726 - *
727 - * @return void
728 - */
729 662 private function erase_stream_records() {
730 663 global $wpdb;
731 664
732 - // If this is a multisite and it's not network activated,
733 - // only delete the entries from the blog which made the request.
734 - if ( $this->plugin->is_multisite_not_network_activated() ) {
665 + $where = '';
735 666
736 - // First check the log size.
737 - $stream_log_size = self::get_blog_record_table_size();
738 -
739 - // If this is a large log and we need to delete only the entries
740 - // pertaining to an individual site, we will need to do those in batches.
741 - if ( $this->plugin->is_large_records_table( $stream_log_size ) ) {
742 - $this->schedule_erase_large_records( $stream_log_size );
743 - return;
744 - }
745 -
746 - $wpdb->query(
747 - $wpdb->prepare(
748 - "DELETE `stream`, `meta`
749 - FROM {$wpdb->stream} AS `stream`
750 - LEFT JOIN {$wpdb->streammeta} AS `meta`
751 - ON `meta`.`record_id` = `stream`.`ID`
752 - WHERE `blog_id`=%d;",
753 - get_current_blog_id()
754 - )
755 - );
756 - } else {
757 - // If we are deleting all the entries, we can truncate the tables.
758 - $wpdb->query( "TRUNCATE {$wpdb->streammeta};" );
759 - $wpdb->query( "TRUNCATE {$wpdb->stream};" );
760 - // Tidy up any meta which may have been added in between the two truncations.
761 - $this->delete_orphaned_meta();
667 + if ( is_multisite() && ! $this->plugin->is_network_activated() ) {
668 + $where .= $wpdb->prepare( ' AND `blog_id` = %d', get_current_blog_id() );
762 669 }
763 - }
764 670
765 - /**
766 - * Schedule the initial event to start erasing the logs from now.
767 - *
768 - * @param int $log_size The number of rows which will be affected.
769 - * @return void
770 - */
771 - private function schedule_erase_large_records( int $log_size ) {
772 - global $wpdb;
773 -
774 - $last_entry = $wpdb->get_var(
775 - $wpdb->prepare(
776 - "SELECT ID FROM {$wpdb->stream} WHERE `blog_id`=%d ORDER BY ID DESC LIMIT 1",
777 - get_current_blog_id()
778 - )
779 - );
780 -
781 - // If there are no entries to erase, don't try to erase them.
782 - if ( empty( $last_entry ) ) {
783 - return;
784 - }
785 -
786 - // We are going to delete this many and this many only.
787 - // This is to avoid the situation where rows keep getting added
788 - // between the Action Scheduler runs and they never stop.
789 - $args = array(
790 - 'total' => (int) $log_size,
791 - 'done' => 0,
792 - 'last_entry' => (int) $last_entry,
793 - 'blog_id' => (int) get_current_blog_id(),
794 - );
795 -
796 - $this->plugin->scheduler->enqueue_async( self::ASYNC_DELETION_ACTION, $args );
797 -
798 - $this->maybe_warn_large_table_without_action_scheduler(
799 - (int) $log_size,
800 - __( 'reset the Stream database (delete all records for this site)', 'stream' )
801 - );
802 - }
803 -
804 - /**
805 - * Warn when a large-table batched operation has to lean on WP-Cron.
806 - *
807 - * Action Scheduler is purpose-built to drain long self-chaining batch
808 - * jobs reliably; default WP-Cron fires opportunistically on traffic and
809 - * can stall a multi-hour chain on a low-traffic site. When Stream is
810 - * running the WP-Cron fallback (the `wp_stream_use_action_scheduler`
811 - * filter returned false, or the bundled AS library is absent) against a
812 - * table over the large-table threshold, surface a notice pointing the
813 - * operator at a deterministic WP-CLI drain instead of failing silently.
814 - *
815 - * Delivery depends on context. Under WP-CLI the warning is emitted
816 - * immediately via {@see Admin::notice()} (WP_CLI::warning) — scheduling
817 - * the batch chain onto WP-Cron does not drain it, so a headless /
818 - * low-traffic site is exactly where the chain can stall. Outside WP-CLI
819 - * neither call site renders its own output (the recurring purge runs
820 - * under DOING_CRON; the manual reset redirects and exits before its
821 - * shutdown hook output reaches the browser), so the message is persisted
822 - * to {@see Admin::LARGE_TABLE_CRON_NOTICE_OPTION} and rendered on the
823 - * next admin page load by {@see Admin::display_large_table_cron_notice()}.
824 - *
825 - * No-op when Action Scheduler is the active backend (built to drain long
826 - * chains). The `wp_stream_enable_auto_purge` filter deliberately does NOT
827 - * gate this helper: it governs TTL retention purging only, while this
828 - * warning also covers the manual database reset — an operator who manages
829 - * retention externally can still click "Reset Stream Database" and needs
830 - * the stall warning. The auto-purge call site is already gated by the
831 - * filter's early return in {@see Admin::purge_scheduled_action()}.
832 - *
833 - * @param int $record_count Number of rows the operation will touch.
834 - * @param string $operation Human-readable, translated description of what the
835 - * batched work does (e.g. "delete records older than
836 - * the retention period"), interpolated into the notice.
837 - * @return void
838 - */
839 - private function maybe_warn_large_table_without_action_scheduler( int $record_count, string $operation ) {
840 - if ( $this->plugin->scheduler instanceof AS_Scheduler ) {
841 - return;
842 - }
843 -
844 - if ( ! $this->plugin->is_large_records_table( $record_count ) ) {
845 - return;
846 - }
847 -
848 - $message = sprintf(
849 - /* translators: 1: operation description (e.g. "delete records older than the retention period"), 2: number of records, 3: WP-CLI command. */
850 - __( 'Stream queued a large batched operation to %1$s (%2$s records) to WP-Cron because Action Scheduler is disabled. The records are removed in chained batches as WP-Cron runs. This completes on its own where reliable cron is configured (a Linux crontab or third-party cron service triggering wp-cron.php on a fixed interval, without an execution timeout). On sites relying on default traffic-triggered WP-Cron the chain may stall before it finishes, leaving records only partly removed; to run it to completion deterministically, use WP-CLI: %3$s', 'stream' ),
851 - $operation,
852 - number_format_i18n( $record_count ),
853 - '<code>wp cron event run --due-now</code>'
854 - );
855 -
856 - if ( defined( 'WP_CLI' ) && WP_CLI ) {
857 - // Immediate WP_CLI::warning — the operator is watching the terminal.
858 - $this->notice( $message );
859 - return;
860 - }
861 -
862 - // Persist for the next admin page load. Neither call site can render
863 - // output itself: the recurring purge runs under DOING_CRON (response
864 - // discarded) and the manual reset redirects + exits before shutdown
865 - // output reaches the browser. No autoload — this is set rarely and
866 - // read only in the admin.
867 - update_option( self::LARGE_TABLE_CRON_NOTICE_OPTION, $message, false );
868 - }
869 -
870 - /**
871 - * Render (and clear) the persisted large-table WP-Cron warning.
872 - *
873 - * Counterpart to {@see Admin::maybe_warn_large_table_without_action_scheduler()}:
874 - * displays the stored warning on the first admin page an operator with
875 - * the Stream settings capability loads after a large batched operation
876 - * was queued onto WP-Cron.
877 - *
878 - * @action admin_notices
879 - * @action network_admin_notices
880 - *
881 - * @return void
882 - */
883 - public function display_large_table_cron_notice() {
884 - if ( ! current_user_can( $this->settings_cap ) ) {
885 - return;
886 - }
887 -
888 - $message = get_option( self::LARGE_TABLE_CRON_NOTICE_OPTION );
889 - if ( empty( $message ) ) {
890 - return;
891 - }
892 -
893 - delete_option( self::LARGE_TABLE_CRON_NOTICE_OPTION );
894 -
895 - printf(
896 - '<div class="notice notice-warning">%s</div>',
897 - wp_kses_post( wpautop( $message ) )
898 - );
899 - }
900 -
901 - /**
902 - * Checks if the async deletion process is running.
903 - *
904 - * Checks pending AND in-flight state, mirroring
905 - * {@see Admin::is_running_auto_purge()}. Under WP-Cron the event is
906 - * removed from the cron array before its callback runs, so a
907 - * pending-only probe would momentarily read idle mid-chain and briefly
908 - * re-expose the reset link in Settings. The batch worker keeps the
909 - * best-effort running marker set for that window (see
910 - * {@see Admin::erase_large_records()}). The marker transient is shared
911 - * with the auto-purge chain, which only makes both guards more
912 - * conservative — never less safe.
913 - *
914 - * @return bool True if the async deletion process is running, false otherwise.
915 - */
916 - public static function is_running_async_deletion() {
917 - $plugin = wp_stream_get_instance();
918 - if ( empty( $plugin->scheduler ) ) {
919 - return false;
920 - }
921 - return $plugin->scheduler->any_pending_or_running( array( self::ASYNC_DELETION_ACTION ) );
922 - }
923 -
924 - /**
925 - * Checks if any auto-purge action is currently scheduled or in-flight.
926 - *
927 - * Returns true when either the batched chain worker or the terminal
928 - * orphan reaper is pending OR running. The recurring scheduler is
929 - * intentionally excluded — it is always pending under normal operation,
930 - * so including it here would make the probe useless. Used by the
931 - * Settings → Advanced UI to render an "Auto-purge currently running"
932 - * notice and by the recurring callback as an overlap guard.
933 - *
934 - * Checks both PENDING and IN-PROGRESS statuses so a chain that is
935 - * mid-execution (e.g. the batch worker is currently running and has not
936 - * yet enqueued the next batch) still reports as running. Without the
937 - * RUNNING check the overlap guard can let a second parallel chain stack
938 - * against the same rows.
939 - *
940 - * @return bool
941 - */
942 - public static function is_running_auto_purge() {
943 - $plugin = wp_stream_get_instance();
944 - if ( empty( $plugin->scheduler ) ) {
945 - return false;
946 - }
947 -
948 - return $plugin->scheduler->any_pending_or_running(
949 - array( self::AUTO_PURGE_BATCH_ACTION, self::AUTO_PURGE_REAPER_ACTION )
950 - );
951 - }
952 -
953 - /**
954 - * Erases large records from the stream table.
955 - *
956 - * This function deletes records from the stream table in batches, starting from a given entry ID.
957 - * It deletes records in reverse chronological order, starting from the largest ID and going back.
958 - * The number of records deleted in each batch is determined by the batch size, which can be filtered
959 - * using the 'wp_stream_batch_size' hook.
960 - *
961 - * @param int $total The total number of records to be deleted.
962 - * @param int $done The number of records that have already been deleted.
963 - * @param int $last_entry The ID of the last entry that was deleted.
964 - * @param int $blog_id The ID of the blog for which the records should be deleted.
965 - * @return void
966 - */
967 - public function erase_large_records( int $total, int $done, int $last_entry, int $blog_id ) {
968 - global $wpdb;
969 -
970 - // Best-effort "running" marker, mirroring auto_purge_batch(). Under
971 - // WP-Cron the event is dequeued before this callback runs, so without
972 - // the marker is_running_async_deletion() would momentarily read idle
973 - // between batches and briefly re-expose the reset link in Settings.
974 - // No-op under Action Scheduler; self-expires on a fatal.
975 - $this->plugin->scheduler->mark_running( 'async_deletion' );
976 -
977 - $start_from = $wpdb->get_var(
978 - $wpdb->prepare(
979 - "SELECT ID FROM {$wpdb->stream} WHERE ID < %d AND `blog_id`=%d ORDER BY ID DESC LIMIT 1",
980 - $last_entry + 1, // A tweak to get it correct the first time through.
981 - get_current_blog_id()
982 - )
983 - );
984 -
985 - if ( empty( $start_from ) ) {
986 - // Terminal batch: nothing left to delete, no further event will
987 - // be chained, and no work follows within this callback — safe to
988 - // clear the marker immediately (unlike the auto-purge chain,
989 - // whose terminal batch hands off to the reaper).
990 - $this->plugin->scheduler->mark_done( 'async_deletion' );
991 - return;
992 - }
993 -
994 - /**
995 - * Filters the number of records in the {$wpdb->stream} table to do at a time.
996 - *
997 - * @since 4.1.0
998 - *
999 - * @param int $batch_size The batch size, default 250000.
1000 - */
1001 - $batch_size = apply_filters( 'wp_stream_batch_size', 250000 );
1002 -
1003 - // This will tend to erase them in reverse chronological order,
1004 - // ie it will start from the largest ID and go back from there.
1005 671 $wpdb->query(
1006 - $wpdb->prepare(
1007 - "DELETE `stream`, `meta`
1008 - FROM {$wpdb->stream} AS `stream`
1009 - LEFT JOIN {$wpdb->streammeta} AS `meta`
1010 - ON `meta`.`record_id` = `stream`.`ID`
1011 - WHERE ID <= %d AND ID >= %d AND `blog_id`=%d;",
1012 - $start_from,
1013 - $start_from - $batch_size,
1014 - get_current_blog_id()
1015 - )
672 + "DELETE `stream`, `meta`
673 + FROM {$wpdb->stream} AS `stream`
674 + LEFT JOIN {$wpdb->streammeta} AS `meta`
675 + ON `meta`.`record_id` = `stream`.`ID`
676 + WHERE 1=1 {$where};" // @codingStandardsIgnoreLine $where already prepared
1016 677 );
1017 -
1018 - $remaining = $wpdb->get_var(
1019 - $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->stream} WHERE `blog_id`=%d", $blog_id )
1020 - );
1021 -
1022 - $done = $total - $remaining;
1023 -
1024 - $this->plugin->scheduler->enqueue_async(
1025 - self::ASYNC_DELETION_ACTION,
1026 - array(
1027 - 'total' => (int) $total,
1028 - 'done' => (int) $done,
1029 - 'last_entry' => (int) $start_from - $batch_size, // The last ID checked.
1030 - 'blog_id' => (int) $blog_id,
1031 - )
1032 - );
1033 678 }
1034 679
1035 - /**
1036 - * Retrieves the size of the blog record table for a specific blog.
1037 - *
1038 - * @param int|null $blog_id The ID of the blog. If not provided, the current blog ID will be used.
1039 - * @return int The size of the blog record table.
1040 - */
1041 - public static function get_blog_record_table_size( $blog_id = null ): int {
1042 - global $wpdb;
1043 -
1044 - $blog_id = empty( $blog_id ) ? get_current_blog_id() : $blog_id;
1045 -
1046 - $blog_size = $wpdb->get_var(
1047 - $wpdb->prepare(
1048 - "SELECT COUNT(ID) FROM {$wpdb->stream} WHERE `blog_id`=%d",
1049 - $blog_id
1050 - )
1051 - );
1052 -
1053 - return (int) $blog_size;
1054 - }
1055 -
1056 - /**
1057 - * Schedules a purge of records.
1058 - *
1059 - * @return void
1060 - */
1061 680 public function purge_schedule_setup() {
1062 - // Clear the legacy WP-Cron event scheduled by Stream <= 4.1.x so it
1063 - // cannot double-fire alongside the new recurring action.
1064 - if ( wp_next_scheduled( 'wp_stream_auto_purge' ) ) {
1065 - wp_clear_scheduled_hook( 'wp_stream_auto_purge' );
681 + if ( ! wp_next_scheduled( 'wp_stream_auto_purge' ) ) {
682 + wp_schedule_event( time(), 'twicedaily', 'wp_stream_auto_purge' );
1066 683 }
1067 -
1068 - $scheduler = $this->plugin->scheduler;
1069 -
1070 - /**
1071 - * Filter whether Stream schedules its TTL record auto-purge at all.
1072 - *
1073 - * Custom storage drivers that manage retention externally (TTL
1074 - * indexes, partition rotation, a warehouse job, etc.) can return
1075 - * false to disable all TTL purge scheduling regardless of the
1076 - * scheduler backend. Any already-registered recurring purge is
1077 - * unscheduled from both backends so it cannot keep firing.
1078 - *
1079 - * @param bool $enabled Whether auto-purge scheduling is enabled.
1080 - */
1081 - if ( ! apply_filters( 'wp_stream_enable_auto_purge', true ) ) {
1082 - // Tear down only once, then record the 'disabled' sentinel in the
1083 - // backend marker. This runs on every wp_loaded, so without the
1084 - // guard a permanently-disabled site would pay the unschedule
1085 - // probes on every request; with it, steady state is a single
1086 - // in-memory compare (the marker is autoloaded). The sentinel also
1087 - // covers a site upgrading with the filter already active (no
1088 - // marker yet, but a recurring action left by a previous version).
1089 - // The executing path is independently gated by the same filter in
1090 - // purge_scheduled_action(), so a stray entry that somehow survives
1091 - // cannot purge anything anyway.
1092 - if ( 'disabled' !== get_option( self::SCHEDULER_BACKEND_OPTION ) ) {
1093 - $scheduler->unschedule_all( self::AUTO_PURGE_ACTION );
1094 - wp_unschedule_hook( self::AUTO_PURGE_ACTION );
1095 -
1096 - // Also clear the Action Scheduler store when its API is
1097 - // available but AS is not the active backend (e.g. the cron
1098 - // backend is selected while WooCommerce provides AS). The
1099 - // active-backend unschedule above cannot see AS's store, and
1100 - // this filter promises teardown from BOTH backends. When AS
1101 - // is entirely absent this is skipped — a stray AS entry
1102 - // cannot execute (no AS runner), and if AS appears later the
1103 - // action fires as a no-op thanks to the execute-path gate.
1104 - if ( ! $scheduler instanceof AS_Scheduler && function_exists( 'as_unschedule_all_actions' ) ) {
1105 - ( new AS_Scheduler() )->unschedule_all( self::AUTO_PURGE_ACTION );
1106 - }
1107 -
1108 - update_option( self::SCHEDULER_BACKEND_OPTION, 'disabled' );
1109 - }
1110 - return;
1111 - }
1112 -
1113 - $backend = $scheduler instanceof AS_Scheduler ? 'action_scheduler' : 'wp_cron';
1114 -
1115 - // Detect a backend switch and clear the inactive backend's copy of the
1116 - // recurring action exactly once. A site that switched schedulers (via
1117 - // the wp_stream_use_action_scheduler filter) would otherwise keep
1118 - // firing the purge from BOTH backends — the two stores are independent
1119 - // and neither overlap guard can see the other. The marker is an
1120 - // autoloaded option, so the steady-state cost on every wp_loaded is a
1121 - // single in-memory compare; the cleanup query runs only on the first
1122 - // page load after a switch. Idempotent and self-healing. No data is
1123 - // affected — only the redundant schedule entry.
1124 - if ( get_option( self::SCHEDULER_BACKEND_OPTION ) !== $backend ) {
1125 - $cleanup_done = true;
1126 -
1127 - if ( 'action_scheduler' === $backend ) {
1128 - // Drop any leftover WP-Cron recurring event.
1129 - wp_unschedule_hook( self::AUTO_PURGE_ACTION );
1130 - } elseif ( function_exists( 'as_unschedule_all_actions' ) ) {
1131 - // Drop any leftover Action Scheduler recurring action. Routed
1132 - // through AS_Scheduler so the as_*() call stays contained there.
1133 - ( new AS_Scheduler() )->unschedule_all( self::AUTO_PURGE_ACTION );
1134 - } else {
1135 - // Action Scheduler is not loaded (cron backend selected and no
1136 - // other plugin provides AS), so its store cannot be cleaned
1137 - // right now. Do NOT write the marker: if an AS-providing
1138 - // plugin (e.g. WooCommerce) is installed later, the stray
1139 - // Stream recurring action in the AS store would resume firing
1140 - // alongside the cron one — and the cron overlap guard cannot
1141 - // see it. Leaving the marker stale retries this cleanup on a
1142 - // later request once as_unschedule_all_actions() exists.
1143 - $cleanup_done = false;
1144 - }
1145 -
1146 - if ( $cleanup_done ) {
1147 - update_option( self::SCHEDULER_BACKEND_OPTION, $backend );
1148 - }
1149 - }
1150 -
1151 - // 12 hours == old `twicedaily` interval. The scheduler only schedules
1152 - // a fresh recurring action when one is not already registered.
1153 - $scheduler->schedule_recurring(
1154 - time(),
1155 - 12 * HOUR_IN_SECONDS,
1156 - self::AUTO_PURGE_ACTION,
1157 - array(),
1158 - self::AUTO_PURGE_GROUP
1159 - );
1160 684 }
1161 685
1162 - /**
1163 - * Deletes orphaned meta records from the database.
1164 - *
1165 - * Deletes meta records from the stream meta table where the corresponding
1166 - * stream record no longer exists.
1167 - *
1168 - * @global wpdb $wpdb The WordPress database object.
1169 - */
1170 - protected function delete_orphaned_meta() {
686 + public function purge_scheduled_action() {
1171 687 global $wpdb;
1172 688
1173 - $wpdb->query(
1174 - "DELETE `meta` FROM {$wpdb->streammeta} as `meta` LEFT JOIN {$wpdb->stream} as `stream` ON `stream`.`ID`=`meta`.`record_id` WHERE `stream`.`ID` IS NULL"
1175 - );
1176 - }
1177 -
1178 - /**
1179 - * Executes a scheduled purge
1180 - *
1181 - * @return void
1182 - */
1183 - public function purge_scheduled_action() {
1184 - // Respect the auto-purge master switch on the executing path too, not
1185 - // just at scheduling time. A recurring action already in flight when
1186 - // the filter flips to false (or an args-specific entry the unschedule
1187 - // missed) would otherwise still run a purge cycle the operator opted
1188 - // out of. This filter is documented in Admin::purge_schedule_setup().
1189 - if ( ! apply_filters( 'wp_stream_enable_auto_purge', true ) ) {
1190 - return;
1191 - }
1192 -
1193 - // Don't purge when in Network Admin unless Stream is network activated.
689 + // Don't purge when in Network Admin unless Stream is network activated
1194 690 if (
1195 - $this->plugin->is_multisite_not_network_activated()
691 + is_multisite()
1196 692 &&
1197 693 is_network_admin()
694 + &&
695 + ! $this->plugin->is_network_activated()
1198 696 ) {
1199 697 return;
1200 698 }
1201 699
1202 - $defaults = $this->plugin->settings->get_defaults();
1203 - if ( $this->plugin->is_multisite_network_activated() ) {
1204 - $options = wp_parse_args( (array) get_site_option( 'wp_stream_network', array() ), $defaults );
700 + if ( is_multisite() && $this->plugin->is_network_activated() ) {
701 + $options = (array) get_site_option( 'wp_stream_network', array() );
1205 702 } else {
1206 - $options = wp_parse_args( (array) get_option( 'wp_stream', array() ), $defaults );
703 + $options = (array) get_option( 'wp_stream', array() );
1207 704 }
1208 705
1209 - // TTL fallback. Settings::get_defaults() runs every settings field
1210 - // through the `wp_stream_settings_option_fields` filter, which
1211 - // Network::get_network_admin_fields() uses to strip the `records_ttl`
1212 - // field from the per-site option's defaults set. When this callback runs
1213 - // outside any admin context (Action Scheduler, WP-CLI, system cron), the
1214 - // per-site option_key is in effect, so the filtered defaults array does
1215 - // not contain general_records_ttl at all. Apply the documented 30-day
1216 - // default (classes/class-settings.php, `records_ttl` field) only when
1217 - // the key is genuinely missing, so an operator who set the value via
1218 - // CLI/SQL keeps their explicit choice.
1219 - if ( ! isset( $options['general_records_ttl'] ) ) {
1220 - $options['general_records_ttl'] = 30;
1221 - }
1222 -
1223 - if ( ! empty( $options['general_keep_records_indefinitely'] ) ) {
706 + if ( ! empty( $options['general_keep_records_indefinitely'] ) || ! isset( $options['general_records_ttl'] ) ) {
1224 707 return;
1225 708 }
1226 709
1227 - // Refuse to purge with a non-positive TTL. The UI enforces min=1, but
1228 - // CLI/SQL can set 0 or a negative integer. Honoring those would mean
1229 - // "delete every record on every cycle", which has no legitimate use
1230 - // case (keep_records_indefinitely covers the opposite extreme).
1231 - // Bailing out makes operator error visible (records stop being purged)
1232 - // instead of catastrophic (records get wiped repeatedly).
1233 - if ( (int) $options['general_records_ttl'] < 1 ) {
1234 - return;
1235 - }
710 + $days = $options['general_records_ttl'];
711 + $timezone = new DateTimeZone( 'UTC' );
712 + $date = new DateTime( 'now', $timezone );
1236 713
1237 - // Overlap guard: if any auto-purge action (batch worker or reaper) is
1238 - // pending or in-progress, don't stack a new chain. Reuses the same
1239 - // probe used by the Settings UI so the two views of "running" agree.
1240 - if ( self::is_running_auto_purge() ) {
1241 - return;
1242 - }
714 + $date->sub( DateInterval::createFromDateString( "$days days" ) );
1243 715
1244 - /**
1245 - * Fires once per auto-purge cycle, after all bail-out checks pass and
1246 - * immediately before deletion work is enqueued.
1247 - *
1248 - * Preserved for backward compatibility with consumers that hooked the
1249 - * legacy WP-Cron event of the same name in Stream <= 4.1.x. Note that
1250 - * since 4.2.0 this fires only when a purge is actually about to run —
1251 - * it no longer fires on every cron tick regardless of whether work
1252 - * happens. Hook into the recurring AS action (Admin::AUTO_PURGE_ACTION)
1253 - * directly if you need the older "every tick" semantics.
1254 - */
1255 - do_action( 'wp_stream_auto_purge' );
716 + $where = $wpdb->prepare( ' AND `stream`.`created` < %s', $date->format( 'Y-m-d H:i:s' ) );
1256 717
1257 - // Snapshot the UTC cutoff once per recurring tick. Each batch in this
1258 - // chain operates against this fixed cutoff so the chain is finite.
1259 - $days = (int) $options['general_records_ttl'];
1260 - $cutoff = ( new DateTime( 'now', new DateTimeZone( 'UTC' ) ) )
1261 - ->sub( DateInterval::createFromDateString( $days . ' days' ) )
1262 - ->format( 'Y-m-d H:i:s' );
1263 -
1264 - // blog_id = 0 means "all blogs" (network-activated path).
1265 - $blog_id = $this->plugin->is_multisite_not_network_activated() ? (int) get_current_blog_id() : 0;
1266 -
1267 - global $wpdb;
1268 -
1269 - // "Is this a large table?" decision matches the manual reset path
1270 - // (Admin::erase_stream_records()). When the table is small the cost
1271 - // of scheduling a chain (and waiting for AS to drain it on the next
1272 - // runner tick) exceeds the cost of a single inline DELETE. Only fall
1273 - // through to the batched chain when the filter says "yes, large".
1274 - if ( $blog_id > 0 ) {
1275 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1276 - $record_count = (int) $wpdb->get_var(
1277 - $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->stream} WHERE `blog_id` = %d", $blog_id )
1278 - );
1279 - } else {
1280 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1281 - $record_count = (int) $wpdb->get_var( "SELECT COUNT(ID) FROM {$wpdb->stream}" );
718 + // Multisite but NOT network activated, only purge the current blog
719 + if ( is_multisite() && ! $this->plugin->is_network_activated() ) {
720 + $where .= $wpdb->prepare( ' AND `blog_id` = %d', get_current_blog_id() );
1282 721 }
1283 722
1284 - if ( ! $this->plugin->is_large_records_table( $record_count ) ) {
1285 - // Small-table fast path: one inline multi-table DELETE, then enqueue
1286 - // the orphan reaper as a one-shot async action so the heal step is
1287 - // still observable in Tools → Scheduled Actions.
1288 - if ( $blog_id > 0 ) {
1289 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1290 - $wpdb->query(
1291 - $wpdb->prepare(
1292 - "DELETE `stream`, `meta`
1293 - FROM {$wpdb->stream} AS `stream`
1294 - LEFT JOIN {$wpdb->streammeta} AS `meta`
1295 - ON `meta`.`record_id` = `stream`.`ID`
1296 - WHERE `stream`.`created` < %s AND `stream`.`blog_id` = %d;",
1297 - $cutoff,
1298 - $blog_id
1299 - )
1300 - );
1301 - } else {
1302 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1303 - $wpdb->query(
1304 - $wpdb->prepare(
1305 - "DELETE `stream`, `meta`
1306 - FROM {$wpdb->stream} AS `stream`
1307 - LEFT JOIN {$wpdb->streammeta} AS `meta`
1308 - ON `meta`.`record_id` = `stream`.`ID`
1309 - WHERE `stream`.`created` < %s;",
1310 - $cutoff
1311 - )
1312 - );
1313 - }
1314 -
1315 - $this->plugin->scheduler->enqueue_async( self::AUTO_PURGE_REAPER_ACTION, array(), self::AUTO_PURGE_GROUP );
1316 - return;
1317 - }
1318 -
1319 - // Large-table path: batched chain.
1320 - $this->plugin->scheduler->enqueue_async(
1321 - self::AUTO_PURGE_BATCH_ACTION,
1322 - array(
1323 - 'cutoff' => $cutoff,
1324 - 'blog_id' => $blog_id,
1325 - ),
1326 - self::AUTO_PURGE_GROUP
723 + $wpdb->query(
724 + "DELETE `stream`, `meta`
725 + FROM {$wpdb->stream} AS `stream`
726 + LEFT JOIN {$wpdb->streammeta} AS `meta`
727 + ON `meta`.`record_id` = `stream`.`ID`
728 + WHERE 1=1 {$where};" // @codingStandardsIgnoreLine $where already prepared
1327 729 );
1328 -
1329 - $this->maybe_warn_large_table_without_action_scheduler(
1330 - $record_count,
1331 - __( 'delete records older than the retention period', 'stream' )
1332 - );
1333 730 }
1334 731
1335 732 /**
1336 - * Async Action Scheduler callback: delete one batch of records eligible
1337 - * under the snapshotted UTC cutoff, then chain the next batch (or the
1338 - * orphan reaper when nothing remains).
733 + * @param array $links
734 + * @param string $file
1339 735 *
1340 - * Window-based deletion mirrors {@see Admin::erase_large_records()} so the
1341 - * InnoDB lock footprint is bounded and predictable on bloated tables.
1342 - *
1343 - * @param string $cutoff MySQL DATETIME string in UTC.
1344 - * @param int $blog_id Blog to scope to, or 0 for all blogs (network-activated).
1345 - * @param int $last_entry The lower-bound ID of the previous batch's window; 0 on the
1346 - * first batch in a chain. The next SELECT uses `ID < last_entry`
1347 - * when non-zero, guaranteeing forward progress even on tables
1348 - * that grow rapidly during the chain. Trade-off: any eligible
1349 - * row that lands inside the already-touched ID range
1350 - * [window_low, start_from] after that batch ran is skipped
1351 - * by the current chain and picked up on the next recurring
1352 - * tick (or small-table fast path). Possible sources: dev/test
1353 - * seeders, importer/migration plugins replaying historical
1354 - * rows, or PHP/MySQL clock skew on `created`. Steady-state
1355 - * logging via Log::log() uses monotonic IDs and current UTC,
1356 - * so this is a no-op for normal production traffic.
1357 - * @throws \InvalidArgumentException When $cutoff is empty (signals AS to mark the action as failed).
1358 - * @return void
1359 - */
1360 - public function auto_purge_batch( $cutoff, $blog_id = 0, $last_entry = 0 ) {
1361 - global $wpdb;
1362 -
1363 - $cutoff = (string) $cutoff;
1364 - $blog_id = (int) $blog_id;
1365 - $last_entry = (int) $last_entry;
1366 -
1367 - // Defensive: a malformed cutoff would otherwise translate to a no-op
1368 - // DELETE that still busies the DB. Throw so Action Scheduler marks
1369 - // the action as failed (and visible in Tools → Scheduled Actions)
1370 - // rather than silently completing. In practice this is unreachable
1371 - // because purge_scheduled_action() always populates the cutoff arg
1372 - // and AS args are immutable; the guard exists for third-party code
1373 - // that may enqueue the action with bad input.
1374 - if ( '' === $cutoff ) {
1375 - throw new \InvalidArgumentException( 'auto_purge_batch requires a non-empty cutoff.' );
1376 - }
1377 -
1378 - // Best-effort "running" marker for schedulers without a native RUNNING
1379 - // store (cron). Bridges the gap between this batch starting and the
1380 - // next chained event being enqueued; self-expires on a fatal. No-op
1381 - // under Action Scheduler. Cleared when the chain reaches its terminal
1382 - // reaper (see the empty-$start_from branch below).
1383 - $this->plugin->scheduler->mark_running( 'auto_purge' );
1384 -
1385 - /**
1386 - * Filters the number of records to delete per batch.
1387 - *
1388 - * Shared with the manual reset path (see {@see Admin::erase_large_records()})
1389 - * so site owners only need to tune one knob.
1390 - *
1391 - * @since 4.1.0
1392 - *
1393 - * @param int $batch_size Default 250000.
1394 - */
1395 - $batch_size = (int) apply_filters( 'wp_stream_batch_size', 250000 );
1396 - if ( $batch_size < 1 ) {
1397 - $batch_size = 250000;
1398 - }
1399 -
1400 - // Find the highest-ID record still eligible under the snapshotted cutoff
1401 - // that lies strictly below the previous window's lower bound (when set).
1402 - // $last_entry=0 means "first batch in chain" — search from the top.
1403 - if ( $blog_id > 0 && $last_entry > 0 ) {
1404 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1405 - $start_from = $wpdb->get_var(
1406 - $wpdb->prepare(
1407 - "SELECT ID FROM {$wpdb->stream} WHERE `created` < %s AND `blog_id` = %d AND `ID` < %d ORDER BY ID DESC LIMIT 1",
1408 - $cutoff,
1409 - $blog_id,
1410 - $last_entry
1411 - )
1412 - );
1413 - } elseif ( $blog_id > 0 ) {
1414 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1415 - $start_from = $wpdb->get_var(
1416 - $wpdb->prepare(
1417 - "SELECT ID FROM {$wpdb->stream} WHERE `created` < %s AND `blog_id` = %d ORDER BY ID DESC LIMIT 1",
1418 - $cutoff,
1419 - $blog_id
1420 - )
1421 - );
1422 - } elseif ( $last_entry > 0 ) {
1423 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1424 - $start_from = $wpdb->get_var(
1425 - $wpdb->prepare(
1426 - "SELECT ID FROM {$wpdb->stream} WHERE `created` < %s AND `ID` < %d ORDER BY ID DESC LIMIT 1",
1427 - $cutoff,
1428 - $last_entry
1429 - )
1430 - );
1431 - } else {
1432 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1433 - $start_from = $wpdb->get_var(
1434 - $wpdb->prepare(
1435 - "SELECT ID FROM {$wpdb->stream} WHERE `created` < %s ORDER BY ID DESC LIMIT 1",
1436 - $cutoff
1437 - )
1438 - );
1439 - }
1440 -
1441 - if ( empty( $start_from ) ) {
1442 - // Chain is done. Schedule the orphan reaper as the terminal step.
1443 - // The running marker is NOT cleared here: under WP-Cron the reaper
1444 - // event is removed from the cron array before its callback runs,
1445 - // so clearing now would let the overlap guard read "idle" while
1446 - // the reaper's orphan-meta DELETE is still executing. The reaper
1447 - // clears the marker itself when it finishes.
1448 - $this->plugin->scheduler->enqueue_async( self::AUTO_PURGE_REAPER_ACTION, array(), self::AUTO_PURGE_GROUP );
1449 - return;
1450 - }
1451 -
1452 - $start_from = (int) $start_from;
1453 - $window_low = max( 0, $start_from - $batch_size );
1454 -
1455 - // Multi-table DELETE: parent + meta in one statement. Mirrors
1456 - // Admin::erase_large_records().
1457 - if ( $blog_id > 0 ) {
1458 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1459 - $wpdb->query(
1460 - $wpdb->prepare(
1461 - "DELETE `stream`, `meta`
1462 - FROM {$wpdb->stream} AS `stream`
1463 - LEFT JOIN {$wpdb->streammeta} AS `meta`
1464 - ON `meta`.`record_id` = `stream`.`ID`
1465 - WHERE `stream`.`ID` <= %d
1466 - AND `stream`.`ID` >= %d
1467 - AND `stream`.`created` < %s
1468 - AND `stream`.`blog_id` = %d;",
1469 - $start_from,
1470 - $window_low,
1471 - $cutoff,
1472 - $blog_id
1473 - )
1474 - );
1475 - } else {
1476 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1477 - $wpdb->query(
1478 - $wpdb->prepare(
1479 - "DELETE `stream`, `meta`
1480 - FROM {$wpdb->stream} AS `stream`
1481 - LEFT JOIN {$wpdb->streammeta} AS `meta`
1482 - ON `meta`.`record_id` = `stream`.`ID`
1483 - WHERE `stream`.`ID` <= %d
1484 - AND `stream`.`ID` >= %d
1485 - AND `stream`.`created` < %s;",
1486 - $start_from,
1487 - $window_low,
1488 - $cutoff
1489 - )
1490 - );
1491 - }
1492 -
1493 - // Chain the next batch. Pass $window_low as the new upper bound so the
1494 - // next SELECT cannot pick up rows in or above the window we just touched.
1495 - $this->plugin->scheduler->enqueue_async(
1496 - self::AUTO_PURGE_BATCH_ACTION,
1497 - array(
1498 - 'cutoff' => $cutoff,
1499 - 'blog_id' => $blog_id,
1500 - 'last_entry' => $window_low,
1501 - ),
1502 - self::AUTO_PURGE_GROUP
1503 - );
1504 - }
1505 -
1506 - /**
1507 - * Terminal Action Scheduler callback for the auto-purge chain.
1508 - *
1509 - * Runs once per chain (after the last batch) and once when the manual
1510 - * "Clean orphaned meta now" button is used. Cleans up meta rows whose
1511 - * parent stream row is already gone — i.e. residue from historical
1512 - * unbatched purges and from any logger races during a chain.
1513 - *
1514 - * @return void
1515 - */
1516 - public function auto_purge_reaper() {
1517 - // Keep the overlap guard reading "busy" while the orphan-meta DELETE
1518 - // runs. Under WP-Cron the event is removed from the cron array before
1519 - // this callback executes, so without the marker a recurring purge
1520 - // tick or a manual "clean orphaned meta" click could stack parallel
1521 - // work against the same rows. No-op under Action Scheduler, which
1522 - // tracks RUNNING state natively. Self-expires on a fatal.
1523 - $this->plugin->scheduler->mark_running( 'auto_purge' );
1524 -
1525 - $this->delete_orphaned_meta();
1526 -
1527 - $this->plugin->scheduler->mark_done( 'auto_purge' );
1528 - }
1529 -
1530 - /**
1531 - * Ajax handler for the "Clean orphaned meta now" button on
1532 - * Settings → Advanced.
1533 - *
1534 - * Schedules an immediate async run of the orphan reaper. Idempotent:
1535 - * if a reaper is already scheduled, returns without enqueuing a second.
1536 - *
1537 - * Returns true under WP_STREAM_TESTS so PHPUnit can call this directly
1538 - * without exiting the worker.
1539 - *
1540 - * @return bool|void True under tests; otherwise redirects and exits.
1541 - */
1542 - public function wp_ajax_clean_orphan_meta() {
1543 - if ( ! current_user_can( $this->settings_cap ) ) {
1544 - wp_die( esc_html__( 'You do not have permission to do this.', 'stream' ), 403 );
1545 - }
1546 -
1547 - check_ajax_referer( 'stream_nonce_clean_orphan_meta', 'wp_stream_nonce_clean_orphan_meta' );
1548 -
1549 - if ( empty( $this->plugin->scheduler ) ) {
1550 - wp_die( esc_html__( 'No scheduler is available.', 'stream' ), 500 );
1551 - }
1552 -
1553 - // Idempotency: skip enqueue when any auto-purge action is already
1554 - // pending or running. is_running_auto_purge() checks PENDING + RUNNING
1555 - // across the batch worker and the reaper, so a chain that will run
1556 - // its own terminal reaper is not duplicated by a manual click landing
1557 - // in the small CSRF/stale-URL window where the UI link is hidden.
1558 - if ( ! self::is_running_auto_purge() ) {
1559 - $this->plugin->scheduler->enqueue_async( self::AUTO_PURGE_REAPER_ACTION, array(), self::AUTO_PURGE_GROUP );
1560 - }
1561 -
1562 - if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
1563 - return true;
1564 - }
1565 -
1566 - $is_network = $this->plugin->is_multisite_network_activated();
1567 - $page_slug = $is_network ? $this->network->network_settings_page_slug : $this->settings_page_slug;
1568 - $base_url = $is_network ? network_admin_url( $this->admin_parent_page ) : admin_url( $this->admin_parent_page );
1569 -
1570 - wp_safe_redirect(
1571 - add_query_arg(
1572 - array(
1573 - 'page' => $page_slug,
1574 - 'wp_stream_message' => 'orphan_meta_cleanup_scheduled',
1575 - ),
1576 - $base_url
1577 - )
1578 - );
1579 - exit;
1580 - }
1581 -
1582 - /**
1583 - * Render admin notices for post-action redirects.
1584 - *
1585 - * Reads `wp_stream_message` from the query string and renders a matching
1586 - * notice. Used to surface "Clean Orphaned Meta" confirmation after the
1587 - * Ajax handler redirects back to Settings → Advanced.
1588 - *
1589 - * @return void
1590 - */
1591 - public function maybe_display_message() {
1592 - $message = wp_stream_filter_input( INPUT_GET, 'wp_stream_message' );
1593 - if ( empty( $message ) ) {
1594 - return;
1595 - }
1596 -
1597 - $notices = array(
1598 - 'orphan_meta_cleanup_scheduled' => __(
1599 - 'Orphaned meta cleanup scheduled. Progress is visible under Tools → Scheduled Actions.',
1600 - 'stream'
1601 - ),
1602 - );
1603 -
1604 - if ( ! isset( $notices[ $message ] ) ) {
1605 - return;
1606 - }
1607 -
1608 - printf(
1609 - '<div class="notice notice-success is-dismissible"><p>%s</p></div>',
1610 - esc_html( $notices[ $message ] )
1611 - );
1612 - }
1613 -
1614 - /**
1615 - * Returns the admin action links.
1616 - *
1617 736 * @filter plugin_action_links
1618 737 *
1619 - * @param array $links Action links.
1620 - * @param string $file Plugin file.
1621 - *
1622 738 * @return array
1623 739 */
1624 740 public function plugin_action_links( $links, $file ) {
1625 741 if ( plugin_basename( $this->plugin->locations['dir'] . 'stream.php' ) !== $file ) {
@@ -1625,10 +741,10 @@
1625 741 if ( plugin_basename( $this->plugin->locations['dir'] . 'stream.php' ) !== $file ) {
1626 742 return $links;
1627 743 }
1628 744
1629 - // Also don't show links in Network Admin if Stream isn't network enabled.
1630 - if ( is_network_admin() && $this->plugin->is_multisite_not_network_activated() ) {
745 + // Also don't show links in Network Admin if Stream isn't network enabled
746 + if ( is_network_admin() && is_multisite() && ! $this->plugin->is_network_activated() ) {
1631 747 return $links;
1632 748 }
1633 749
1634 750 if ( is_network_admin() ) {
@@ -1646,10 +762,22 @@
1646 762 admin_url( $this->admin_parent_page )
1647 763 );
1648 764 }
1649 765
1650 - $links[] = sprintf( '<a href="%s">%s</a>', esc_url( $admin_page_url ), esc_html__( 'Settings', 'stream' ) );
766 + $links[] = sprintf( '<a href="%s">%s</a>', esc_url( $admin_page_url ), esc_html__( 'Settings', 'default' ) );
1651 767
768 + if ( ! defined( 'DISALLOW_FILE_MODS' ) || false === DISALLOW_FILE_MODS ) {
769 + $url = add_query_arg(
770 + array(
771 + 'action' => 'wp_stream_uninstall',
772 + 'wp_stream_nonce' => wp_create_nonce( 'stream_nonce' ),
773 + ),
774 + admin_url( 'admin-ajax.php' )
775 + );
776 +
777 + $links[] = sprintf( '<span id="wp_stream_uninstall" class="delete"><a href="%s">%s</a></span>', esc_url( $url ), esc_html__( 'Uninstall', 'stream' ) );
778 + }
779 +
1652 780 return $links;
1653 781 }
1654 782
1655 783 /**
@@ -1675,18 +803,10 @@
1675 803 $page_description = apply_filters( 'wp_stream_settings_form_description', '' );
1676 804
1677 805 $sections = $this->plugin->settings->get_fields();
1678 806 $active_tab = wp_stream_filter_input( INPUT_GET, 'tab' );
1679 -
1680 - $this->plugin->enqueue_asset(
1681 - 'settings',
1682 - array(),
1683 - array(
1684 - 'i18n' => array(
1685 - 'confirm_purge' => __( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
1686 - ),
1687 - )
1688 - );
807 + $min = wp_stream_min_suffix();
808 + wp_enqueue_script( 'wp-stream-settings', $this->plugin->locations['url'] . 'ui/js/settings.' . $min . 'js', array( 'jquery' ), $this->plugin->get_version(), true );
1689 809 ?>
1690 810 <div class="wrap">
1691 811 <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
1692 812
@@ -1699,9 +819,9 @@
1699 819 <?php if ( count( $sections ) > 1 ) : ?>
1700 820 <h2 class="nav-tab-wrapper">
1701 821 <?php $i = 0; ?>
1702 822 <?php foreach ( $sections as $section => $data ) : ?>
1703 - <?php ++$i; ?>
823 + <?php $i++; ?>
1704 824 <?php $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section ); ?>
1705 825 <a href="<?php echo esc_url( add_query_arg( 'tab', $section ) ); ?>" class="nav-tab <?php echo $is_active ? esc_attr( ' nav-tab-active' ) : ''; ?>">
1706 826 <?php echo esc_html( $data['title'] ); ?>
1707 827 </a>
@@ -1714,9 +834,9 @@
1714 834 <div class="settings-sections">
1715 835 <?php
1716 836 $i = 0;
1717 837 foreach ( $sections as $section => $data ) {
1718 - ++$i;
838 + $i++;
1719 839
1720 840 $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section );
1721 841
1722 842 if ( $is_active ) {
@@ -1747,31 +867,27 @@
1747 867
1748 868 /**
1749 869 * Check if a particular role has access
1750 870 *
1751 - * The user_has_cap/role_has_cap filters that call this are registered in the
1752 - * constructor, but the Settings object is not constructed until init priority 9.
1753 - * A capability check fired before then (e.g. by a security plugin evaluating
1754 - * firewall rules on plugins_loaded) must be denied rather than fatal on the
1755 - * null options chain.
871 + * @param string $role
1756 872 *
1757 - * @param string $role User role.
1758 - *
1759 873 * @return bool
1760 874 */
1761 875 private function role_can_view( $role ) {
1762 - $allowed_roles = $this->plugin->settings->options['general_role_access'] ?? array();
876 + if ( in_array( $role, $this->plugin->settings->options['general_role_access'], true ) ) {
877 + return true;
878 + }
1763 879
1764 - return in_array( $role, (array) $allowed_roles, true );
880 + return false;
1765 881 }
1766 882
1767 883 /**
1768 884 * Filter user caps to dynamically grant our view cap based on allowed roles
1769 885 *
1770 - * @param array $allcaps All capabilities.
1771 - * @param array $caps Required caps.
1772 - * @param array $args Unused.
1773 - * @param WP_User $user User.
886 + * @param $allcaps
887 + * @param $caps
888 + * @param $args
889 + * @param $user
1774 890 *
1775 891 * @filter user_has_cap
1776 892 *
1777 893 * @return array
@@ -1816,11 +932,11 @@
1816 932 * Filter role caps to dynamically grant our view cap based on allowed roles
1817 933 *
1818 934 * @filter role_has_cap
1819 935 *
1820 - * @param array $allcaps All capabilities.
1821 - * @param string $cap Require cap.
1822 - * @param string $role User role.
936 + * @param $allcaps
937 + * @param $cap
938 + * @param $role
1823 939 *
1824 940 * @return array
1825 941 */
1826 942 public function filter_role_caps( $allcaps, $cap, $role ) {
@@ -1833,10 +949,8 @@
1833 949 return $allcaps;
1834 950 }
1835 951
1836 952 /**
1837 - * Ajax callback for return a user list.
1838 - *
1839 953 * @action wp_ajax_wp_stream_filters
1840 954 */
1841 955 public function ajax_filters() {
1842 956 if ( ! defined( 'DOING_AJAX' ) || ! current_user_can( $this->plugin->admin->settings_cap ) ) {
@@ -1870,9 +984,9 @@
1870 984 if ( count( $users ) > $this->preload_users_max ) {
1871 985 $users = array_slice( $users, 0, $this->preload_users_max );
1872 986 }
1873 987
1874 - // Get gravatar / roles for final result set.
988 + // Get gravatar / roles for final result set
1875 989 $results = $this->get_users_record_meta( $users );
1876 990
1877 991 break;
1878 992 }
@@ -1877,20 +991,14 @@
1877 991 break;
1878 992 }
1879 993
1880 994 if ( isset( $results ) ) {
1881 - echo wp_json_encode( $results );
995 + echo wp_stream_json_encode( $results ); // xss ok
1882 996 }
1883 997
1884 998 die();
1885 999 }
1886 1000
1887 - /**
1888 - * Return relevant user meta data.
1889 - *
1890 - * @param array $authors Author data.
1891 - * @return array
1892 - */
1893 1001 public function get_users_record_meta( $authors ) {
1894 1002 $authors_records = array();
1895 1003
1896 1004 foreach ( $authors as $user_id => $args ) {
@@ -1910,15 +1018,19 @@
1910 1018
1911 1019 /**
1912 1020 * Get user meta in a way that is also safe for VIP
1913 1021 *
1914 - * @param int $user_id User ID.
1915 - * @param string $meta_key Meta key.
1916 - * @param bool $single Return first found meta value connected to the meta key (optional).
1022 + * @param int $user_id
1023 + * @param string $meta_key
1024 + * @param bool $single (optional)
1917 1025 *
1918 1026 * @return mixed
1919 1027 */
1920 1028 public function get_user_meta( $user_id, $meta_key, $single = true ) {
1029 + if ( wp_stream_is_vip() && function_exists( 'get_user_attribute' ) ) {
1030 + return get_user_attribute( $user_id, $meta_key );
1031 + }
1032 +
1921 1033 return get_user_meta( $user_id, $meta_key, $single );
1922 1034 }
1923 1035
1924 1036 /**
@@ -1923,16 +1035,20 @@
1923 1035
1924 1036 /**
1925 1037 * Update user meta in a way that is also safe for VIP
1926 1038 *
1927 - * @param int $user_id User ID.
1928 - * @param string $meta_key Meta key.
1929 - * @param mixed $meta_value Meta value.
1930 - * @param mixed $prev_value Previous meta value being overwritten (optional).
1039 + * @param int $user_id
1040 + * @param string $meta_key
1041 + * @param mixed $meta_value
1042 + * @param mixed $prev_value (optional)
1931 1043 *
1932 1044 * @return int|bool
1933 1045 */
1934 1046 public function update_user_meta( $user_id, $meta_key, $meta_value, $prev_value = '' ) {
1047 + if ( wp_stream_is_vip() && function_exists( 'update_user_attribute' ) ) {
1048 + return update_user_attribute( $user_id, $meta_key, $meta_value );
1049 + }
1050 +
1935 1051 return update_user_meta( $user_id, $meta_key, $meta_value, $prev_value );
1936 1052 }
1937 1053
1938 1054 /**
@@ -1937,14 +1053,18 @@
1937 1053
1938 1054 /**
1939 1055 * Delete user meta in a way that is also safe for VIP
1940 1056 *
1941 - * @param int $user_id User ID.
1942 - * @param string $meta_key Meta key.
1943 - * @param mixed $meta_value Meta value (optional).
1057 + * @param int $user_id
1058 + * @param string $meta_key
1059 + * @param mixed $meta_value (optional)
1944 1060 *
1945 1061 * @return bool
1946 1062 */
1947 1063 public function delete_user_meta( $user_id, $meta_key, $meta_value = '' ) {
1064 + if ( wp_stream_is_vip() && function_exists( 'delete_user_attribute' ) ) {
1065 + return delete_user_attribute( $user_id, $meta_key, $meta_value );
1066 + }
1067 +
1948 1068 return delete_user_meta( $user_id, $meta_key, $meta_value );
1949 1069 }
1950 1070 }