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 +246 -867 4.2.23.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,54 +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 - * Holds Instance of plugin object
58 - *
59 16 * @var Plugin
60 17 */
61 18 public $plugin;
62 19
@@ -137,13 +94,13 @@
137 94 */
138 95 public $view_cap = 'view_stream';
139 96
140 97 /**
141 - * Capability name for managing settings
98 + * Capability name for viewing settings
142 99 *
143 100 * @var string
144 101 */
145 - public $settings_cap = WP_STREAM_SETTINGS_CAPABILITY;
102 + public $settings_cap = 'manage_options';
146 103
147 104 /**
148 105 * Total amount of authors to pre-load
149 106 *
@@ -160,9 +117,9 @@
160 117
161 118 /**
162 119 * Class constructor.
163 120 *
164 - * @param Plugin $plugin Instance of plugin object.
121 + * @param Plugin $plugin The main Plugin class.
165 122 */
166 123 public function __construct( $plugin ) {
167 124 $this->plugin = $plugin;
168 125
@@ -176,9 +133,9 @@
176 133 // User and role caps.
177 134 add_filter( 'user_has_cap', array( $this, 'filter_user_caps' ), 10, 4 );
178 135 add_filter( 'role_has_cap', array( $this, 'filter_role_caps' ), 10, 3 );
179 136
180 - if ( $this->plugin->is_multisite_network_activated() && ! is_network_admin() ) {
137 + if ( is_multisite() && $plugin->is_network_activated() && ! is_network_admin() ) {
181 138 $options = (array) get_site_option( 'wp_stream_network', array() );
182 139 $option = isset( $options['general_site_access'] ) ? absint( $options['general_site_access'] ) : 1;
183 140
184 141 $this->disable_access = ( $option ) ? false : true;
@@ -192,11 +149,8 @@
192 149 // Admin notices.
193 150 add_action( 'admin_notices', array( $this, 'prepare_admin_notices' ) );
194 151 add_action( 'shutdown', array( $this, 'admin_notices' ) );
195 152
196 - // Feature request notice.
197 - add_action( 'admin_notices', array( $this, 'display_feature_request_notice' ) );
198 -
199 153 // Add admin body class.
200 154 add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
201 155
202 156 // Plugin action links.
@@ -228,35 +182,20 @@
228 182 'wp_ajax_reset',
229 183 )
230 184 );
231 185
232 - // Manual "Clean orphaned meta now" action (Settings → Advanced).
233 - add_action(
234 - 'wp_ajax_wp_stream_clean_orphan_meta',
235 - array( $this, 'wp_ajax_clean_orphan_meta' )
236 - );
186 + // Uninstall Streams and Deactivate plugin.
187 + $uninstall = $this->plugin->db->driver->purge_storage( $this->plugin );
237 188
238 - // Render confirmation notices keyed by the wp_stream_message query
239 - // arg set on post-action redirects (e.g. orphan_meta_cleanup_scheduled).
240 - add_action( 'admin_notices', array( $this, 'maybe_display_message' ) );
241 - add_action( 'network_admin_notices', array( $this, 'maybe_display_message' ) );
242 -
243 - // Auto purge setup (Action Scheduler).
189 + // Auto purge setup.
244 190 add_action( 'wp_loaded', array( $this, 'purge_schedule_setup' ) );
245 191 add_action(
246 - self::AUTO_PURGE_ACTION,
247 - array( $this, 'purge_scheduled_action' )
192 + 'wp_stream_auto_purge',
193 + array(
194 + $this,
195 + 'purge_scheduled_action',
196 + )
248 197 );
249 - add_action(
250 - self::AUTO_PURGE_BATCH_ACTION,
251 - array( $this, 'auto_purge_batch' ),
252 - 10,
253 - 3
254 - );
255 - add_action(
256 - self::AUTO_PURGE_REAPER_ACTION,
257 - array( $this, 'auto_purge_reaper' )
258 - );
259 198
260 199 // Ajax users list.
261 200 add_action(
262 201 'wp_ajax_wp_stream_filters',
@@ -264,19 +203,8 @@
264 203 $this,
265 204 'ajax_filters',
266 205 )
267 206 );
268 -
269 - // Async action for erasing large log tables.
270 - add_action(
271 - self::ASYNC_DELETION_ACTION,
272 - array(
273 - $this,
274 - 'erase_large_records',
275 - ),
276 - 10,
277 - 4
278 - );
279 207 }
280 208
281 209 /**
282 210 * Load admin classes
@@ -286,14 +214,8 @@
286 214 public function init() {
287 215 $this->network = new Network( $this->plugin );
288 216 $this->live_update = new Live_Update( $this->plugin );
289 217 $this->export = new Export( $this->plugin );
290 -
291 - // Check if the host has configured the `REMOTE_ADDR` correctly.
292 - $client_ip = $this->plugin->get_client_ip_address();
293 - if ( empty( $client_ip ) && $this->is_stream_screen() ) {
294 - $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' ) );
295 - }
296 218 }
297 219
298 220 /**
299 221 * Output specific updates passed as URL parameters.
@@ -319,9 +241,9 @@
319 241 * @param bool $is_error If the message is error_level (true) or warning (false).
320 242 */
321 243 public function notice( $message, $is_error = true ) {
322 244 if ( defined( 'WP_CLI' ) && WP_CLI ) {
323 - $message = wp_strip_all_tags( $message );
245 + $message = strip_tags( $message );
324 246
325 247 if ( $is_error ) {
326 248 WP_CLI::warning( $message );
327 249 } else {
@@ -369,28 +291,8 @@
369 291 }
370 292 }
371 293
372 294 /**
373 - * Display a feature request notice.
374 - *
375 - * @return void
376 - */
377 - public function display_feature_request_notice() {
378 - $screen = get_current_screen();
379 -
380 - // Display the notice only on the Stream settings page.
381 - if ( empty( $this->screen_id['settings'] ) || $this->screen_id['settings'] !== $screen->id ) {
382 - return;
383 - }
384 -
385 - printf(
386 - '<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>',
387 - esc_html__( 'Have suggestions or found a bug?', 'stream' ),
388 - esc_html__( 'Click here to let us know!', 'stream' )
389 - );
390 - }
391 -
392 - /**
393 295 * Register menu page
394 296 *
395 297 * @action admin_menu
396 298 *
@@ -425,9 +327,9 @@
425 327 $main_menu_title,
426 328 $this->view_cap,
427 329 $this->records_page_slug,
428 330 array( $this, 'render_list_table' ),
429 - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIGZpbGw9IjAwMCI+Cgk8cGF0aCBkPSJNOTAzLjExNSA1MTUuNDEzYy00OS4zOTIgMC05MS40NzQgMzEuMzM3LTEwNy40NiA3NS4yMDNsLTEyNC40MTEtMS41MzJjLTExLjM3Ny0uMzQ2LTIyLjc1MS0uNjg5LTM0LjEyOS0uOTk4bC0uMjQxLjU3NC0yMi40MzYtLjI3OC0uMTUzLS45Mi0xNS4wNTYtODIuOTMzLTIwLjE0Ni0xMDguNDA1LTIwLjU0NC0xMDguMzM3TDUwMy45ODIgMGwtNTMuMTQxIDQyOS4wMTMtMTYuMjE0IDEzNy45MzUtMTIuMDE2IDEwNi45MjQtMTE3LjI4Ni0yODUuMjItMTguMzUzIDIwMi44MWMtNDIuNTYyIDEuNDU0LTg1LjEyNyAyLjkzNC0xMjcuNjg4IDQuNzM4LTUzLjA5NyAyLjI5Mi0xMDYuMTg3IDQuNDczLTE1OS4yODQgNy41MzZ2NDIuMDQyYzUzLjA5NyAzLjA2IDEwNi4xODcgNS4yNDcgMTU5LjI4NCA3LjUzMyA1My4wOTMgMi4yNDUgMTA2LjE4IDQuMTk0IDE1OS4yNzMgNS45MDNsMTQuMjQuNDY1IDE3LjM1MSA0OC4zOWMxOC44NDIgNTEuODc0IDM3LjU0MiAxMDMuODA2IDU2Ljc2NSAxNTUuNTQxTDQ2Ni41MiAxMDI0bDQxLjUxMi0zMDguMjkzIDE3LjYzMy0xMzYuNjg1IDEwLjc3NiA1MC4zMjkgNTQuODE1IDI0OC41NDQgNzIuNTE2LTIxNy4yMTdoMTI5LjI2MWMxMy40OTMgNDguMTIxIDU3LjY1NSA4My40MjkgMTEwLjA3NSA4My40MjkgNjMuMTYgMCAxMTQuMzUyLTUxLjIwNSAxMTQuMzUyLTExNC4zNDggMC02My4xMzktNTEuMTg5LTExNC4zNDUtMTE0LjM0OS0xMTQuMzQ1bC4wMDQtLjAwMVoiIC8+Cjwvc3ZnPgo=',
331 + 'div',
430 332 $main_menu_position
431 333 );
432 334
433 335 /**
@@ -477,55 +379,93 @@
477 379 * Enqueue scripts/styles for admin screen
478 380 *
479 381 * @action admin_enqueue_scripts
480 382 *
481 - * @param string $hook Current hook.
383 + * @param string $hook
482 384 *
483 385 * @return void
484 386 */
485 387 public function admin_enqueue_scripts( $hook ) {
486 - if ( in_array( $hook, $this->screen_id, true ) ) {
487 - $this->plugin->enqueue_asset(
488 - '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',
489 416 array(
490 - $this->plugin->with_select2(),
491 - $this->plugin->with_jquery_timeago(),
417 + 'jquery',
418 + 'wp-stream-select2',
492 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',
493 425 array(
494 - 'i18n' => array(
495 - 'confirm_purge' => __( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
496 - 'confirm_defaults' => __( 'Are you sure you want to reset all site settings to default? This cannot be undone.', 'stream' ),
497 - ),
498 - 'locale' => strtolower( substr( get_locale(), 0, 2 ) ),
499 - 'gmt_offset' => get_option( 'gmt_offset' ),
500 - )
426 + 'jquery',
427 + 'wp-stream-select2',
428 + ),
429 + $this->plugin->get_version()
501 430 );
502 -
503 - $this->plugin->enqueue_asset(
504 - 'admin-exclude',
431 + wp_enqueue_script(
432 + 'wp-stream-live-updates',
433 + $this->plugin->locations['url'] . 'ui/js/live-updates.' . $min . 'js',
505 434 array(
506 - $this->plugin->with_select2(),
435 + 'jquery',
436 + 'heartbeat',
507 437 ),
438 + $this->plugin->get_version()
439 + );
440 +
441 + wp_localize_script(
442 + 'wp-stream-admin',
443 + 'wp_stream',
508 444 array(
509 - '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' ),
510 452 )
511 453 );
512 454
513 - $current_order = isset( $_GET['order'] ) ? sanitize_key( wp_unslash( $_GET['order'] ) ) : 'desc'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
514 - if ( ! in_array( $current_order, array( 'asc', 'desc' ), true ) ) {
515 - $current_order = 'desc';
516 - }
517 - $current_query = map_deep( wp_unslash( $_GET ), 'sanitize_text_field' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
518 -
519 - $this->plugin->enqueue_asset(
520 - 'live-updates',
521 - array( 'heartbeat' ),
455 + wp_localize_script(
456 + 'wp-stream-live-updates',
457 + 'wp_stream_live_updates',
522 458 array(
523 459 'current_screen' => $hook,
524 - 'current_page' => isset( $_GET['paged'] ) ? absint( wp_unslash( $_GET['paged'] ) ) : '1', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
525 - 'current_order' => $current_order,
526 - 'current_query' => wp_json_encode( $current_query ),
527 - '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
528 468 )
529 469 );
530 470 }
531 471
@@ -542,16 +482,17 @@
542 482 * @return int
543 483 */
544 484 $bulk_actions_threshold = apply_filters( 'wp_stream_bulk_actions_threshold', 100 );
545 485
546 - $this->plugin->enqueue_asset(
547 - 'global',
548 - 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',
549 490 array(
550 491 'bulk_actions' => array(
551 492 'i18n' => array(
552 - /* translators: %s: a number of items (e.g. "1,742") */
553 - '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 ) ) ),
554 495 ),
555 496 'threshold' => absint( $bulk_actions_threshold ),
556 497 ),
557 498 'plugins_screen_url' => self_admin_url( 'plugins.php#stream' ),
@@ -564,23 +505,17 @@
564 505 *
565 506 * @return bool
566 507 */
567 508 public function is_stream_screen() {
568 - if ( ! is_admin() ) {
569 - return false;
509 + if ( is_admin() && false !== strpos( wp_stream_filter_input( INPUT_GET, 'page' ), $this->records_page_slug ) ) {
510 + return true;
570 511 }
571 512
572 - $page = wp_stream_filter_input( INPUT_GET, 'page' );
573 - 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 ) {
574 515 return true;
575 516 }
576 517
577 - if ( is_admin() && function_exists( 'get_current_screen' ) ) {
578 - $screen = get_current_screen();
579 -
580 - return ( Alerts::POST_TYPE === $screen->post_type );
581 - }
582 -
583 518 return false;
584 519 }
585 520
586 521 /**
@@ -585,9 +520,9 @@
585 520
586 521 /**
587 522 * Add a specific body class to all Stream admin screens
588 523 *
589 - * @param string $classes CSS classes to output to body.
524 + * @param string $classes CSS classes to output to body
590 525 *
591 526 * @filter admin_body_class
592 527 *
593 528 * @return string
@@ -597,10 +532,10 @@
597 532
598 533 if ( $this->is_stream_screen() ) {
599 534 $stream_classes[] = $this->admin_body_class;
600 535
601 - if ( isset( $_GET['page'] ) ) { // // phpcs:ignore WordPress.Security.NonceVerification.Recommended
602 - $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
603 538 }
604 539 }
605 540
606 541 /**
@@ -614,14 +549,20 @@
614 549 return sprintf( '%s %s ', $classes, $stream_classes );
615 550 }
616 551
617 552 /**
618 - * Add menu styles for various WP Admin skins.
553 + * Add menu styles for various WP Admin skins
619 554 *
555 + * @uses \wp_add_inline_style()
556 + *
620 557 * @action admin_enqueue_scripts
621 558 */
622 559 public function admin_menu_css() {
623 - // 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
624 565 if ( ! file_exists( ABSPATH . WPINC . '/version.php' ) ) {
625 566 return;
626 567 }
627 568 include ABSPATH . WPINC . '/version.php';
@@ -629,29 +570,61 @@
629 570 if ( ! isset( $wp_version ) ) {
630 571 return;
631 572 }
632 573
633 - $css = "
634 - body.{$this->admin_body_class} #wpbody-content .wrap h1:nth-child(1):before {
635 - content: '';
636 - display: inline-block;
637 - width: 24px;
638 - height: 24px;
639 - margin-right: 8px;
640 - vertical-align: text-bottom;
641 - background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIGZpbGw9ImN1cnJlbnRjb2xvciI+Cgk8cGF0aCBkPSJNOTAzLjExNSA1MTUuNDEzYy00OS4zOTIgMC05MS40NzQgMzEuMzM3LTEwNy40NiA3NS4yMDNsLTEyNC40MTEtMS41MzJjLTExLjM3Ny0uMzQ2LTIyLjc1MS0uNjg5LTM0LjEyOS0uOTk4bC0uMjQxLjU3NC0yMi40MzYtLjI3OC0uMTUzLS45Mi0xNS4wNTYtODIuOTMzLTIwLjE0Ni0xMDguNDA1LTIwLjU0NC0xMDguMzM3TDUwMy45ODIgMGwtNTMuMTQxIDQyOS4wMTMtMTYuMjE0IDEzNy45MzUtMTIuMDE2IDEwNi45MjQtMTE3LjI4Ni0yODUuMjItMTguMzUzIDIwMi44MWMtNDIuNTYyIDEuNDU0LTg1LjEyNyAyLjkzNC0xMjcuNjg4IDQuNzM4LTUzLjA5NyAyLjI5Mi0xMDYuMTg3IDQuNDczLTE1OS4yODQgNy41MzZ2NDIuMDQyYzUzLjA5NyAzLjA2IDEwNi4xODcgNS4yNDcgMTU5LjI4NCA3LjUzMyA1My4wOTMgMi4yNDUgMTA2LjE4IDQuMTk0IDE1OS4yNzMgNS45MDNsMTQuMjQuNDY1IDE3LjM1MSA0OC4zOWMxOC44NDIgNTEuODc0IDM3LjU0MiAxMDMuODA2IDU2Ljc2NSAxNTUuNTQxTDQ2Ni41MiAxMDI0bDQxLjUxMi0zMDguMjkzIDE3LjYzMy0xMzYuNjg1IDEwLjc3NiA1MC4zMjkgNTQuODE1IDI0OC41NDQgNzIuNTE2LTIxNy4yMTdoMTI5LjI2MWMxMy40OTMgNDguMTIxIDU3LjY1NSA4My40MjkgMTEwLjA3NSA4My40MjkgNjMuMTYgMCAxMTQuMzUyLTUxLjIwNSAxMTQuMzUyLTExNC4zNDggMC02My4xMzktNTEuMTg5LTExNC4zNDUtMTE0LjM0OS0xMTQuMzQ1bC4wMDQtLjAwMVoiIC8+Cjwvc3ZnPgo=');
642 - }
643 - #menu-posts-feedback .wp-menu-image:before {
644 - font-family: dashicons !important;
645 - content: '\\f175';
646 - }
647 - #adminmenu #menu-posts-feedback div.wp-menu-image {
648 - background: none !important;
649 - background-repeat: no-repeat;
650 - }
651 - ";
574 + $body_class = $this->admin_body_class;
575 + $records_page = $this->records_page_slug;
576 + $stream_url = $this->plugin->locations['url'];
652 577
653 - 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 );
654 627 }
655 628
656 629 /**
657 630 * Handle the reset AJAX request to reset logs.
@@ -666,14 +639,8 @@
666 639 esc_html__( "You don't have sufficient privileges to do this action.", 'stream' )
667 640 );
668 641 }
669 642
670 - // Ensure the database tables exist before attempting to clear records.
671 - // Install::check() short-circuits on DOING_AJAX, so call install()
672 - // directly. dbDelta is idempotent and safe to run when tables already
673 - // exist.
674 - $this->plugin->install->install( $this->plugin->get_version() );
675 -
676 643 $this->erase_stream_records();
677 644
678 645 if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
679 646 return true;
@@ -678,9 +645,9 @@
678 645 if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
679 646 return true;
680 647 }
681 648
682 - wp_safe_redirect(
649 + wp_redirect(
683 650 add_query_arg(
684 651 array(
685 652 'page' => is_network_admin() ? $this->network->network_settings_page_slug : $this->settings_page_slug,
686 653 'message' => 'data_erased',
@@ -691,680 +658,84 @@
691 658
692 659 exit;
693 660 }
694 661
695 - /**
696 - * Clears stream records from the database.
697 - *
698 - * @return void
699 - */
700 662 private function erase_stream_records() {
701 663 global $wpdb;
702 664
703 - // If this is a multisite and it's not network activated,
704 - // only delete the entries from the blog which made the request.
705 - if ( $this->plugin->is_multisite_not_network_activated() ) {
665 + $where = '';
706 666
707 - // First check the log size.
708 - $stream_log_size = self::get_blog_record_table_size();
709 -
710 - // If this is a large log and we need to delete only the entries
711 - // pertaining to an individual site, we will need to do those in batches.
712 - if ( $this->plugin->is_large_records_table( $stream_log_size ) ) {
713 - $this->schedule_erase_large_records( $stream_log_size );
714 - return;
715 - }
716 -
717 - $wpdb->query(
718 - $wpdb->prepare(
719 - "DELETE `stream`, `meta`
720 - FROM {$wpdb->stream} AS `stream`
721 - LEFT JOIN {$wpdb->streammeta} AS `meta`
722 - ON `meta`.`record_id` = `stream`.`ID`
723 - WHERE `blog_id`=%d;",
724 - get_current_blog_id()
725 - )
726 - );
727 - } else {
728 - // If we are deleting all the entries, we can truncate the tables.
729 - $wpdb->query( "TRUNCATE {$wpdb->streammeta};" );
730 - $wpdb->query( "TRUNCATE {$wpdb->stream};" );
731 - // Tidy up any meta which may have been added in between the two truncations.
732 - $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() );
733 669 }
734 - }
735 670
736 - /**
737 - * Schedule the initial event to start erasing the logs from now.
738 - *
739 - * @param int $log_size The number of rows which will be affected.
740 - * @return void
741 - */
742 - private function schedule_erase_large_records( int $log_size ) {
743 - global $wpdb;
744 -
745 - $last_entry = $wpdb->get_var(
746 - $wpdb->prepare(
747 - "SELECT ID FROM {$wpdb->stream} WHERE `blog_id`=%d ORDER BY ID DESC LIMIT 1",
748 - get_current_blog_id()
749 - )
750 - );
751 -
752 - // If there are no entries to erase, don't try to erase them.
753 - if ( empty( $last_entry ) ) {
754 - return;
755 - }
756 -
757 - // We are going to delete this many and this many only.
758 - // This is to avoid the situation where rows keep getting added
759 - // between the Action Scheduler runs and they never stop.
760 - $args = array(
761 - 'total' => (int) $log_size,
762 - 'done' => 0,
763 - 'last_entry' => (int) $last_entry,
764 - 'blog_id' => (int) get_current_blog_id(),
765 - );
766 -
767 - as_enqueue_async_action( self::ASYNC_DELETION_ACTION, $args );
768 - }
769 -
770 - /**
771 - * Checks if the async deletion process is running.
772 - *
773 - * @return bool True if the async deletion process is running, false otherwise.
774 - */
775 - public static function is_running_async_deletion() {
776 - return as_has_scheduled_action( self::ASYNC_DELETION_ACTION );
777 - }
778 -
779 - /**
780 - * Checks if any auto-purge action is currently scheduled or in-flight.
781 - *
782 - * Returns true when either the batched chain worker or the terminal
783 - * orphan reaper is pending OR running. The recurring scheduler is
784 - * intentionally excluded — it is always pending under normal operation,
785 - * so including it here would make the probe useless. Used by the
786 - * Settings → Advanced UI to render an "Auto-purge currently running"
787 - * notice and by the recurring callback as an overlap guard.
788 - *
789 - * Checks both PENDING and IN-PROGRESS statuses so a chain that is
790 - * mid-execution (e.g. the batch worker is currently running and has not
791 - * yet enqueued the next batch) still reports as running. Without the
792 - * RUNNING check the overlap guard can let a second parallel chain stack
793 - * against the same rows.
794 - *
795 - * @return bool
796 - */
797 - public static function is_running_auto_purge() {
798 - if ( ! function_exists( 'as_get_scheduled_actions' ) ) {
799 - return false;
800 - }
801 -
802 - foreach ( array( self::AUTO_PURGE_BATCH_ACTION, self::AUTO_PURGE_REAPER_ACTION ) as $hook ) {
803 - $found = as_get_scheduled_actions(
804 - array(
805 - 'hook' => $hook,
806 - 'status' => array(
807 - \ActionScheduler_Store::STATUS_PENDING,
808 - \ActionScheduler_Store::STATUS_RUNNING,
809 - ),
810 - 'per_page' => 1,
811 - ),
812 - 'ids'
813 - );
814 - if ( ! empty( $found ) ) {
815 - return true;
816 - }
817 - }
818 -
819 - return false;
820 - }
821 -
822 - /**
823 - * Erases large records from the stream table.
824 - *
825 - * This function deletes records from the stream table in batches, starting from a given entry ID.
826 - * It deletes records in reverse chronological order, starting from the largest ID and going back.
827 - * The number of records deleted in each batch is determined by the batch size, which can be filtered
828 - * using the 'wp_stream_batch_size' hook.
829 - *
830 - * @param int $total The total number of records to be deleted.
831 - * @param int $done The number of records that have already been deleted.
832 - * @param int $last_entry The ID of the last entry that was deleted.
833 - * @param int $blog_id The ID of the blog for which the records should be deleted.
834 - * @return void
835 - */
836 - public function erase_large_records( int $total, int $done, int $last_entry, int $blog_id ) {
837 - global $wpdb;
838 -
839 - $start_from = $wpdb->get_var(
840 - $wpdb->prepare(
841 - "SELECT ID FROM {$wpdb->stream} WHERE ID < %d AND `blog_id`=%d ORDER BY ID DESC LIMIT 1",
842 - $last_entry + 1, // A tweak to get it correct the first time through.
843 - get_current_blog_id()
844 - )
845 - );
846 -
847 - if ( empty( $start_from ) ) {
848 - return;
849 - }
850 -
851 - /**
852 - * Filters the number of records in the {$wpdb->stream} table to do at a time.
853 - *
854 - * @since 4.1.0
855 - *
856 - * @param int $batch_size The batch size, default 250000.
857 - */
858 - $batch_size = apply_filters( 'wp_stream_batch_size', 250000 );
859 -
860 - // This will tend to erase them in reverse chronological order,
861 - // ie it will start from the largest ID and go back from there.
862 671 $wpdb->query(
863 - $wpdb->prepare(
864 - "DELETE `stream`, `meta`
865 - FROM {$wpdb->stream} AS `stream`
866 - LEFT JOIN {$wpdb->streammeta} AS `meta`
867 - ON `meta`.`record_id` = `stream`.`ID`
868 - WHERE ID <= %d AND ID >= %d AND `blog_id`=%d;",
869 - $start_from,
870 - $start_from - $batch_size,
871 - get_current_blog_id()
872 - )
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
873 677 );
874 -
875 - $remaining = $wpdb->get_var(
876 - $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->stream} WHERE `blog_id`=%d", $blog_id )
877 - );
878 -
879 - $done = $total - $remaining;
880 -
881 - as_enqueue_async_action(
882 - self::ASYNC_DELETION_ACTION,
883 - array(
884 - 'total' => (int) $total,
885 - 'done' => (int) $done,
886 - 'last_entry' => (int) $start_from - $batch_size, // The last ID checked.
887 - 'blog_id' => (int) $blog_id,
888 - )
889 - );
890 678 }
891 679
892 - /**
893 - * Retrieves the size of the blog record table for a specific blog.
894 - *
895 - * @param int|null $blog_id The ID of the blog. If not provided, the current blog ID will be used.
896 - * @return int The size of the blog record table.
897 - */
898 - public static function get_blog_record_table_size( $blog_id = null ): int {
899 - global $wpdb;
900 -
901 - $blog_id = empty( $blog_id ) ? get_current_blog_id() : $blog_id;
902 -
903 - $blog_size = $wpdb->get_var(
904 - $wpdb->prepare(
905 - "SELECT COUNT(ID) FROM {$wpdb->stream} WHERE `blog_id`=%d",
906 - $blog_id
907 - )
908 - );
909 -
910 - return (int) $blog_size;
911 - }
912 -
913 - /**
914 - * Schedules a purge of records.
915 - *
916 - * @return void
917 - */
918 680 public function purge_schedule_setup() {
919 - // Clear the legacy WP-Cron event scheduled by Stream <= 4.1.x so it
920 - // cannot double-fire alongside the new AS recurring action.
921 - if ( wp_next_scheduled( 'wp_stream_auto_purge' ) ) {
922 - 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' );
923 683 }
924 -
925 - if ( ! function_exists( 'as_schedule_recurring_action' ) ) {
926 - // Action Scheduler not yet loaded (e.g. very early hook); bail.
927 - // Plugin::__construct() loads it before init, so this should be unreachable.
928 - return;
929 - }
930 -
931 - if ( false === as_next_scheduled_action( self::AUTO_PURGE_ACTION ) ) {
932 - // 12 hours == old `twicedaily` interval.
933 - as_schedule_recurring_action(
934 - time(),
935 - 12 * HOUR_IN_SECONDS,
936 - self::AUTO_PURGE_ACTION,
937 - array(),
938 - self::AUTO_PURGE_GROUP
939 - );
940 - }
941 684 }
942 685
943 - /**
944 - * Deletes orphaned meta records from the database.
945 - *
946 - * Deletes meta records from the stream meta table where the corresponding
947 - * stream record no longer exists.
948 - *
949 - * @global wpdb $wpdb The WordPress database object.
950 - */
951 - protected function delete_orphaned_meta() {
686 + public function purge_scheduled_action() {
952 687 global $wpdb;
953 688
954 - $wpdb->query(
955 - "DELETE `meta` FROM {$wpdb->streammeta} as `meta` LEFT JOIN {$wpdb->stream} as `stream` ON `stream`.`ID`=`meta`.`record_id` WHERE `stream`.`ID` IS NULL"
956 - );
957 - }
958 -
959 - /**
960 - * Executes a scheduled purge
961 - *
962 - * @return void
963 - */
964 - public function purge_scheduled_action() {
965 - // 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
966 690 if (
967 - $this->plugin->is_multisite_not_network_activated()
691 + is_multisite()
968 692 &&
969 693 is_network_admin()
694 + &&
695 + ! $this->plugin->is_network_activated()
970 696 ) {
971 697 return;
972 698 }
973 699
974 - $defaults = $this->plugin->settings->get_defaults();
975 - if ( $this->plugin->is_multisite_network_activated() ) {
976 - $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() );
977 702 } else {
978 - $options = wp_parse_args( (array) get_option( 'wp_stream', array() ), $defaults );
703 + $options = (array) get_option( 'wp_stream', array() );
979 704 }
980 705
981 - // TTL fallback. Settings::get_defaults() runs every settings field
982 - // through the `wp_stream_settings_option_fields` filter, which
983 - // Network::get_network_admin_fields() uses to strip the `records_ttl`
984 - // field from the per-site option's defaults set. When this callback runs
985 - // outside any admin context (Action Scheduler, WP-CLI, system cron), the
986 - // per-site option_key is in effect, so the filtered defaults array does
987 - // not contain general_records_ttl at all. Apply the documented 30-day
988 - // default (classes/class-settings.php, `records_ttl` field) only when
989 - // the key is genuinely missing, so an operator who set the value via
990 - // CLI/SQL keeps their explicit choice.
991 - if ( ! isset( $options['general_records_ttl'] ) ) {
992 - $options['general_records_ttl'] = 30;
993 - }
994 -
995 - if ( ! empty( $options['general_keep_records_indefinitely'] ) ) {
706 + if ( ! empty( $options['general_keep_records_indefinitely'] ) || ! isset( $options['general_records_ttl'] ) ) {
996 707 return;
997 708 }
998 709
999 - // Refuse to purge with a non-positive TTL. The UI enforces min=1, but
1000 - // CLI/SQL can set 0 or a negative integer. Honoring those would mean
1001 - // "delete every record on every cycle", which has no legitimate use
1002 - // case (keep_records_indefinitely covers the opposite extreme).
1003 - // Bailing out makes operator error visible (records stop being purged)
1004 - // instead of catastrophic (records get wiped repeatedly).
1005 - if ( (int) $options['general_records_ttl'] < 1 ) {
1006 - return;
1007 - }
710 + $days = $options['general_records_ttl'];
711 + $timezone = new DateTimeZone( 'UTC' );
712 + $date = new DateTime( 'now', $timezone );
1008 713
1009 - // Overlap guard: if any auto-purge action (batch worker or reaper) is
1010 - // pending or in-progress, don't stack a new chain. Reuses the same
1011 - // probe used by the Settings UI so the two views of "running" agree.
1012 - if ( self::is_running_auto_purge() ) {
1013 - return;
1014 - }
714 + $date->sub( DateInterval::createFromDateString( "$days days" ) );
1015 715
1016 - /**
1017 - * Fires once per auto-purge cycle, after all bail-out checks pass and
1018 - * immediately before deletion work is enqueued.
1019 - *
1020 - * Preserved for backward compatibility with consumers that hooked the
1021 - * legacy WP-Cron event of the same name in Stream <= 4.1.x. Note that
1022 - * since 4.2.0 this fires only when a purge is actually about to run —
1023 - * it no longer fires on every cron tick regardless of whether work
1024 - * happens. Hook into the recurring AS action (Admin::AUTO_PURGE_ACTION)
1025 - * directly if you need the older "every tick" semantics.
1026 - */
1027 - do_action( 'wp_stream_auto_purge' );
716 + $where = $wpdb->prepare( ' AND `stream`.`created` < %s', $date->format( 'Y-m-d H:i:s' ) );
1028 717
1029 - // Snapshot the UTC cutoff once per recurring tick. Each batch in this
1030 - // chain operates against this fixed cutoff so the chain is finite.
1031 - $days = (int) $options['general_records_ttl'];
1032 - $cutoff = ( new DateTime( 'now', new DateTimeZone( 'UTC' ) ) )
1033 - ->sub( DateInterval::createFromDateString( $days . ' days' ) )
1034 - ->format( 'Y-m-d H:i:s' );
1035 -
1036 - // blog_id = 0 means "all blogs" (network-activated path).
1037 - $blog_id = $this->plugin->is_multisite_not_network_activated() ? (int) get_current_blog_id() : 0;
1038 -
1039 - global $wpdb;
1040 -
1041 - // "Is this a large table?" decision matches the manual reset path
1042 - // (Admin::erase_stream_records()). When the table is small the cost
1043 - // of scheduling a chain (and waiting for AS to drain it on the next
1044 - // runner tick) exceeds the cost of a single inline DELETE. Only fall
1045 - // through to the batched chain when the filter says "yes, large".
1046 - if ( $blog_id > 0 ) {
1047 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1048 - $record_count = (int) $wpdb->get_var(
1049 - $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->stream} WHERE `blog_id` = %d", $blog_id )
1050 - );
1051 - } else {
1052 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1053 - $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() );
1054 721 }
1055 722
1056 - if ( ! $this->plugin->is_large_records_table( $record_count ) ) {
1057 - // Small-table fast path: one inline multi-table DELETE, then enqueue
1058 - // the orphan reaper as a one-shot async action so the heal step is
1059 - // still observable in Tools → Scheduled Actions.
1060 - if ( $blog_id > 0 ) {
1061 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1062 - $wpdb->query(
1063 - $wpdb->prepare(
1064 - "DELETE `stream`, `meta`
1065 - FROM {$wpdb->stream} AS `stream`
1066 - LEFT JOIN {$wpdb->streammeta} AS `meta`
1067 - ON `meta`.`record_id` = `stream`.`ID`
1068 - WHERE `stream`.`created` < %s AND `stream`.`blog_id` = %d;",
1069 - $cutoff,
1070 - $blog_id
1071 - )
1072 - );
1073 - } else {
1074 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1075 - $wpdb->query(
1076 - $wpdb->prepare(
1077 - "DELETE `stream`, `meta`
1078 - FROM {$wpdb->stream} AS `stream`
1079 - LEFT JOIN {$wpdb->streammeta} AS `meta`
1080 - ON `meta`.`record_id` = `stream`.`ID`
1081 - WHERE `stream`.`created` < %s;",
1082 - $cutoff
1083 - )
1084 - );
1085 - }
1086 -
1087 - as_enqueue_async_action( self::AUTO_PURGE_REAPER_ACTION, array(), self::AUTO_PURGE_GROUP );
1088 - return;
1089 - }
1090 -
1091 - // Large-table path: batched chain.
1092 - as_enqueue_async_action(
1093 - self::AUTO_PURGE_BATCH_ACTION,
1094 - array(
1095 - 'cutoff' => $cutoff,
1096 - 'blog_id' => $blog_id,
1097 - ),
1098 - 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
1099 729 );
1100 730 }
1101 731
1102 732 /**
1103 - * Async Action Scheduler callback: delete one batch of records eligible
1104 - * under the snapshotted UTC cutoff, then chain the next batch (or the
1105 - * orphan reaper when nothing remains).
733 + * @param array $links
734 + * @param string $file
1106 735 *
1107 - * Window-based deletion mirrors {@see Admin::erase_large_records()} so the
1108 - * InnoDB lock footprint is bounded and predictable on bloated tables.
1109 - *
1110 - * @param string $cutoff MySQL DATETIME string in UTC.
1111 - * @param int $blog_id Blog to scope to, or 0 for all blogs (network-activated).
1112 - * @param int $last_entry The lower-bound ID of the previous batch's window; 0 on the
1113 - * first batch in a chain. The next SELECT uses `ID < last_entry`
1114 - * when non-zero, guaranteeing forward progress even on tables
1115 - * that grow rapidly during the chain. Trade-off: any eligible
1116 - * row that lands inside the already-touched ID range
1117 - * [window_low, start_from] after that batch ran is skipped
1118 - * by the current chain and picked up on the next recurring
1119 - * tick (or small-table fast path). Possible sources: dev/test
1120 - * seeders, importer/migration plugins replaying historical
1121 - * rows, or PHP/MySQL clock skew on `created`. Steady-state
1122 - * logging via Log::log() uses monotonic IDs and current UTC,
1123 - * so this is a no-op for normal production traffic.
1124 - * @throws \InvalidArgumentException When $cutoff is empty (signals AS to mark the action as failed).
1125 - * @return void
1126 - */
1127 - public function auto_purge_batch( $cutoff, $blog_id = 0, $last_entry = 0 ) {
1128 - global $wpdb;
1129 -
1130 - $cutoff = (string) $cutoff;
1131 - $blog_id = (int) $blog_id;
1132 - $last_entry = (int) $last_entry;
1133 -
1134 - // Defensive: a malformed cutoff would otherwise translate to a no-op
1135 - // DELETE that still busies the DB. Throw so Action Scheduler marks
1136 - // the action as failed (and visible in Tools → Scheduled Actions)
1137 - // rather than silently completing. In practice this is unreachable
1138 - // because purge_scheduled_action() always populates the cutoff arg
1139 - // and AS args are immutable; the guard exists for third-party code
1140 - // that may enqueue the action with bad input.
1141 - if ( '' === $cutoff ) {
1142 - throw new \InvalidArgumentException( 'auto_purge_batch requires a non-empty cutoff.' );
1143 - }
1144 -
1145 - /**
1146 - * Filters the number of records to delete per batch.
1147 - *
1148 - * Shared with the manual reset path (see {@see Admin::erase_large_records()})
1149 - * so site owners only need to tune one knob.
1150 - *
1151 - * @since 4.1.0
1152 - *
1153 - * @param int $batch_size Default 250000.
1154 - */
1155 - $batch_size = (int) apply_filters( 'wp_stream_batch_size', 250000 );
1156 - if ( $batch_size < 1 ) {
1157 - $batch_size = 250000;
1158 - }
1159 -
1160 - // Find the highest-ID record still eligible under the snapshotted cutoff
1161 - // that lies strictly below the previous window's lower bound (when set).
1162 - // $last_entry=0 means "first batch in chain" — search from the top.
1163 - if ( $blog_id > 0 && $last_entry > 0 ) {
1164 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1165 - $start_from = $wpdb->get_var(
1166 - $wpdb->prepare(
1167 - "SELECT ID FROM {$wpdb->stream} WHERE `created` < %s AND `blog_id` = %d AND `ID` < %d ORDER BY ID DESC LIMIT 1",
1168 - $cutoff,
1169 - $blog_id,
1170 - $last_entry
1171 - )
1172 - );
1173 - } elseif ( $blog_id > 0 ) {
1174 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1175 - $start_from = $wpdb->get_var(
1176 - $wpdb->prepare(
1177 - "SELECT ID FROM {$wpdb->stream} WHERE `created` < %s AND `blog_id` = %d ORDER BY ID DESC LIMIT 1",
1178 - $cutoff,
1179 - $blog_id
1180 - )
1181 - );
1182 - } elseif ( $last_entry > 0 ) {
1183 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1184 - $start_from = $wpdb->get_var(
1185 - $wpdb->prepare(
1186 - "SELECT ID FROM {$wpdb->stream} WHERE `created` < %s AND `ID` < %d ORDER BY ID DESC LIMIT 1",
1187 - $cutoff,
1188 - $last_entry
1189 - )
1190 - );
1191 - } else {
1192 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1193 - $start_from = $wpdb->get_var(
1194 - $wpdb->prepare(
1195 - "SELECT ID FROM {$wpdb->stream} WHERE `created` < %s ORDER BY ID DESC LIMIT 1",
1196 - $cutoff
1197 - )
1198 - );
1199 - }
1200 -
1201 - if ( empty( $start_from ) ) {
1202 - // Chain is done. Schedule the orphan reaper as the terminal step.
1203 - as_enqueue_async_action( self::AUTO_PURGE_REAPER_ACTION, array(), self::AUTO_PURGE_GROUP );
1204 - return;
1205 - }
1206 -
1207 - $start_from = (int) $start_from;
1208 - $window_low = max( 0, $start_from - $batch_size );
1209 -
1210 - // Multi-table DELETE: parent + meta in one statement. Mirrors
1211 - // Admin::erase_large_records().
1212 - if ( $blog_id > 0 ) {
1213 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1214 - $wpdb->query(
1215 - $wpdb->prepare(
1216 - "DELETE `stream`, `meta`
1217 - FROM {$wpdb->stream} AS `stream`
1218 - LEFT JOIN {$wpdb->streammeta} AS `meta`
1219 - ON `meta`.`record_id` = `stream`.`ID`
1220 - WHERE `stream`.`ID` <= %d
1221 - AND `stream`.`ID` >= %d
1222 - AND `stream`.`created` < %s
1223 - AND `stream`.`blog_id` = %d;",
1224 - $start_from,
1225 - $window_low,
1226 - $cutoff,
1227 - $blog_id
1228 - )
1229 - );
1230 - } else {
1231 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
1232 - $wpdb->query(
1233 - $wpdb->prepare(
1234 - "DELETE `stream`, `meta`
1235 - FROM {$wpdb->stream} AS `stream`
1236 - LEFT JOIN {$wpdb->streammeta} AS `meta`
1237 - ON `meta`.`record_id` = `stream`.`ID`
1238 - WHERE `stream`.`ID` <= %d
1239 - AND `stream`.`ID` >= %d
1240 - AND `stream`.`created` < %s;",
1241 - $start_from,
1242 - $window_low,
1243 - $cutoff
1244 - )
1245 - );
1246 - }
1247 -
1248 - // Chain the next batch. Pass $window_low as the new upper bound so the
1249 - // next SELECT cannot pick up rows in or above the window we just touched.
1250 - as_enqueue_async_action(
1251 - self::AUTO_PURGE_BATCH_ACTION,
1252 - array(
1253 - 'cutoff' => $cutoff,
1254 - 'blog_id' => $blog_id,
1255 - 'last_entry' => $window_low,
1256 - ),
1257 - self::AUTO_PURGE_GROUP
1258 - );
1259 - }
1260 -
1261 - /**
1262 - * Terminal Action Scheduler callback for the auto-purge chain.
1263 - *
1264 - * Runs once per chain (after the last batch) and once when the manual
1265 - * "Clean orphaned meta now" button is used. Cleans up meta rows whose
1266 - * parent stream row is already gone — i.e. residue from historical
1267 - * unbatched purges and from any logger races during a chain.
1268 - *
1269 - * @return void
1270 - */
1271 - public function auto_purge_reaper() {
1272 - $this->delete_orphaned_meta();
1273 - }
1274 -
1275 - /**
1276 - * Ajax handler for the "Clean orphaned meta now" button on
1277 - * Settings → Advanced.
1278 - *
1279 - * Schedules an immediate async run of the orphan reaper. Idempotent:
1280 - * if a reaper is already scheduled, returns without enqueuing a second.
1281 - *
1282 - * Returns true under WP_STREAM_TESTS so PHPUnit can call this directly
1283 - * without exiting the worker.
1284 - *
1285 - * @return bool|void True under tests; otherwise redirects and exits.
1286 - */
1287 - public function wp_ajax_clean_orphan_meta() {
1288 - if ( ! current_user_can( $this->settings_cap ) ) {
1289 - wp_die( esc_html__( 'You do not have permission to do this.', 'stream' ), 403 );
1290 - }
1291 -
1292 - check_ajax_referer( 'stream_nonce_clean_orphan_meta', 'wp_stream_nonce_clean_orphan_meta' );
1293 -
1294 - if ( ! function_exists( 'as_get_scheduled_actions' ) || ! function_exists( 'as_enqueue_async_action' ) ) {
1295 - wp_die( esc_html__( 'Action Scheduler is not available.', 'stream' ), 500 );
1296 - }
1297 -
1298 - // Idempotency: skip enqueue when any auto-purge action is already
1299 - // pending or running. is_running_auto_purge() checks PENDING + RUNNING
1300 - // across the batch worker and the reaper, so a chain that will run
1301 - // its own terminal reaper is not duplicated by a manual click landing
1302 - // in the small CSRF/stale-URL window where the UI link is hidden.
1303 - if ( ! self::is_running_auto_purge() ) {
1304 - as_enqueue_async_action( self::AUTO_PURGE_REAPER_ACTION, array(), self::AUTO_PURGE_GROUP );
1305 - }
1306 -
1307 - if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
1308 - return true;
1309 - }
1310 -
1311 - $is_network = $this->plugin->is_multisite_network_activated();
1312 - $page_slug = $is_network ? $this->network->network_settings_page_slug : $this->settings_page_slug;
1313 - $base_url = $is_network ? network_admin_url( $this->admin_parent_page ) : admin_url( $this->admin_parent_page );
1314 -
1315 - wp_safe_redirect(
1316 - add_query_arg(
1317 - array(
1318 - 'page' => $page_slug,
1319 - 'wp_stream_message' => 'orphan_meta_cleanup_scheduled',
1320 - ),
1321 - $base_url
1322 - )
1323 - );
1324 - exit;
1325 - }
1326 -
1327 - /**
1328 - * Render admin notices for post-action redirects.
1329 - *
1330 - * Reads `wp_stream_message` from the query string and renders a matching
1331 - * notice. Used to surface "Clean Orphaned Meta" confirmation after the
1332 - * Ajax handler redirects back to Settings → Advanced.
1333 - *
1334 - * @return void
1335 - */
1336 - public function maybe_display_message() {
1337 - $message = wp_stream_filter_input( INPUT_GET, 'wp_stream_message' );
1338 - if ( empty( $message ) ) {
1339 - return;
1340 - }
1341 -
1342 - $notices = array(
1343 - 'orphan_meta_cleanup_scheduled' => __(
1344 - 'Orphaned meta cleanup scheduled. Progress is visible under Tools → Scheduled Actions.',
1345 - 'stream'
1346 - ),
1347 - );
1348 -
1349 - if ( ! isset( $notices[ $message ] ) ) {
1350 - return;
1351 - }
1352 -
1353 - printf(
1354 - '<div class="notice notice-success is-dismissible"><p>%s</p></div>',
1355 - esc_html( $notices[ $message ] )
1356 - );
1357 - }
1358 -
1359 - /**
1360 - * Returns the admin action links.
1361 - *
1362 736 * @filter plugin_action_links
1363 737 *
1364 - * @param array $links Action links.
1365 - * @param string $file Plugin file.
1366 - *
1367 738 * @return array
1368 739 */
1369 740 public function plugin_action_links( $links, $file ) {
1370 741 if ( plugin_basename( $this->plugin->locations['dir'] . 'stream.php' ) !== $file ) {
@@ -1370,10 +741,10 @@
1370 741 if ( plugin_basename( $this->plugin->locations['dir'] . 'stream.php' ) !== $file ) {
1371 742 return $links;
1372 743 }
1373 744
1374 - // Also don't show links in Network Admin if Stream isn't network enabled.
1375 - 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() ) {
1376 747 return $links;
1377 748 }
1378 749
1379 750 if ( is_network_admin() ) {
@@ -1391,10 +762,22 @@
1391 762 admin_url( $this->admin_parent_page )
1392 763 );
1393 764 }
1394 765
1395 - $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' ) );
1396 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 +
1397 780 return $links;
1398 781 }
1399 782
1400 783 /**
@@ -1420,18 +803,10 @@
1420 803 $page_description = apply_filters( 'wp_stream_settings_form_description', '' );
1421 804
1422 805 $sections = $this->plugin->settings->get_fields();
1423 806 $active_tab = wp_stream_filter_input( INPUT_GET, 'tab' );
1424 -
1425 - $this->plugin->enqueue_asset(
1426 - 'settings',
1427 - array(),
1428 - array(
1429 - 'i18n' => array(
1430 - 'confirm_purge' => __( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
1431 - ),
1432 - )
1433 - );
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 );
1434 809 ?>
1435 810 <div class="wrap">
1436 811 <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
1437 812
@@ -1444,9 +819,9 @@
1444 819 <?php if ( count( $sections ) > 1 ) : ?>
1445 820 <h2 class="nav-tab-wrapper">
1446 821 <?php $i = 0; ?>
1447 822 <?php foreach ( $sections as $section => $data ) : ?>
1448 - <?php ++$i; ?>
823 + <?php $i++; ?>
1449 824 <?php $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section ); ?>
1450 825 <a href="<?php echo esc_url( add_query_arg( 'tab', $section ) ); ?>" class="nav-tab <?php echo $is_active ? esc_attr( ' nav-tab-active' ) : ''; ?>">
1451 826 <?php echo esc_html( $data['title'] ); ?>
1452 827 </a>
@@ -1459,9 +834,9 @@
1459 834 <div class="settings-sections">
1460 835 <?php
1461 836 $i = 0;
1462 837 foreach ( $sections as $section => $data ) {
1463 - ++$i;
838 + $i++;
1464 839
1465 840 $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section );
1466 841
1467 842 if ( $is_active ) {
@@ -1492,9 +867,9 @@
1492 867
1493 868 /**
1494 869 * Check if a particular role has access
1495 870 *
1496 - * @param string $role User role.
871 + * @param string $role
1497 872 *
1498 873 * @return bool
1499 874 */
1500 875 private function role_can_view( $role ) {
@@ -1507,12 +882,12 @@
1507 882
1508 883 /**
1509 884 * Filter user caps to dynamically grant our view cap based on allowed roles
1510 885 *
1511 - * @param array $allcaps All capabilities.
1512 - * @param array $caps Required caps.
1513 - * @param array $args Unused.
1514 - * @param WP_User $user User.
886 + * @param $allcaps
887 + * @param $caps
888 + * @param $args
889 + * @param $user
1515 890 *
1516 891 * @filter user_has_cap
1517 892 *
1518 893 * @return array
@@ -1557,11 +932,11 @@
1557 932 * Filter role caps to dynamically grant our view cap based on allowed roles
1558 933 *
1559 934 * @filter role_has_cap
1560 935 *
1561 - * @param array $allcaps All capabilities.
1562 - * @param string $cap Require cap.
1563 - * @param string $role User role.
936 + * @param $allcaps
937 + * @param $cap
938 + * @param $role
1564 939 *
1565 940 * @return array
1566 941 */
1567 942 public function filter_role_caps( $allcaps, $cap, $role ) {
@@ -1574,10 +949,8 @@
1574 949 return $allcaps;
1575 950 }
1576 951
1577 952 /**
1578 - * Ajax callback for return a user list.
1579 - *
1580 953 * @action wp_ajax_wp_stream_filters
1581 954 */
1582 955 public function ajax_filters() {
1583 956 if ( ! defined( 'DOING_AJAX' ) || ! current_user_can( $this->plugin->admin->settings_cap ) ) {
@@ -1611,9 +984,9 @@
1611 984 if ( count( $users ) > $this->preload_users_max ) {
1612 985 $users = array_slice( $users, 0, $this->preload_users_max );
1613 986 }
1614 987
1615 - // Get gravatar / roles for final result set.
988 + // Get gravatar / roles for final result set
1616 989 $results = $this->get_users_record_meta( $users );
1617 990
1618 991 break;
1619 992 }
@@ -1618,20 +991,14 @@
1618 991 break;
1619 992 }
1620 993
1621 994 if ( isset( $results ) ) {
1622 - echo wp_json_encode( $results );
995 + echo wp_stream_json_encode( $results ); // xss ok
1623 996 }
1624 997
1625 998 die();
1626 999 }
1627 1000
1628 - /**
1629 - * Return relevant user meta data.
1630 - *
1631 - * @param array $authors Author data.
1632 - * @return array
1633 - */
1634 1001 public function get_users_record_meta( $authors ) {
1635 1002 $authors_records = array();
1636 1003
1637 1004 foreach ( $authors as $user_id => $args ) {
@@ -1651,15 +1018,19 @@
1651 1018
1652 1019 /**
1653 1020 * Get user meta in a way that is also safe for VIP
1654 1021 *
1655 - * @param int $user_id User ID.
1656 - * @param string $meta_key Meta key.
1657 - * @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)
1658 1025 *
1659 1026 * @return mixed
1660 1027 */
1661 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 +
1662 1033 return get_user_meta( $user_id, $meta_key, $single );
1663 1034 }
1664 1035
1665 1036 /**
@@ -1664,16 +1035,20 @@
1664 1035
1665 1036 /**
1666 1037 * Update user meta in a way that is also safe for VIP
1667 1038 *
1668 - * @param int $user_id User ID.
1669 - * @param string $meta_key Meta key.
1670 - * @param mixed $meta_value Meta value.
1671 - * @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)
1672 1043 *
1673 1044 * @return int|bool
1674 1045 */
1675 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 +
1676 1051 return update_user_meta( $user_id, $meta_key, $meta_value, $prev_value );
1677 1052 }
1678 1053
1679 1054 /**
@@ -1678,14 +1053,18 @@
1678 1053
1679 1054 /**
1680 1055 * Delete user meta in a way that is also safe for VIP
1681 1056 *
1682 - * @param int $user_id User ID.
1683 - * @param string $meta_key Meta key.
1684 - * @param mixed $meta_value Meta value (optional).
1057 + * @param int $user_id
1058 + * @param string $meta_key
1059 + * @param mixed $meta_value (optional)
1685 1060 *
1686 1061 * @return bool
1687 1062 */
1688 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 +
1689 1068 return delete_user_meta( $user_id, $meta_key, $meta_value );
1690 1069 }
1691 1070 }