PluginProbe
Stream – Activity Log & Audit Trail / 3.9.0
Stream – Activity Log & Audit Trail v3.9.0
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 +237 -1053 4.3.03.9.0 View file →
@@ -9,10 +9,10 @@
9 9
10 10 use DateTime;
11 11 use DateTimeZone;
12 12 use DateInterval;
13 -use WP_CLI;
14 -use WP_Roles;
13 +use \WP_CLI;
14 +use \WP_Roles;
15 15
16 16 /**
17 17 * Class - Admin
18 18 */
@@ -18,66 +18,8 @@
18 18 */
19 19 class Admin {
20 20
21 21 /**
22 - * The async deletion action for large sites.
23 - *
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 22 * Holds Instance of plugin object
81 23 *
82 24 * @var Plugin
83 25 */
@@ -160,13 +102,13 @@
160 102 */
161 103 public $view_cap = 'view_stream';
162 104
163 105 /**
164 - * Capability name for managing settings
106 + * Capability name for viewing settings
165 107 *
166 108 * @var string
167 109 */
168 - public $settings_cap = WP_STREAM_SETTINGS_CAPABILITY;
110 + public $settings_cap = 'manage_options';
169 111
170 112 /**
171 113 * Total amount of authors to pre-load
172 114 *
@@ -199,9 +141,9 @@
199 141 // User and role caps.
200 142 add_filter( 'user_has_cap', array( $this, 'filter_user_caps' ), 10, 4 );
201 143 add_filter( 'role_has_cap', array( $this, 'filter_role_caps' ), 10, 3 );
202 144
203 - if ( $this->plugin->is_multisite_network_activated() && ! is_network_admin() ) {
145 + if ( is_multisite() && $plugin->is_network_activated() && ! is_network_admin() ) {
204 146 $options = (array) get_site_option( 'wp_stream_network', array() );
205 147 $option = isset( $options['general_site_access'] ) ? absint( $options['general_site_access'] ) : 1;
206 148
207 149 $this->disable_access = ( $option ) ? false : true;
@@ -215,11 +157,8 @@
215 157 // Admin notices.
216 158 add_action( 'admin_notices', array( $this, 'prepare_admin_notices' ) );
217 159 add_action( 'shutdown', array( $this, 'admin_notices' ) );
218 160
219 - // Feature request notice.
220 - add_action( 'admin_notices', array( $this, 'display_feature_request_notice' ) );
221 -
222 161 // Add admin body class.
223 162 add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
224 163
225 164 // Plugin action links.
@@ -251,41 +190,24 @@
251 190 'wp_ajax_reset',
252 191 )
253 192 );
254 193
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 - );
194 + /**
195 + * Uninstall Streams and Deactivate plugin.
196 + *
197 + * @todo Confirm if variable assignment is necessary.
198 + */
199 + $uninstall = $this->plugin->db->driver->purge_storage( $this->plugin );
260 200
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).
201 + // Auto purge setup.
273 202 add_action( 'wp_loaded', array( $this, 'purge_schedule_setup' ) );
274 203 add_action(
275 - self::AUTO_PURGE_ACTION,
276 - array( $this, 'purge_scheduled_action' )
204 + 'wp_stream_auto_purge',
205 + array(
206 + $this,
207 + 'purge_scheduled_action',
208 + )
277 209 );
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 210
289 211 // Ajax users list.
290 212 add_action(
291 213 'wp_ajax_wp_stream_filters',
@@ -293,19 +215,8 @@
293 215 $this,
294 216 'ajax_filters',
295 217 )
296 218 );
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 219 }
309 220
310 221 /**
311 222 * Load admin classes
@@ -315,14 +226,8 @@
315 226 public function init() {
316 227 $this->network = new Network( $this->plugin );
317 228 $this->live_update = new Live_Update( $this->plugin );
318 229 $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 230 }
326 231
327 232 /**
328 233 * Output specific updates passed as URL parameters.
@@ -398,28 +303,8 @@
398 303 }
399 304 }
400 305
401 306 /**
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 307 * Register menu page
423 308 *
424 309 * @action admin_menu
425 310 *
@@ -454,9 +339,9 @@
454 339 $main_menu_title,
455 340 $this->view_cap,
456 341 $this->records_page_slug,
457 342 array( $this, 'render_list_table' ),
458 - 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIGZpbGw9IjAwMCI+Cgk8cGF0aCBkPSJNOTAzLjExNSA1MTUuNDEzYy00OS4zOTIgMC05MS40NzQgMzEuMzM3LTEwNy40NiA3NS4yMDNsLTEyNC40MTEtMS41MzJjLTExLjM3Ny0uMzQ2LTIyLjc1MS0uNjg5LTM0LjEyOS0uOTk4bC0uMjQxLjU3NC0yMi40MzYtLjI3OC0uMTUzLS45Mi0xNS4wNTYtODIuOTMzLTIwLjE0Ni0xMDguNDA1LTIwLjU0NC0xMDguMzM3TDUwMy45ODIgMGwtNTMuMTQxIDQyOS4wMTMtMTYuMjE0IDEzNy45MzUtMTIuMDE2IDEwNi45MjQtMTE3LjI4Ni0yODUuMjItMTguMzUzIDIwMi44MWMtNDIuNTYyIDEuNDU0LTg1LjEyNyAyLjkzNC0xMjcuNjg4IDQuNzM4LTUzLjA5NyAyLjI5Mi0xMDYuMTg3IDQuNDczLTE1OS4yODQgNy41MzZ2NDIuMDQyYzUzLjA5NyAzLjA2IDEwNi4xODcgNS4yNDcgMTU5LjI4NCA3LjUzMyA1My4wOTMgMi4yNDUgMTA2LjE4IDQuMTk0IDE1OS4yNzMgNS45MDNsMTQuMjQuNDY1IDE3LjM1MSA0OC4zOWMxOC44NDIgNTEuODc0IDM3LjU0MiAxMDMuODA2IDU2Ljc2NSAxNTUuNTQxTDQ2Ni41MiAxMDI0bDQxLjUxMi0zMDguMjkzIDE3LjYzMy0xMzYuNjg1IDEwLjc3NiA1MC4zMjkgNTQuODE1IDI0OC41NDQgNzIuNTE2LTIxNy4yMTdoMTI5LjI2MWMxMy40OTMgNDguMTIxIDU3LjY1NSA4My40MjkgMTEwLjA3NSA4My40MjkgNjMuMTYgMCAxMTQuMzUyLTUxLjIwNSAxMTQuMzUyLTExNC4zNDggMC02My4xMzktNTEuMTg5LTExNC4zNDUtMTE0LjM0OS0xMTQuMzQ1bC4wMDQtLjAwMVoiIC8+Cjwvc3ZnPgo=',
343 + 'div',
459 344 $main_menu_position
460 345 );
461 346
462 347 /**
@@ -511,50 +396,103 @@
511 396 *
512 397 * @return void
513 398 */
514 399 public function admin_enqueue_scripts( $hook ) {
515 - if ( in_array( $hook, $this->screen_id, true ) ) {
516 - $this->plugin->enqueue_asset(
517 - 'admin',
400 + wp_register_script( 'wp-stream-select2', $this->plugin->locations['url'] . 'ui/lib/select2/js/select2.full.min.js', array( 'jquery' ), '3.5.2', true );
401 + wp_register_style( 'wp-stream-select2', $this->plugin->locations['url'] . 'ui/lib/select2/css/select2.min.css', array(), '3.5.2' );
402 + wp_register_script( 'wp-stream-timeago', $this->plugin->locations['url'] . 'ui/lib/timeago/jquery.timeago.js', array(), '1.4.1', true );
403 +
404 + $locale = strtolower( substr( get_locale(), 0, 2 ) );
405 + $file_tmpl = 'ui/lib/timeago/locales/jquery.timeago.%s.js';
406 +
407 + if ( file_exists( $this->plugin->locations['dir'] . sprintf( $file_tmpl, $locale ) ) ) {
408 + wp_register_script(
409 + 'wp-stream-timeago-locale',
410 + $this->plugin->locations['url'] . sprintf( $file_tmpl, $locale ),
411 + array( 'wp-stream-timeago' ),
412 + '1',
413 + false
414 + );
415 + } else {
416 + wp_register_script(
417 + 'wp-stream-timeago-locale',
418 + $this->plugin->locations['url'] . sprintf( $file_tmpl, 'en' ),
419 + array( 'wp-stream-timeago' ),
420 + '1',
421 + false
422 + );
423 + }
424 +
425 + $min = wp_stream_min_suffix();
426 + wp_enqueue_style( 'wp-stream-admin', $this->plugin->locations['url'] . 'ui/css/admin.' . $min . 'css', array(), $this->plugin->get_version() );
427 +
428 + $script_screens = array( 'plugins.php' );
429 +
430 + if ( in_array( $hook, $this->screen_id, true ) || in_array( $hook, $script_screens, true ) ) {
431 + wp_enqueue_script( 'wp-stream-select2' );
432 + wp_enqueue_style( 'wp-stream-select2' );
433 +
434 + wp_enqueue_script( 'wp-stream-timeago' );
435 + wp_enqueue_script( 'wp-stream-timeago-locale' );
436 +
437 + wp_enqueue_script(
438 + 'wp-stream-admin',
439 + $this->plugin->locations['url'] . 'ui/js/admin.' . $min . 'js',
518 440 array(
519 - $this->plugin->with_select2(),
520 - $this->plugin->with_jquery_timeago(),
441 + 'jquery',
442 + 'wp-stream-select2',
521 443 ),
444 + $this->plugin->get_version(),
445 + false
446 + );
447 + wp_enqueue_script(
448 + 'wp-stream-admin-exclude',
449 + $this->plugin->locations['url'] . 'ui/js/exclude.' . $min . 'js',
522 450 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 - )
451 + 'jquery',
452 + 'wp-stream-select2',
453 + ),
454 + $this->plugin->get_version(),
455 + false
530 456 );
531 -
532 - $this->plugin->enqueue_asset(
533 - 'admin-exclude',
457 + wp_enqueue_script(
458 + 'wp-stream-live-updates',
459 + $this->plugin->locations['url'] . 'ui/js/live-updates.' . $min . 'js',
534 460 array(
535 - $this->plugin->with_select2(),
461 + 'jquery',
462 + 'heartbeat',
536 463 ),
464 + $this->plugin->get_version(),
465 + false
466 + );
467 +
468 + wp_localize_script(
469 + 'wp-stream-admin',
470 + 'wp_stream',
537 471 array(
538 - 'getActionsNonce' => wp_create_nonce( 'stream_get_actions' ),
472 + 'i18n' => array(
473 + 'confirm_purge' => esc_html__( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
474 + 'confirm_defaults' => esc_html__( 'Are you sure you want to reset all site settings to default? This cannot be undone.', 'stream' ),
475 + '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' ),
476 + ),
477 + 'locale' => esc_js( $locale ),
478 + 'gmt_offset' => get_option( 'gmt_offset' ),
539 479 )
540 480 );
541 481
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
482 + $order_types = array( 'asc', 'desc' );
547 483
548 - $this->plugin->enqueue_asset(
549 - 'live-updates',
550 - array( 'heartbeat' ),
484 + wp_localize_script(
485 + 'wp-stream-live-updates',
486 + 'wp_stream_live_updates',
551 487 array(
552 488 'current_screen' => $hook,
553 489 '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 ),
490 + 'current_order' => isset( $_GET['order'] ) && in_array( strtolower( $_GET['order'] ), $order_types, true ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
491 + ? esc_js( $_GET['order'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
492 + : 'desc',
493 + 'current_query' => wp_stream_json_encode( $_GET ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
494 + 'current_query_count' => count( $_GET ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
557 495 )
558 496 );
559 497 }
560 498
@@ -571,16 +509,24 @@
571 509 * @return int
572 510 */
573 511 $bulk_actions_threshold = apply_filters( 'wp_stream_bulk_actions_threshold', 100 );
574 512
575 - $this->plugin->enqueue_asset(
576 - 'global',
577 - array(),
513 + wp_enqueue_script(
514 + 'wp-stream-global',
515 + $this->plugin->locations['url'] . 'ui/js/global.' . $min . 'js',
516 + array( 'jquery' ),
517 + $this->plugin->get_version(),
518 + false
519 + );
520 +
521 + wp_localize_script(
522 + 'wp-stream-global',
523 + 'wp_stream_global',
578 524 array(
579 525 'bulk_actions' => array(
580 526 'i18n' => array(
581 527 /* 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 ) ) ),
528 + '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 529 ),
584 530 'threshold' => absint( $bulk_actions_threshold ),
585 531 ),
586 532 'plugins_screen_url' => self_admin_url( 'plugins.php#stream' ),
@@ -593,23 +539,17 @@
593 539 *
594 540 * @return bool
595 541 */
596 542 public function is_stream_screen() {
597 - if ( ! is_admin() ) {
598 - return false;
543 + if ( is_admin() && false !== strpos( wp_stream_filter_input( INPUT_GET, 'page' ), $this->records_page_slug ) ) {
544 + return true;
599 545 }
600 546
601 - $page = wp_stream_filter_input( INPUT_GET, 'page' );
602 - if ( is_string( $page ) && false !== strpos( $page, $this->records_page_slug ) ) {
547 + $screen = get_current_screen();
548 + if ( is_admin() && Alerts::POST_TYPE === $screen->post_type ) {
603 549 return true;
604 550 }
605 551
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 552 return false;
613 553 }
614 554
615 555 /**
@@ -643,13 +583,19 @@
643 583 return sprintf( '%s %s ', $classes, $stream_classes );
644 584 }
645 585
646 586 /**
647 - * Add menu styles for various WP Admin skins.
587 + * Add menu styles for various WP Admin skins
648 588 *
589 + * @uses \wp_add_inline_style()
590 + *
649 591 * @action admin_enqueue_scripts
650 592 */
651 593 public function admin_menu_css() {
594 + $min = wp_stream_min_suffix();
595 + wp_register_style( 'wp-stream-datepicker', $this->plugin->locations['url'] . 'ui/css/datepicker.' . $min . 'css', array(), $this->plugin->get_version() );
596 + wp_register_style( 'wp-stream-icons', $this->plugin->locations['url'] . 'ui/stream-icons/style.css', array(), $this->plugin->get_version() );
597 +
652 598 // Make sure we're working off a clean version.
653 599 if ( ! file_exists( ABSPATH . WPINC . '/version.php' ) ) {
654 600 return;
655 601 }
@@ -658,29 +604,61 @@
658 604 if ( ! isset( $wp_version ) ) {
659 605 return;
660 606 }
661 607
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 - ";
608 + $body_class = $this->admin_body_class;
609 + $records_page = $this->records_page_slug;
610 + $stream_url = $this->plugin->locations['url'];
681 611
682 - wp_add_inline_style( 'wp-admin', $css );
612 + if ( version_compare( $wp_version, '3.8-alpha', '>=' ) ) {
613 + wp_enqueue_style( 'wp-stream-icons' );
614 +
615 + $css = "
616 + #toplevel_page_{$records_page} .wp-menu-image:before {
617 + font-family: 'WP Stream' !important;
618 + content: '\\73' !important;
619 + }
620 + #toplevel_page_{$records_page} .wp-menu-image {
621 + background-repeat: no-repeat;
622 + }
623 + #menu-posts-feedback .wp-menu-image:before {
624 + font-family: dashicons !important;
625 + content: '\\f175';
626 + }
627 + #adminmenu #menu-posts-feedback div.wp-menu-image {
628 + background: none !important;
629 + background-repeat: no-repeat;
630 + }
631 + body.{$body_class} #wpbody-content .wrap h1:nth-child(1):before {
632 + font-family: 'WP Stream' !important;
633 + content: '\\73';
634 + padding: 0 8px 0 0;
635 + }
636 + ";
637 + } else {
638 + $css = "
639 + #toplevel_page_{$records_page} .wp-menu-image {
640 + background: url( {$stream_url}ui/stream-icons/menuicon-sprite.png ) 0 90% no-repeat;
641 + }
642 + /* Retina Stream Menu Icon */
643 + @media only screen and (-moz-min-device-pixel-ratio: 1.5),
644 + only screen and (-o-min-device-pixel-ratio: 3/2),
645 + only screen and (-webkit-min-device-pixel-ratio: 1.5),
646 + only screen and (min-device-pixel-ratio: 1.5) {
647 + #toplevel_page_{$records_page} .wp-menu-image {
648 + background: url( {$stream_url}ui/stream-icons/menuicon-sprite-2x.png ) 0 90% no-repeat;
649 + background-size:30px 64px;
650 + }
651 + }
652 + #toplevel_page_{$records_page}.current .wp-menu-image,
653 + #toplevel_page_{$records_page}.wp-has-current-submenu .wp-menu-image,
654 + #toplevel_page_{$records_page}:hover .wp-menu-image {
655 + background-position: top left;
656 + }
657 + ";
658 + }
659 +
660 + \wp_add_inline_style( 'wp-admin', $css );
683 661 }
684 662
685 663 /**
686 664 * Handle the reset AJAX request to reset logs.
@@ -695,14 +673,8 @@
695 673 esc_html__( "You don't have sufficient privileges to do this action.", 'stream' )
696 674 );
697 675 }
698 676
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 677 $this->erase_stream_records();
706 678
707 679 if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
708 680 return true;
@@ -728,891 +700,87 @@
728 700 */
729 701 private function erase_stream_records() {
730 702 global $wpdb;
731 703
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() ) {
704 + $where = '';
735 705
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();
706 + if ( is_multisite() && ! $this->plugin->is_network_activated() ) {
707 + $where .= $wpdb->prepare( ' AND `blog_id` = %d', get_current_blog_id() );
762 708 }
763 - }
764 709
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 710 $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 - )
711 + "DELETE `stream`, `meta`
712 + FROM {$wpdb->stream} AS `stream`
713 + LEFT JOIN {$wpdb->streammeta} AS `meta`
714 + ON `meta`.`record_id` = `stream`.`ID`
715 + WHERE 1=1 {$where};" // @codingStandardsIgnoreLine $where already prepared
1016 716 );
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 717 }
1034 718
1035 719 /**
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 720 * Schedules a purge of records.
1058 721 *
1059 722 * @return void
1060 723 */
1061 724 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' );
725 + if ( ! wp_next_scheduled( 'wp_stream_auto_purge' ) ) {
726 + wp_schedule_event( time(), 'twicedaily', 'wp_stream_auto_purge' );
1066 727 }
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 728 }
1161 729
1162 730 /**
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() {
1171 - global $wpdb;
1172 -
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 731 * Executes a scheduled purge
1180 732 *
1181 733 * @return void
1182 734 */
1183 735 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 - }
736 + global $wpdb;
1192 737
1193 738 // Don't purge when in Network Admin unless Stream is network activated.
1194 739 if (
1195 - $this->plugin->is_multisite_not_network_activated()
740 + is_multisite()
1196 741 &&
1197 742 is_network_admin()
743 + &&
744 + ! $this->plugin->is_network_activated()
1198 745 ) {
1199 746 return;
1200 747 }
1201 748
1202 749 $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 );
750 + if ( is_multisite() && $this->plugin->is_network_activated() ) {
751 + $options = (array) get_site_option( 'wp_stream_network', $defaults );
1205 752 } else {
1206 - $options = wp_parse_args( (array) get_option( 'wp_stream', array() ), $defaults );
753 + $options = (array) get_option( 'wp_stream', $defaults );
1207 754 }
1208 755
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'] ) ) {
756 + if ( ! empty( $options['general_keep_records_indefinitely'] ) || ! isset( $options['general_records_ttl'] ) ) {
1224 757 return;
1225 758 }
1226 759
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 - }
760 + $days = $options['general_records_ttl'];
761 + $timezone = new DateTimeZone( 'UTC' );
762 + $date = new DateTime( 'now', $timezone );
1236 763
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 - }
764 + $date->sub( DateInterval::createFromDateString( "$days days" ) );
1243 765
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' );
766 + $where = $wpdb->prepare( ' AND `stream`.`created` < %s', $date->format( 'Y-m-d H:i:s' ) );
1256 767
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}" );
768 + // Multisite but NOT network activated, only purge the current blog.
769 + if ( is_multisite() && ! $this->plugin->is_network_activated() ) {
770 + $where .= $wpdb->prepare( ' AND `blog_id` = %d', get_current_blog_id() );
1282 771 }
1283 772
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
773 + $wpdb->query(
774 + "DELETE `stream`, `meta`
775 + FROM {$wpdb->stream} AS `stream`
776 + LEFT JOIN {$wpdb->streammeta} AS `meta`
777 + ON `meta`.`record_id` = `stream`.`ID`
778 + WHERE 1=1 {$where};" // @codingStandardsIgnoreLine $where already prepared
1327 779 );
1328 -
1329 - $this->maybe_warn_large_table_without_action_scheduler(
1330 - $record_count,
1331 - __( 'delete records older than the retention period', 'stream' )
1332 - );
1333 780 }
1334 781
1335 782 /**
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).
1339 - *
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 783 * Returns the admin action links.
1616 784 *
1617 785 * @filter plugin_action_links
1618 786 *
@@ -1626,9 +794,9 @@
1626 794 return $links;
1627 795 }
1628 796
1629 797 // 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() ) {
798 + if ( is_network_admin() && is_multisite() && ! $this->plugin->is_network_activated() ) {
1631 799 return $links;
1632 800 }
1633 801
1634 802 if ( is_network_admin() ) {
@@ -1646,10 +814,22 @@
1646 814 admin_url( $this->admin_parent_page )
1647 815 );
1648 816 }
1649 817
1650 - $links[] = sprintf( '<a href="%s">%s</a>', esc_url( $admin_page_url ), esc_html__( 'Settings', 'stream' ) );
818 + $links[] = sprintf( '<a href="%s">%s</a>', esc_url( $admin_page_url ), esc_html__( 'Settings', 'default' ) );
1651 819
820 + if ( ! defined( 'DISALLOW_FILE_MODS' ) || false === DISALLOW_FILE_MODS ) {
821 + $url = add_query_arg(
822 + array(
823 + 'action' => 'wp_stream_uninstall',
824 + 'wp_stream_nonce' => wp_create_nonce( 'stream_nonce' ),
825 + ),
826 + admin_url( 'admin-ajax.php' )
827 + );
828 +
829 + $links[] = sprintf( '<span id="wp_stream_uninstall" class="delete"><a href="%s">%s</a></span>', esc_url( $url ), esc_html__( 'Uninstall', 'stream' ) );
830 + }
831 +
1652 832 return $links;
1653 833 }
1654 834
1655 835 /**
@@ -1675,18 +855,10 @@
1675 855 $page_description = apply_filters( 'wp_stream_settings_form_description', '' );
1676 856
1677 857 $sections = $this->plugin->settings->get_fields();
1678 858 $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 - );
859 + $min = wp_stream_min_suffix();
860 + wp_enqueue_script( 'wp-stream-settings', $this->plugin->locations['url'] . 'ui/js/settings.' . $min . 'js', array( 'jquery' ), $this->plugin->get_version(), true );
1689 861 ?>
1690 862 <div class="wrap">
1691 863 <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
1692 864
@@ -1699,9 +871,9 @@
1699 871 <?php if ( count( $sections ) > 1 ) : ?>
1700 872 <h2 class="nav-tab-wrapper">
1701 873 <?php $i = 0; ?>
1702 874 <?php foreach ( $sections as $section => $data ) : ?>
1703 - <?php ++$i; ?>
875 + <?php $i++; ?>
1704 876 <?php $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section ); ?>
1705 877 <a href="<?php echo esc_url( add_query_arg( 'tab', $section ) ); ?>" class="nav-tab <?php echo $is_active ? esc_attr( ' nav-tab-active' ) : ''; ?>">
1706 878 <?php echo esc_html( $data['title'] ); ?>
1707 879 </a>
@@ -1714,9 +886,9 @@
1714 886 <div class="settings-sections">
1715 887 <?php
1716 888 $i = 0;
1717 889 foreach ( $sections as $section => $data ) {
1718 - ++$i;
890 + $i++;
1719 891
1720 892 $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section );
1721 893
1722 894 if ( $is_active ) {
@@ -1873,9 +1045,9 @@
1873 1045 break;
1874 1046 }
1875 1047
1876 1048 if ( isset( $results ) ) {
1877 - echo wp_json_encode( $results );
1049 + echo wp_stream_json_encode( $results ); // xss ok.
1878 1050 }
1879 1051
1880 1052 die();
1881 1053 }
@@ -1913,8 +1085,12 @@
1913 1085 *
1914 1086 * @return mixed
1915 1087 */
1916 1088 public function get_user_meta( $user_id, $meta_key, $single = true ) {
1089 + if ( wp_stream_is_vip() && function_exists( 'get_user_attribute' ) ) {
1090 + return get_user_attribute( $user_id, $meta_key );
1091 + }
1092 +
1917 1093 return get_user_meta( $user_id, $meta_key, $single );
1918 1094 }
1919 1095
1920 1096 /**
@@ -1927,8 +1103,12 @@
1927 1103 *
1928 1104 * @return int|bool
1929 1105 */
1930 1106 public function update_user_meta( $user_id, $meta_key, $meta_value, $prev_value = '' ) {
1107 + if ( wp_stream_is_vip() && function_exists( 'update_user_attribute' ) ) {
1108 + return update_user_attribute( $user_id, $meta_key, $meta_value );
1109 + }
1110 +
1931 1111 return update_user_meta( $user_id, $meta_key, $meta_value, $prev_value );
1932 1112 }
1933 1113
1934 1114 /**
@@ -1940,7 +1120,11 @@
1940 1120 *
1941 1121 * @return bool
1942 1122 */
1943 1123 public function delete_user_meta( $user_id, $meta_key, $meta_value = '' ) {
1124 + if ( wp_stream_is_vip() && function_exists( 'delete_user_attribute' ) ) {
1125 + return delete_user_attribute( $user_id, $meta_key, $meta_value );
1126 + }
1127 +
1944 1128 return delete_user_meta( $user_id, $meta_key, $meta_value );
1945 1129 }
1946 1130 }