PluginProbe
Stream – Activity Log & Audit Trail / 4.1.0
Stream – Activity Log & Audit Trail v4.1.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
stream / classes / class-admin.php

class-admin.php in Stream – Activity Log & Audit Trail 4.1.0, at classes/class-admin.php

1,221 lines 33.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Centralized manager for WordPress backend functionality.
4 *
5 * @package WP_Stream
6 */
7
8 namespace WP_Stream;
9
10 use DateTime;
11 use DateTimeZone;
12 use DateInterval;
13 use WP_CLI;
14 use WP_Roles;
15
16 /**
17 * Class - Admin
18 */
19 class Admin {
20
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 * Holds Instance of plugin object
30 *
31 * @var Plugin
32 */
33 public $plugin;
34
35 /**
36 * Holds Network class
37 *
38 * @var Network
39 */
40 public $network;
41
42 /**
43 * Holds Live Update class
44 *
45 * @var Live_Update
46 */
47 public $live_update;
48
49 /**
50 * Holds Export class
51 *
52 * @var Export
53 */
54 public $export;
55
56 /**
57 * Menu page screen id
58 *
59 * @var string
60 */
61 public $screen_id = array();
62
63 /**
64 * List table object
65 *
66 * @var List_Table
67 */
68 public $list_table = null;
69
70 /**
71 * Option to disable access to Stream
72 *
73 * @var bool
74 */
75 public $disable_access = false;
76
77 /**
78 * Class applied to the body of the admin screen
79 *
80 * @var string
81 */
82 public $admin_body_class = 'wp_stream_screen';
83
84 /**
85 * Slug of the records page
86 *
87 * @var string
88 */
89 public $records_page_slug = 'wp_stream';
90
91 /**
92 * Slug of the settings page
93 *
94 * @var string
95 */
96 public $settings_page_slug = 'wp_stream_settings';
97
98 /**
99 * Parent page of the records and settings pages
100 *
101 * @var string
102 */
103 public $admin_parent_page = 'admin.php';
104
105 /**
106 * Capability name for viewing records
107 *
108 * @var string
109 */
110 public $view_cap = 'view_stream';
111
112 /**
113 * Capability name for managing settings
114 *
115 * @var string
116 */
117 public $settings_cap = WP_STREAM_SETTINGS_CAPABILITY;
118
119 /**
120 * Total amount of authors to pre-load
121 *
122 * @var int
123 */
124 public $preload_users_max = 50;
125
126 /**
127 * Admin notices, collected and displayed on proper action
128 *
129 * @var array
130 */
131 public $notices = array();
132
133 /**
134 * Class constructor.
135 *
136 * @param Plugin $plugin Instance of plugin object.
137 */
138 public function __construct( $plugin ) {
139 $this->plugin = $plugin;
140
141 add_action( 'init', array( $this, 'init' ) );
142
143 // Ensure function used in various methods is pre-loaded.
144 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
145 require_once ABSPATH . '/wp-admin/includes/plugin.php';
146 }
147
148 // User and role caps.
149 add_filter( 'user_has_cap', array( $this, 'filter_user_caps' ), 10, 4 );
150 add_filter( 'role_has_cap', array( $this, 'filter_role_caps' ), 10, 3 );
151
152 if ( $this->plugin->is_multisite_network_activated() && ! is_network_admin() ) {
153 $options = (array) get_site_option( 'wp_stream_network', array() );
154 $option = isset( $options['general_site_access'] ) ? absint( $options['general_site_access'] ) : 1;
155
156 $this->disable_access = ( $option ) ? false : true;
157 }
158
159 // Register settings page.
160 if ( ! $this->disable_access ) {
161 add_action( 'admin_menu', array( $this, 'register_menu' ) );
162 }
163
164 // Admin notices.
165 add_action( 'admin_notices', array( $this, 'prepare_admin_notices' ) );
166 add_action( 'shutdown', array( $this, 'admin_notices' ) );
167
168 // Feature request notice.
169 add_action( 'admin_notices', array( $this, 'display_feature_request_notice' ) );
170
171 // Add admin body class.
172 add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
173
174 // Plugin action links.
175 add_filter(
176 'plugin_action_links',
177 array(
178 $this,
179 'plugin_action_links',
180 ),
181 10,
182 2
183 );
184
185 // Load admin scripts and styles.
186 add_action(
187 'admin_enqueue_scripts',
188 array(
189 $this,
190 'admin_enqueue_scripts',
191 )
192 );
193 add_action( 'admin_enqueue_scripts', array( $this, 'admin_menu_css' ) );
194
195 // Reset Streams database.
196 add_action(
197 'wp_ajax_wp_stream_reset',
198 array(
199 $this,
200 'wp_ajax_reset',
201 )
202 );
203
204 // Auto purge setup.
205 add_action( 'wp_loaded', array( $this, 'purge_schedule_setup' ) );
206 add_action(
207 'wp_stream_auto_purge',
208 array(
209 $this,
210 'purge_scheduled_action',
211 )
212 );
213
214 // Ajax users list.
215 add_action(
216 'wp_ajax_wp_stream_filters',
217 array(
218 $this,
219 'ajax_filters',
220 )
221 );
222
223 // Async action for erasing large log tables.
224 add_action(
225 self::ASYNC_DELETION_ACTION,
226 array(
227 $this,
228 'erase_large_records',
229 ),
230 10,
231 4
232 );
233 }
234
235 /**
236 * Load admin classes
237 *
238 * @action init
239 */
240 public function init() {
241 $this->network = new Network( $this->plugin );
242 $this->live_update = new Live_Update( $this->plugin );
243 $this->export = new Export( $this->plugin );
244
245 // Check if the host has configured the `REMOTE_ADDR` correctly.
246 $client_ip = $this->plugin->get_client_ip_address();
247 if ( empty( $client_ip ) && $this->is_stream_screen() ) {
248 $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' ) );
249 }
250 }
251
252 /**
253 * Output specific updates passed as URL parameters.
254 *
255 * @action admin_notices
256 *
257 * @return void
258 */
259 public function prepare_admin_notices() {
260 $message = wp_stream_filter_input( INPUT_GET, 'message' );
261
262 switch ( $message ) {
263 case 'settings_reset':
264 $this->notice( esc_html__( 'All site settings have been successfully reset.', 'stream' ) );
265 break;
266 }
267 }
268
269 /**
270 * Handle notice messages according to the appropriate context (WP-CLI or the WP Admin)
271 *
272 * @param string $message Message to output.
273 * @param bool $is_error If the message is error_level (true) or warning (false).
274 */
275 public function notice( $message, $is_error = true ) {
276 if ( defined( 'WP_CLI' ) && WP_CLI ) {
277 $message = wp_strip_all_tags( $message );
278
279 if ( $is_error ) {
280 WP_CLI::warning( $message );
281 } else {
282 WP_CLI::success( $message );
283 }
284 } else {
285 // Trigger admin notices late, so that any notices which occur during page load are displayed.
286 add_action( 'shutdown', array( $this, 'admin_notices' ) );
287
288 $notice = compact( 'message', 'is_error' );
289
290 if ( ! in_array( $notice, $this->notices, true ) ) {
291 $this->notices[] = $notice;
292 }
293 }
294 }
295
296 /**
297 * Show an error or other message in the WP Admin
298 *
299 * @action shutdown
300 */
301 public function admin_notices() {
302 global $allowedposttags;
303
304 $custom = array(
305 'progress' => array(
306 'class' => true,
307 'id' => true,
308 'max' => true,
309 'style' => true,
310 'value' => true,
311 ),
312 );
313
314 $allowed_html = array_merge( $allowedposttags, $custom );
315
316 ksort( $allowed_html );
317
318 foreach ( $this->notices as $notice ) {
319 $class_name = empty( $notice['is_error'] ) ? 'updated' : 'error';
320 $html_message = sprintf( '<div class="%s">%s</div>', esc_attr( $class_name ), wpautop( $notice['message'] ) );
321
322 echo wp_kses( $html_message, $allowed_html );
323 }
324 }
325
326 /**
327 * Display a feature request notice.
328 *
329 * @return void
330 */
331 public function display_feature_request_notice() {
332 $screen = get_current_screen();
333
334 // Display the notice only on the Stream settings page.
335 if ( empty( $this->screen_id['settings'] ) || $this->screen_id['settings'] !== $screen->id ) {
336 return;
337 }
338
339 printf(
340 '<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>',
341 esc_html__( 'Have suggestions or found a bug?', 'stream' ),
342 esc_html__( 'Click here to let us know!', 'stream' )
343 );
344 }
345
346 /**
347 * Register menu page
348 *
349 * @action admin_menu
350 *
351 * @return void
352 */
353 public function register_menu() {
354 /**
355 * Filter the main admin menu title
356 *
357 * @return string
358 */
359 $main_menu_title = apply_filters( 'wp_stream_admin_menu_title', esc_html__( 'Stream', 'stream' ) );
360
361 /**
362 * Filter the main admin menu position
363 *
364 * Note: Using longtail decimal string to reduce the chance of position conflicts, see Codex
365 *
366 * @return string
367 */
368 $main_menu_position = apply_filters( 'wp_stream_menu_position', '2.999999' );
369
370 /**
371 * Filter the main admin page title
372 *
373 * @return string
374 */
375 $main_page_title = apply_filters( 'wp_stream_admin_page_title', esc_html__( 'Stream Records', 'stream' ) );
376
377 $this->screen_id['main'] = add_menu_page(
378 $main_page_title,
379 $main_menu_title,
380 $this->view_cap,
381 $this->records_page_slug,
382 array( $this, 'render_list_table' ),
383 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIGZpbGw9IjAwMCI+Cgk8cGF0aCBkPSJNOTAzLjExNSA1MTUuNDEzYy00OS4zOTIgMC05MS40NzQgMzEuMzM3LTEwNy40NiA3NS4yMDNsLTEyNC40MTEtMS41MzJjLTExLjM3Ny0uMzQ2LTIyLjc1MS0uNjg5LTM0LjEyOS0uOTk4bC0uMjQxLjU3NC0yMi40MzYtLjI3OC0uMTUzLS45Mi0xNS4wNTYtODIuOTMzLTIwLjE0Ni0xMDguNDA1LTIwLjU0NC0xMDguMzM3TDUwMy45ODIgMGwtNTMuMTQxIDQyOS4wMTMtMTYuMjE0IDEzNy45MzUtMTIuMDE2IDEwNi45MjQtMTE3LjI4Ni0yODUuMjItMTguMzUzIDIwMi44MWMtNDIuNTYyIDEuNDU0LTg1LjEyNyAyLjkzNC0xMjcuNjg4IDQuNzM4LTUzLjA5NyAyLjI5Mi0xMDYuMTg3IDQuNDczLTE1OS4yODQgNy41MzZ2NDIuMDQyYzUzLjA5NyAzLjA2IDEwNi4xODcgNS4yNDcgMTU5LjI4NCA3LjUzMyA1My4wOTMgMi4yNDUgMTA2LjE4IDQuMTk0IDE1OS4yNzMgNS45MDNsMTQuMjQuNDY1IDE3LjM1MSA0OC4zOWMxOC44NDIgNTEuODc0IDM3LjU0MiAxMDMuODA2IDU2Ljc2NSAxNTUuNTQxTDQ2Ni41MiAxMDI0bDQxLjUxMi0zMDguMjkzIDE3LjYzMy0xMzYuNjg1IDEwLjc3NiA1MC4zMjkgNTQuODE1IDI0OC41NDQgNzIuNTE2LTIxNy4yMTdoMTI5LjI2MWMxMy40OTMgNDguMTIxIDU3LjY1NSA4My40MjkgMTEwLjA3NSA4My40MjkgNjMuMTYgMCAxMTQuMzUyLTUxLjIwNSAxMTQuMzUyLTExNC4zNDggMC02My4xMzktNTEuMTg5LTExNC4zNDUtMTE0LjM0OS0xMTQuMzQ1bC4wMDQtLjAwMVoiIC8+Cjwvc3ZnPgo=',
384 $main_menu_position
385 );
386
387 /**
388 * Fires before submenu items are added to the Stream menu
389 * allowing plugins to add menu items before Settings
390 *
391 * @return void
392 */
393 do_action( 'wp_stream_admin_menu' );
394
395 /**
396 * Filter the Settings admin page title
397 *
398 * @return string
399 */
400 $settings_page_title = apply_filters( 'wp_stream_settings_form_title', esc_html__( 'Stream Settings', 'stream' ) );
401
402 $this->screen_id['settings'] = add_submenu_page(
403 $this->records_page_slug,
404 $settings_page_title,
405 esc_html__( 'Settings', 'stream' ),
406 $this->settings_cap,
407 $this->settings_page_slug,
408 array( $this, 'render_settings_page' )
409 );
410
411 if ( isset( $this->screen_id['main'] ) ) {
412 /**
413 * Fires just before the Stream list table is registered.
414 *
415 * @return void
416 */
417 do_action( 'wp_stream_admin_menu_screens' );
418
419 // Register the list table early, so it associates the column headers with 'Screen settings'.
420 add_action(
421 'load-' . $this->screen_id['main'],
422 array(
423 $this,
424 'register_list_table',
425 )
426 );
427 }
428 }
429
430 /**
431 * Enqueue scripts/styles for admin screen
432 *
433 * @action admin_enqueue_scripts
434 *
435 * @param string $hook Current hook.
436 *
437 * @return void
438 */
439 public function admin_enqueue_scripts( $hook ) {
440 if ( in_array( $hook, $this->screen_id, true ) ) {
441 $this->plugin->enqueue_asset(
442 'admin',
443 array(
444 $this->plugin->with_select2(),
445 $this->plugin->with_jquery_timeago(),
446 ),
447 array(
448 'i18n' => array(
449 'confirm_purge' => __( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
450 'confirm_defaults' => __( 'Are you sure you want to reset all site settings to default? This cannot be undone.', 'stream' ),
451 ),
452 'locale' => strtolower( substr( get_locale(), 0, 2 ) ),
453 'gmt_offset' => get_option( 'gmt_offset' ),
454 )
455 );
456
457 $this->plugin->enqueue_asset(
458 'admin-exclude',
459 array(
460 $this->plugin->with_select2(),
461 )
462 );
463
464 $this->plugin->enqueue_asset(
465 'live-updates',
466 array( 'heartbeat' ),
467 array(
468 'current_screen' => $hook,
469 'current_page' => isset( $_GET['paged'] ) ? absint( wp_unslash( $_GET['paged'] ) ) : '1', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
470 'current_order' => isset( $_GET['order'] ) && in_array( strtolower( $_GET['order'] ), array( 'asc', 'desc' ), true ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
471 ? esc_js( $_GET['order'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended
472 : 'desc',
473 'current_query' => wp_json_encode( $_GET ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
474 'current_query_count' => count( $_GET ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
475 )
476 );
477 }
478
479 /**
480 * The maximum number of items that can be updated in bulk without receiving a warning.
481 *
482 * Stream watches for bulk actions performed in the WordPress Admin (such as updating
483 * many posts at once) and warns the user before proceeding if the number of items they
484 * are attempting to update exceeds this threshold value. Since Stream will try to save
485 * a log for each item, it will take longer than usual to complete the operation.
486 *
487 * The default threshold is 100 items.
488 *
489 * @return int
490 */
491 $bulk_actions_threshold = apply_filters( 'wp_stream_bulk_actions_threshold', 100 );
492
493 $this->plugin->enqueue_asset(
494 'global',
495 array(),
496 array(
497 'bulk_actions' => array(
498 'i18n' => array(
499 /* translators: %s: a number of items (e.g. "1,742") */
500 '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 ) ) ),
501 ),
502 'threshold' => absint( $bulk_actions_threshold ),
503 ),
504 'plugins_screen_url' => self_admin_url( 'plugins.php#stream' ),
505 )
506 );
507 }
508
509 /**
510 * Check whether or not the current admin screen belongs to Stream
511 *
512 * @return bool
513 */
514 public function is_stream_screen() {
515 if ( ! is_admin() ) {
516 return false;
517 }
518
519 $page = wp_stream_filter_input( INPUT_GET, 'page' );
520 if ( is_string( $page ) && false !== strpos( $page, $this->records_page_slug ) ) {
521 return true;
522 }
523
524 if ( is_admin() && function_exists( 'get_current_screen' ) ) {
525 $screen = get_current_screen();
526
527 return ( Alerts::POST_TYPE === $screen->post_type );
528 }
529
530 return false;
531 }
532
533 /**
534 * Add a specific body class to all Stream admin screens
535 *
536 * @param string $classes CSS classes to output to body.
537 *
538 * @filter admin_body_class
539 *
540 * @return string
541 */
542 public function admin_body_class( $classes ) {
543 $stream_classes = array();
544
545 if ( $this->is_stream_screen() ) {
546 $stream_classes[] = $this->admin_body_class;
547
548 if ( isset( $_GET['page'] ) ) { // // phpcs:ignore WordPress.Security.NonceVerification.Recommended
549 $stream_classes[] = sanitize_key( $_GET['page'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
550 }
551 }
552
553 /**
554 * Filter the Stream admin body classes
555 *
556 * @return array
557 */
558 $stream_classes = apply_filters( 'wp_stream_admin_body_classes', $stream_classes );
559 $stream_classes = implode( ' ', array_map( 'trim', $stream_classes ) );
560
561 return sprintf( '%s %s ', $classes, $stream_classes );
562 }
563
564 /**
565 * Add menu styles for various WP Admin skins.
566 *
567 * @action admin_enqueue_scripts
568 */
569 public function admin_menu_css() {
570 // Make sure we're working off a clean version.
571 if ( ! file_exists( ABSPATH . WPINC . '/version.php' ) ) {
572 return;
573 }
574 include ABSPATH . WPINC . '/version.php';
575
576 if ( ! isset( $wp_version ) ) {
577 return;
578 }
579
580 $css = "
581 body.{$this->admin_body_class} #wpbody-content .wrap h1:nth-child(1):before {
582 content: '';
583 display: inline-block;
584 width: 24px;
585 height: 24px;
586 margin-right: 8px;
587 vertical-align: text-bottom;
588 background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiIGZpbGw9ImN1cnJlbnRjb2xvciI+Cgk8cGF0aCBkPSJNOTAzLjExNSA1MTUuNDEzYy00OS4zOTIgMC05MS40NzQgMzEuMzM3LTEwNy40NiA3NS4yMDNsLTEyNC40MTEtMS41MzJjLTExLjM3Ny0uMzQ2LTIyLjc1MS0uNjg5LTM0LjEyOS0uOTk4bC0uMjQxLjU3NC0yMi40MzYtLjI3OC0uMTUzLS45Mi0xNS4wNTYtODIuOTMzLTIwLjE0Ni0xMDguNDA1LTIwLjU0NC0xMDguMzM3TDUwMy45ODIgMGwtNTMuMTQxIDQyOS4wMTMtMTYuMjE0IDEzNy45MzUtMTIuMDE2IDEwNi45MjQtMTE3LjI4Ni0yODUuMjItMTguMzUzIDIwMi44MWMtNDIuNTYyIDEuNDU0LTg1LjEyNyAyLjkzNC0xMjcuNjg4IDQuNzM4LTUzLjA5NyAyLjI5Mi0xMDYuMTg3IDQuNDczLTE1OS4yODQgNy41MzZ2NDIuMDQyYzUzLjA5NyAzLjA2IDEwNi4xODcgNS4yNDcgMTU5LjI4NCA3LjUzMyA1My4wOTMgMi4yNDUgMTA2LjE4IDQuMTk0IDE1OS4yNzMgNS45MDNsMTQuMjQuNDY1IDE3LjM1MSA0OC4zOWMxOC44NDIgNTEuODc0IDM3LjU0MiAxMDMuODA2IDU2Ljc2NSAxNTUuNTQxTDQ2Ni41MiAxMDI0bDQxLjUxMi0zMDguMjkzIDE3LjYzMy0xMzYuNjg1IDEwLjc3NiA1MC4zMjkgNTQuODE1IDI0OC41NDQgNzIuNTE2LTIxNy4yMTdoMTI5LjI2MWMxMy40OTMgNDguMTIxIDU3LjY1NSA4My40MjkgMTEwLjA3NSA4My40MjkgNjMuMTYgMCAxMTQuMzUyLTUxLjIwNSAxMTQuMzUyLTExNC4zNDggMC02My4xMzktNTEuMTg5LTExNC4zNDUtMTE0LjM0OS0xMTQuMzQ1bC4wMDQtLjAwMVoiIC8+Cjwvc3ZnPgo=');
589 }
590 #menu-posts-feedback .wp-menu-image:before {
591 font-family: dashicons !important;
592 content: '\\f175';
593 }
594 #adminmenu #menu-posts-feedback div.wp-menu-image {
595 background: none !important;
596 background-repeat: no-repeat;
597 }
598 ";
599
600 wp_add_inline_style( 'wp-admin', $css );
601 }
602
603 /**
604 * Handle the reset AJAX request to reset logs.
605 *
606 * @return bool
607 */
608 public function wp_ajax_reset() {
609 check_ajax_referer( 'stream_nonce_reset', 'wp_stream_nonce_reset' );
610
611 if ( ! current_user_can( $this->settings_cap ) ) {
612 wp_die(
613 esc_html__( "You don't have sufficient privileges to do this action.", 'stream' )
614 );
615 }
616
617 $this->erase_stream_records();
618
619 if ( defined( 'WP_STREAM_TESTS' ) && WP_STREAM_TESTS ) {
620 return true;
621 }
622
623 wp_safe_redirect(
624 add_query_arg(
625 array(
626 'page' => is_network_admin() ? $this->network->network_settings_page_slug : $this->settings_page_slug,
627 'message' => 'data_erased',
628 ),
629 self_admin_url( $this->admin_parent_page )
630 )
631 );
632
633 exit;
634 }
635
636 /**
637 * Clears stream records from the database.
638 *
639 * @return void
640 */
641 private function erase_stream_records() {
642 global $wpdb;
643
644 // If this is a multisite and it's not network activated,
645 // only delete the entries from the blog which made the request.
646 if ( $this->plugin->is_multisite_not_network_activated() ) {
647
648 // First check the log size.
649 $stream_log_size = self::get_blog_record_table_size();
650
651 // If this is a large log and we need to delete only the entries
652 // pertaining to an individual site, we will need to do those in batches.
653 if ( $this->plugin->is_large_records_table( $stream_log_size ) ) {
654 $this->schedule_erase_large_records( $stream_log_size );
655 return;
656 }
657
658 $wpdb->query(
659 $wpdb->prepare(
660 "DELETE `stream`, `meta`
661 FROM {$wpdb->stream} AS `stream`
662 LEFT JOIN {$wpdb->streammeta} AS `meta`
663 ON `meta`.`record_id` = `stream`.`ID`
664 WHERE `blog_id`=%d;",
665 get_current_blog_id()
666 )
667 );
668 } else {
669 // If we are deleting all the entries, we can truncate the tables.
670 $wpdb->query( "TRUNCATE {$wpdb->streammeta};" );
671 $wpdb->query( "TRUNCATE {$wpdb->stream};" );
672 // Tidy up any meta which may have been added in between the two truncations.
673 $this->delete_orphaned_meta();
674 }
675 }
676
677 /**
678 * Schedule the initial event to start erasing the logs from now.
679 *
680 * @param int $log_size The number of rows which will be affected.
681 * @return void
682 */
683 private function schedule_erase_large_records( int $log_size ) {
684 global $wpdb;
685
686 $last_entry = $wpdb->get_var(
687 $wpdb->prepare(
688 "SELECT ID FROM {$wpdb->stream} WHERE `blog_id`=%d ORDER BY ID DESC LIMIT 1",
689 get_current_blog_id()
690 )
691 );
692
693 // If there are no entries to erase, don't try to erase them.
694 if ( empty( $last_entry ) ) {
695 return;
696 }
697
698 // We are going to delete this many and this many only.
699 // This is to avoid the situation where rows keep getting added
700 // between the Action Scheduler runs and they never stop.
701 $args = array(
702 'total' => (int) $log_size,
703 'done' => 0,
704 'last_entry' => (int) $last_entry,
705 'blog_id' => (int) get_current_blog_id(),
706 );
707
708 as_enqueue_async_action( self::ASYNC_DELETION_ACTION, $args );
709 }
710
711 /**
712 * Checks if the async deletion process is running.
713 *
714 * @return bool True if the async deletion process is running, false otherwise.
715 */
716 public static function is_running_async_deletion() {
717 return as_has_scheduled_action( self::ASYNC_DELETION_ACTION );
718 }
719
720 /**
721 * Erases large records from the stream table.
722 *
723 * This function deletes records from the stream table in batches, starting from a given entry ID.
724 * It deletes records in reverse chronological order, starting from the largest ID and going back.
725 * The number of records deleted in each batch is determined by the batch size, which can be filtered
726 * using the 'wp_stream_batch_size' hook.
727 *
728 * @param int $total The total number of records to be deleted.
729 * @param int $done The number of records that have already been deleted.
730 * @param int $last_entry The ID of the last entry that was deleted.
731 * @param int $blog_id The ID of the blog for which the records should be deleted.
732 * @return void
733 */
734 public function erase_large_records( int $total, int $done, int $last_entry, int $blog_id ) {
735 global $wpdb;
736
737 $start_from = $wpdb->get_var(
738 $wpdb->prepare(
739 "SELECT ID FROM {$wpdb->stream} WHERE ID < %d AND `blog_id`=%d ORDER BY ID DESC LIMIT 1",
740 $last_entry + 1, // A tweak to get it correct the first time through.
741 get_current_blog_id()
742 )
743 );
744
745 if ( empty( $start_from ) ) {
746 return;
747 }
748
749 /**
750 * Filters the number of records in the {$wpdb->stream} table to do at a time.
751 *
752 * @since 4.1.0
753 *
754 * @param int $batch_size The batch size, default 250000.
755 */
756 $batch_size = apply_filters( 'wp_stream_batch_size', 250000 );
757
758 // This will tend to erase them in reverse chronological order,
759 // ie it will start from the largest ID and go back from there.
760 $wpdb->query(
761 $wpdb->prepare(
762 "DELETE `stream`, `meta`
763 FROM {$wpdb->stream} AS `stream`
764 LEFT JOIN {$wpdb->streammeta} AS `meta`
765 ON `meta`.`record_id` = `stream`.`ID`
766 WHERE ID <= %d AND ID >= %d AND `blog_id`=%d;",
767 $start_from,
768 $start_from - $batch_size,
769 get_current_blog_id()
770 )
771 );
772
773 $remaining = $wpdb->get_var(
774 $wpdb->prepare( "SELECT COUNT(ID) FROM {$wpdb->stream} WHERE `blog_id`=%d", $blog_id )
775 );
776
777 $done = $total - $remaining;
778
779 as_enqueue_async_action(
780 self::ASYNC_DELETION_ACTION,
781 array(
782 'total' => (int) $total,
783 'done' => (int) $done,
784 'last_entry' => (int) $start_from - $batch_size, // The last ID checked.
785 'blog_id' => (int) $blog_id,
786 )
787 );
788 }
789
790 /**
791 * Retrieves the size of the blog record table for a specific blog.
792 *
793 * @param int|null $blog_id The ID of the blog. If not provided, the current blog ID will be used.
794 * @return int The size of the blog record table.
795 */
796 public static function get_blog_record_table_size( $blog_id = null ): int {
797 global $wpdb;
798
799 $blog_id = empty( $blog_id ) ? get_current_blog_id() : $blog_id;
800
801 $blog_size = $wpdb->get_var(
802 $wpdb->prepare(
803 "SELECT COUNT(ID) FROM {$wpdb->stream} WHERE `blog_id`=%d",
804 $blog_id
805 )
806 );
807
808 return (int) $blog_size;
809 }
810
811 /**
812 * Schedules a purge of records.
813 *
814 * @return void
815 */
816 public function purge_schedule_setup() {
817 if ( ! wp_next_scheduled( 'wp_stream_auto_purge' ) ) {
818 wp_schedule_event( time(), 'twicedaily', 'wp_stream_auto_purge' );
819 }
820 }
821
822 /**
823 * Deletes orphaned meta records from the database.
824 *
825 * Deletes meta records from the stream meta table where the corresponding
826 * stream record no longer exists.
827 *
828 * @global wpdb $wpdb The WordPress database object.
829 */
830 private function delete_orphaned_meta() {
831 global $wpdb;
832
833 $wpdb->query(
834 "DELETE `meta` FROM {$wpdb->streammeta} as `meta` LEFT JOIN {$wpdb->stream} as `stream` ON `stream`.`ID`=`meta`.`record_id` WHERE `stream`.`ID` IS NULL"
835 );
836 }
837
838 /**
839 * Executes a scheduled purge
840 *
841 * @return void
842 */
843 public function purge_scheduled_action() {
844 global $wpdb;
845
846 // Don't purge when in Network Admin unless Stream is network activated.
847 if (
848 $this->plugin->is_multisite_not_network_activated()
849 &&
850 is_network_admin()
851 ) {
852 return;
853 }
854
855 $defaults = $this->plugin->settings->get_defaults();
856 if ( $this->plugin->is_multisite_network_activated() ) {
857 $options = (array) get_site_option( 'wp_stream_network', $defaults );
858 } else {
859 $options = (array) get_option( 'wp_stream', $defaults );
860 }
861
862 if ( ! empty( $options['general_keep_records_indefinitely'] ) || ! isset( $options['general_records_ttl'] ) ) {
863 return;
864 }
865
866 $days = $options['general_records_ttl'];
867 $timezone = new DateTimeZone( 'UTC' );
868 $date = new DateTime( 'now', $timezone );
869
870 $date->sub( DateInterval::createFromDateString( "$days days" ) );
871
872 $where = $wpdb->prepare( ' AND `stream`.`created` < %s', $date->format( 'Y-m-d H:i:s' ) );
873
874 // Multisite but NOT network activated, only purge the current blog.
875 if ( $this->plugin->is_multisite_not_network_activated() ) {
876 $where .= $wpdb->prepare( ' AND `blog_id` = %d', get_current_blog_id() );
877 }
878
879 $wpdb->query(
880 "DELETE `stream`, `meta`
881 FROM {$wpdb->stream} AS `stream`
882 LEFT JOIN {$wpdb->streammeta} AS `meta`
883 ON `meta`.`record_id` = `stream`.`ID`
884 WHERE 1=1 {$where};", // @codingStandardsIgnoreLine $where already prepared
885 );
886 }
887
888 /**
889 * Returns the admin action links.
890 *
891 * @filter plugin_action_links
892 *
893 * @param array $links Action links.
894 * @param string $file Plugin file.
895 *
896 * @return array
897 */
898 public function plugin_action_links( $links, $file ) {
899 if ( plugin_basename( $this->plugin->locations['dir'] . 'stream.php' ) !== $file ) {
900 return $links;
901 }
902
903 // Also don't show links in Network Admin if Stream isn't network enabled.
904 if ( is_network_admin() && $this->plugin->is_multisite_not_network_activated() ) {
905 return $links;
906 }
907
908 if ( is_network_admin() ) {
909 $admin_page_url = add_query_arg(
910 array(
911 'page' => $this->network->network_settings_page_slug,
912 ),
913 network_admin_url( $this->admin_parent_page )
914 );
915 } else {
916 $admin_page_url = add_query_arg(
917 array(
918 'page' => $this->settings_page_slug,
919 ),
920 admin_url( $this->admin_parent_page )
921 );
922 }
923
924 $links[] = sprintf( '<a href="%s">%s</a>', esc_url( $admin_page_url ), esc_html__( 'Settings', 'default' ) );
925
926 return $links;
927 }
928
929 /**
930 * Render main page
931 */
932 public function render_list_table() {
933 $this->list_table->prepare_items();
934 ?>
935 <div class="wrap">
936 <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
937 <?php $this->list_table->display(); ?>
938 </div>
939 <?php
940 }
941
942 /**
943 * Render settings page
944 */
945 public function render_settings_page() {
946 $option_key = $this->plugin->settings->option_key;
947 $form_action = apply_filters( 'wp_stream_settings_form_action', admin_url( 'options.php' ) );
948
949 $page_description = apply_filters( 'wp_stream_settings_form_description', '' );
950
951 $sections = $this->plugin->settings->get_fields();
952 $active_tab = wp_stream_filter_input( INPUT_GET, 'tab' );
953
954 $this->plugin->enqueue_asset(
955 'settings',
956 array(),
957 array(
958 'i18n' => array(
959 'confirm_purge' => __( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
960 ),
961 )
962 );
963 ?>
964 <div class="wrap">
965 <h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
966
967 <?php if ( ! empty( $page_description ) ) : ?>
968 <p><?php echo esc_html( $page_description ); ?></p>
969 <?php endif; ?>
970
971 <?php settings_errors(); ?>
972
973 <?php if ( count( $sections ) > 1 ) : ?>
974 <h2 class="nav-tab-wrapper">
975 <?php $i = 0; ?>
976 <?php foreach ( $sections as $section => $data ) : ?>
977 <?php ++$i; ?>
978 <?php $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section ); ?>
979 <a href="<?php echo esc_url( add_query_arg( 'tab', $section ) ); ?>" class="nav-tab <?php echo $is_active ? esc_attr( ' nav-tab-active' ) : ''; ?>">
980 <?php echo esc_html( $data['title'] ); ?>
981 </a>
982 <?php endforeach; ?>
983 </h2>
984 <?php endif; ?>
985
986 <div class="nav-tab-content" id="tab-content-settings">
987 <form method="post" action="<?php echo esc_attr( $form_action ); ?>" enctype="multipart/form-data">
988 <div class="settings-sections">
989 <?php
990 $i = 0;
991 foreach ( $sections as $section => $data ) {
992 ++$i;
993
994 $is_active = ( ( 1 === $i && ! $active_tab ) || $active_tab === $section );
995
996 if ( $is_active ) {
997 settings_fields( $option_key );
998 do_settings_sections( $option_key );
999 }
1000 }
1001 ?>
1002 </div>
1003 <?php submit_button(); ?>
1004 </form>
1005 </div>
1006 </div>
1007 <?php
1008 }
1009
1010 /**
1011 * Instantiate the list table
1012 */
1013 public function register_list_table() {
1014 $this->list_table = new List_Table(
1015 $this->plugin,
1016 array(
1017 'screen' => $this->screen_id['main'],
1018 )
1019 );
1020 }
1021
1022 /**
1023 * Check if a particular role has access
1024 *
1025 * @param string $role User role.
1026 *
1027 * @return bool
1028 */
1029 private function role_can_view( $role ) {
1030 if ( in_array( $role, $this->plugin->settings->options['general_role_access'], true ) ) {
1031 return true;
1032 }
1033
1034 return false;
1035 }
1036
1037 /**
1038 * Filter user caps to dynamically grant our view cap based on allowed roles
1039 *
1040 * @param array $allcaps All capabilities.
1041 * @param array $caps Required caps.
1042 * @param array $args Unused.
1043 * @param WP_User $user User.
1044 *
1045 * @filter user_has_cap
1046 *
1047 * @return array
1048 */
1049 public function filter_user_caps( $allcaps, $caps, $args, $user = null ) {
1050 global $wp_roles;
1051
1052 $_wp_roles = isset( $wp_roles ) ? $wp_roles : new WP_Roles();
1053
1054 $user = is_a( $user, 'WP_User' ) ? $user : wp_get_current_user();
1055
1056 // @see
1057 // https://github.com/WordPress/WordPress/blob/c67c9565f1495255807069fdb39dac914046b1a0/wp-includes/capabilities.php#L758
1058 $roles = array_unique(
1059 array_merge(
1060 $user->roles,
1061 array_filter(
1062 array_keys( $user->caps ),
1063 array( $_wp_roles, 'is_role' )
1064 )
1065 )
1066 );
1067
1068 $stream_view_caps = array( $this->view_cap );
1069
1070 foreach ( $caps as $cap ) {
1071 if ( in_array( $cap, $stream_view_caps, true ) ) {
1072 foreach ( $roles as $role ) {
1073 if ( $this->role_can_view( $role ) ) {
1074 $allcaps[ $cap ] = true;
1075
1076 break 2;
1077 }
1078 }
1079 }
1080 }
1081
1082 return $allcaps;
1083 }
1084
1085 /**
1086 * Filter role caps to dynamically grant our view cap based on allowed roles
1087 *
1088 * @filter role_has_cap
1089 *
1090 * @param array $allcaps All capabilities.
1091 * @param string $cap Require cap.
1092 * @param string $role User role.
1093 *
1094 * @return array
1095 */
1096 public function filter_role_caps( $allcaps, $cap, $role ) {
1097 $stream_view_caps = array( $this->view_cap );
1098
1099 if ( in_array( $cap, $stream_view_caps, true ) && $this->role_can_view( $role ) ) {
1100 $allcaps[ $cap ] = true;
1101 }
1102
1103 return $allcaps;
1104 }
1105
1106 /**
1107 * Ajax callback for return a user list.
1108 *
1109 * @action wp_ajax_wp_stream_filters
1110 */
1111 public function ajax_filters() {
1112 if ( ! defined( 'DOING_AJAX' ) || ! current_user_can( $this->plugin->admin->settings_cap ) ) {
1113 wp_die( '-1' );
1114 }
1115
1116 check_ajax_referer( 'stream_filters_user_search_nonce', 'nonce' );
1117
1118 switch ( wp_stream_filter_input( INPUT_GET, 'filter' ) ) {
1119 case 'user_id':
1120 $users = array_merge(
1121 array(
1122 0 => (object) array(
1123 'display_name' => 'WP-CLI',
1124 ),
1125 ),
1126 get_users()
1127 );
1128
1129 $search = wp_stream_filter_input( INPUT_GET, 'q' );
1130 if ( $search ) {
1131 // `search` arg for get_users() is not enough
1132 $users = array_filter(
1133 $users,
1134 function ( $user ) use ( $search ) {
1135 return false !== mb_strpos( mb_strtolower( $user->display_name ), mb_strtolower( $search ) );
1136 }
1137 );
1138 }
1139
1140 if ( count( $users ) > $this->preload_users_max ) {
1141 $users = array_slice( $users, 0, $this->preload_users_max );
1142 }
1143
1144 // Get gravatar / roles for final result set.
1145 $results = $this->get_users_record_meta( $users );
1146
1147 break;
1148 }
1149
1150 if ( isset( $results ) ) {
1151 echo wp_json_encode( $results );
1152 }
1153
1154 die();
1155 }
1156
1157 /**
1158 * Return relevant user meta data.
1159 *
1160 * @param array $authors Author data.
1161 * @return array
1162 */
1163 public function get_users_record_meta( $authors ) {
1164 $authors_records = array();
1165
1166 foreach ( $authors as $user_id => $args ) {
1167 $author = new Author( $args->ID );
1168
1169 $authors_records[ $user_id ] = array(
1170 'text' => $author->get_display_name(),
1171 'id' => $author->id,
1172 'label' => $author->get_display_name(),
1173 'icon' => $author->get_avatar_src( 32 ),
1174 'title' => '',
1175 );
1176 }
1177
1178 return $authors_records;
1179 }
1180
1181 /**
1182 * Get user meta in a way that is also safe for VIP
1183 *
1184 * @param int $user_id User ID.
1185 * @param string $meta_key Meta key.
1186 * @param bool $single Return first found meta value connected to the meta key (optional).
1187 *
1188 * @return mixed
1189 */
1190 public function get_user_meta( $user_id, $meta_key, $single = true ) {
1191 return get_user_meta( $user_id, $meta_key, $single );
1192 }
1193
1194 /**
1195 * Update user meta in a way that is also safe for VIP
1196 *
1197 * @param int $user_id User ID.
1198 * @param string $meta_key Meta key.
1199 * @param mixed $meta_value Meta value.
1200 * @param mixed $prev_value Previous meta value being overwritten (optional).
1201 *
1202 * @return int|bool
1203 */
1204 public function update_user_meta( $user_id, $meta_key, $meta_value, $prev_value = '' ) {
1205 return update_user_meta( $user_id, $meta_key, $meta_value, $prev_value );
1206 }
1207
1208 /**
1209 * Delete user meta in a way that is also safe for VIP
1210 *
1211 * @param int $user_id User ID.
1212 * @param string $meta_key Meta key.
1213 * @param mixed $meta_value Meta value (optional).
1214 *
1215 * @return bool
1216 */
1217 public function delete_user_meta( $user_id, $meta_key, $meta_value = '' ) {
1218 return delete_user_meta( $user_id, $meta_key, $meta_value );
1219 }
1220 }
1221