PluginProbe
Stream – Activity Log & Audit Trail / 4.3.0
Stream – Activity Log & Audit Trail v4.3.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-settings.php

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

1,453 lines 44.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Renders and manages the plugin Settings page.
4 *
5 * @package WP_Stream
6 */
7
8 namespace WP_Stream;
9
10 use WP_Roles;
11 use WP_User;
12 use WP_User_Query;
13
14 /**
15 * Class - Settings
16 */
17 class Settings {
18
19 /**
20 * Holds instance of plugin object
21 *
22 * @var Plugin
23 */
24 public $plugin;
25
26 /**
27 * Settings key/identifier
28 *
29 * @var string
30 */
31 public $option_key = 'wp_stream';
32
33 /**
34 * Network settings key/identifier
35 *
36 * @var string
37 */
38 public $network_options_key = 'wp_stream_network';
39
40 /**
41 * Plugin settings
42 *
43 * @var array
44 */
45 public $options = array();
46
47 /**
48 * Settings fields
49 *
50 * @var array
51 */
52 public $fields = array();
53
54 /**
55 * Class constructor.
56 *
57 * @param Plugin $plugin Instance of plugin object.
58 */
59 public function __construct( $plugin ) {
60 $this->plugin = $plugin;
61
62 $this->option_key = $this->get_option_key();
63 $this->options = $this->get_options();
64
65 // Register settings, and fields.
66 add_action( 'admin_init', array( $this, 'register_settings' ) );
67
68 // Remove records when records TTL is shortened.
69 add_action(
70 'update_option_' . $this->option_key,
71 array(
72 $this,
73 'updated_option_ttl_remove_records',
74 ),
75 10,
76 2
77 );
78
79 // Apply label translations for settings.
80 add_filter(
81 'wp_stream_serialized_labels',
82 array(
83 $this,
84 'get_settings_translations',
85 )
86 );
87
88 // Ajax callback function to search users.
89 add_action( 'wp_ajax_stream_get_users', array( $this, 'get_users' ) );
90
91 // Ajax callback function to search IPs.
92 add_action( 'wp_ajax_stream_get_ips', array( $this, 'get_ips' ) );
93 }
94
95 /**
96 * Ajax callback function to search users, used on exclude setting page
97 *
98 * @uses \WP_User_Query
99 */
100 public function get_users() {
101 if ( ! defined( 'DOING_AJAX' ) || ! current_user_can( $this->plugin->admin->settings_cap ) ) {
102 return;
103 }
104
105 check_ajax_referer( 'stream_get_users', 'nonce' );
106
107 $response = (object) array(
108 'status' => false,
109 'message' => esc_html__( 'There was an error in the request', 'stream' ),
110 );
111
112 $search = '';
113 $input = wp_stream_filter_input( INPUT_POST, 'find' );
114
115 if ( isset( $input['term'] ) ) {
116 $search = wp_unslash( trim( $input['term'] ) );
117 }
118
119 $request = (object) array(
120 'find' => $search,
121 );
122
123 add_filter(
124 'user_search_columns',
125 array(
126 $this,
127 'add_display_name_search_columns',
128 ),
129 10,
130 3
131 );
132
133 $users = new WP_User_Query(
134 array(
135 'search' => "*{$request->find}*",
136 'search_columns' => array(
137 'user_login',
138 'user_nicename',
139 'user_email',
140 'user_url',
141 ),
142 'orderby' => 'display_name',
143 'number' => $this->plugin->admin->preload_users_max,
144 )
145 );
146
147 remove_filter(
148 'user_search_columns',
149 array(
150 $this,
151 'add_display_name_search_columns',
152 ),
153 10
154 );
155
156 if ( 0 === $users->get_total() ) {
157 wp_send_json_error( $response );
158 }
159 $users_array = $users->results;
160
161 if ( is_multisite() && is_super_admin() ) {
162 $super_admins = get_super_admins();
163 foreach ( $super_admins as $admin ) {
164 $user = get_user_by( 'login', $admin );
165 $users_array[] = $user;
166 }
167 }
168
169 $response->status = true;
170 $response->message = '';
171 $response->roles = $this->get_roles();
172 $response->users = array();
173 $users_added_to_response = array();
174
175 foreach ( $users_array as $key => $user ) {
176 // exclude duplications.
177 if ( array_key_exists( $user->ID, $users_added_to_response ) ) {
178 continue;
179 } else {
180 $users_added_to_response[ $user->ID ] = true;
181 }
182
183 $author = new Author( $user->ID );
184
185 $args = array(
186 'id' => $author->ID,
187 'text' => $author->display_name,
188 );
189
190 $args['tooltip'] = esc_attr(
191 sprintf(
192 /* translators: %1$d: user ID, %2$s: username, %3$s: email, %4$s: user role (e.g. "42", "administrator", "foo@bar.com", "subscriber") */
193 __( 'ID: %1$d\nUser: %2$s\nEmail: %3$s\nRole: %4$s', 'stream' ),
194 $author->id,
195 $author->user_login,
196 $author->user_email,
197 ucwords( $author->get_role() )
198 )
199 );
200
201 $args['icon'] = $author->get_avatar_src( 32 );
202
203 $response->users[] = $args;
204 }
205
206 usort(
207 $response->users,
208 function ( $a, $b ) {
209 return strcmp( $a['text'], $b['text'] );
210 }
211 );
212
213 if ( empty( $search ) || preg_match( '/wp|cli|system|unknown/i', $search ) ) {
214 $author = new Author( 0 );
215 $response->users[] = array(
216 'id' => '0',
217 'text' => $author->get_display_name(),
218 'icon' => $author->get_avatar_src( 32 ),
219 'tooltip' => esc_html__( 'Actions performed by the system when a user is not logged in (e.g. auto site upgrader, or invoking WP-CLI without --user)', 'stream' ),
220 );
221 }
222
223 wp_send_json_success( $response );
224 }
225
226 /**
227 * Ajax callback function to search IP addresses, used on exclude setting page
228 */
229 public function get_ips() {
230 if ( ! defined( 'DOING_AJAX' ) || ! current_user_can( $this->plugin->admin->settings_cap ) ) {
231 return;
232 }
233
234 check_ajax_referer( 'stream_get_ips', 'nonce' );
235
236 $ips = $this->plugin->db->existing_records( 'ip' );
237 $find = wp_stream_filter_input( INPUT_POST, 'find' );
238
239 if ( isset( $find['term'] ) && '' !== $find['term'] ) {
240 $ips = array_filter(
241 $ips,
242 function ( $ip ) use ( $find ) {
243 return 0 === strpos( $ip, $find['term'] );
244 }
245 );
246 }
247
248 if ( $ips ) {
249 wp_send_json_success( $ips );
250 } else {
251 wp_send_json_error();
252 }
253 }
254
255 /**
256 * Filter the columns to search in a WP_User_Query search.
257 *
258 * @param array $search_columns Array of column names to be searched.
259 * @param string $search Text being searched.
260 * @param \WP_User_Query $query current WP_User_Query instance.
261 *
262 * @return array
263 */
264 public function add_display_name_search_columns( $search_columns, $search, $query ) {
265 unset( $search );
266 unset( $query );
267
268 $search_columns[] = 'display_name';
269
270 return $search_columns;
271 }
272
273 /**
274 * Returns the option key
275 *
276 * @return string
277 */
278 public function get_option_key() {
279 $option_key = $this->option_key;
280
281 $current_page = wp_stream_filter_input( INPUT_GET, 'page' );
282
283 if ( ! $current_page ) {
284 $current_page = wp_stream_filter_input( INPUT_GET, 'action' );
285 }
286
287 if ( 'wp_stream_network_settings' === $current_page ) {
288 $option_key = $this->network_options_key;
289 }
290
291 return apply_filters( 'wp_stream_settings_option_key', $option_key );
292 }
293
294 /**
295 * Return settings fields
296 *
297 * @return array
298 */
299 public function get_fields() {
300 $fields = array(
301 'general' => array(
302 'title' => esc_html__( 'General', 'stream' ),
303 'fields' => array(
304 array(
305 'name' => 'role_access',
306 'title' => esc_html__( 'Role Access', 'stream' ),
307 'type' => 'multi_checkbox',
308 'desc' => esc_html__( 'Users from the selected roles above will have permission to view Stream Records. However, only site Administrators can access Stream Settings.', 'stream' ),
309 'choices' => $this->get_roles(),
310 'default' => array( 'administrator' ),
311 ),
312 array(
313 'name' => 'records_ttl',
314 'title' => esc_html__( 'Keep Records for', 'stream' ),
315 'type' => 'number',
316 'class' => 'small-text',
317 'desc' => esc_html__( 'Maximum number of days to keep activity records.', 'stream' ),
318 'default' => 30,
319 'min' => 1,
320 'max' => 999,
321 'step' => 1,
322 'after_field' => esc_html__( 'days', 'stream' ),
323 ),
324 array(
325 'name' => 'keep_records_indefinitely',
326 'title' => esc_html__( 'Keep Records Indefinitely', 'stream' ),
327 'type' => 'checkbox',
328 'desc' => sprintf( '<strong>%s</strong> %s', esc_html__( 'Not recommended.', 'stream' ), esc_html__( 'Purging old records helps to keep your WordPress installation running optimally.', 'stream' ) ),
329 'after_field' => esc_html__( 'Enabled', 'stream' ),
330 'default' => 0,
331 ),
332 ),
333 ),
334 'exclude' => array(
335 'title' => esc_html__( 'Exclude', 'stream' ),
336 'fields' => array(
337 array(
338 'name' => 'rules',
339 'title' => esc_html__( 'Exclude Rules', 'stream' ),
340 'type' => 'rule_list',
341 'desc' => esc_html__( 'Create rules to exclude certain kinds of activity from being recorded by Stream.', 'stream' ),
342 'default' => array(),
343 'nonce' => 'stream_get_ips',
344 ),
345 ),
346 ),
347 'advanced' => array(
348 'title' => esc_html__( 'Advanced', 'stream' ),
349 'fields' => array(
350 array(
351 'name' => 'comment_flood_tracking',
352 'title' => esc_html__( 'Comment Flood Tracking', 'stream' ),
353 'type' => 'checkbox',
354 'desc' => esc_html__( 'WordPress will automatically prevent duplicate comments from flooding the database. By default, Stream does not track these attempts unless you opt-in here. Enabling this is not necessary or recommended for most sites.', 'stream' ),
355 'after_field' => esc_html__( 'Enabled', 'stream' ),
356 'default' => 0,
357 ),
358 $this->build_delete_all_records_field(),
359 $this->build_clean_orphan_meta_field(),
360 ),
361 ),
362 );
363
364 // If Akismet is active, allow Admins to opt-in to Akismet tracking.
365 if ( class_exists( 'Akismet' ) ) {
366 $akismet_tracking = array(
367 'name' => 'akismet_tracking',
368 'title' => esc_html__( 'Akismet Tracking', 'stream' ),
369 'type' => 'checkbox',
370 'desc' => esc_html__( 'Akismet already keeps statistics for comment attempts that it blocks as SPAM. By default, Stream does not track these attempts unless you opt-in here. Enabling this is not necessary or recommended for most sites.', 'stream' ),
371 'after_field' => esc_html__( 'Enabled', 'stream' ),
372 'default' => 0,
373 );
374
375 array_push( $fields['advanced']['fields'], $akismet_tracking );
376 }
377
378 $wp_cron_tracking = array(
379 'name' => 'wp_cron_tracking',
380 'title' => esc_html__( 'WP Cron Tracking', 'stream' ),
381 'type' => 'checkbox',
382 'desc' => esc_html__( 'By default, Stream does not track activity performed by WordPress cron events unless you opt-in here. Enabling this is not necessary or recommended for most sites.', 'stream' ),
383 'after_field' => esc_html__( 'Enabled', 'stream' ),
384 'default' => 0,
385 );
386
387 array_push( $fields['advanced']['fields'], $wp_cron_tracking );
388
389 // Abilities API toggle is only meaningful on WordPress 6.9+. On
390 // network-activated multisite, Abilities::is_enabled() reads the
391 // network option (wp_stream_network), so a per-site checkbox on the
392 // site's own settings screen would be a no-op and misleading. Hide
393 // the field from per-site settings pages, but keep it available in
394 // network admin and in REST/CLI contexts where update_all_setting_values()
395 // routes writes to the network option correctly.
396 $hide_per_site = $this->plugin->is_network_activated() && is_admin() && ! is_network_admin();
397
398 if (
399 class_exists( '\WP_Ability' )
400 && ! $hide_per_site
401 ) {
402 $enable_abilities_api = array(
403 'name' => 'enable_abilities_api',
404 'title' => esc_html__( 'Enable Abilities API and MCP', 'stream' ),
405 'type' => 'checkbox',
406 'desc' => esc_html__( 'Expose Stream operations to AI agents via the WordPress Abilities API (and MCP when the MCP Adapter plugin is installed). Requires WordPress 6.9.', 'stream' ),
407 'after_field' => esc_html__( 'Enabled', 'stream' ),
408 'default' => 0,
409 );
410
411 array_push( $fields['advanced']['fields'], $enable_abilities_api );
412 }
413
414 /**
415 * Filter allows for modification of options fields
416 *
417 * @return array Array of option fields
418 */
419 $this->fields = apply_filters( 'wp_stream_settings_option_fields', $fields );
420
421 // Sort option fields in each tab by title ASC.
422 foreach ( $this->fields as $tab => $options ) {
423 $titles = array();
424
425 foreach ( $options['fields'] as $field ) {
426 $prefix = null;
427
428 if ( ! empty( $field['sticky'] ) ) {
429 $prefix = ( 'bottom' === $field['sticky'] ) ? 'ZZZ' : 'AAA';
430 }
431
432 $titles[] = $prefix . $field['title'];
433 }
434
435 array_multisort( $titles, SORT_ASC, $this->fields[ $tab ]['fields'] );
436 }
437
438 return $this->fields;
439 }
440
441 /**
442 * Build the "Reset Stream Database" settings field definition.
443 *
444 * Extracted so the async-deletion running-state check
445 * ({@see Admin::is_running_async_deletion()}) is evaluated once per render
446 * instead of once per field property, and only in admin context.
447 *
448 * `Settings::__construct` populates `$this->options = $this->get_options()`
449 * on the `init` hook for every pageload, which walks `get_fields()`. The
450 * field is only ever rendered in admin, so outside admin the dynamic state
451 * is irrelevant and the Action Scheduler query is skipped entirely.
452 *
453 * @return array
454 */
455 private function build_delete_all_records_field() {
456 $is_running_deletion = is_admin() ? Admin::is_running_async_deletion() : false;
457
458 return array(
459 'name' => 'delete_all_records',
460 'title' => esc_html__( 'Reset Stream Database', 'stream' ),
461 'type' => $is_running_deletion ? 'none' : 'link',
462 'href' => add_query_arg(
463 array(
464 'action' => 'wp_stream_reset',
465 'wp_stream_nonce_reset' => wp_create_nonce( 'stream_nonce_reset' ),
466 ),
467 admin_url( 'admin-ajax.php' )
468 ),
469 'class' => 'warning',
470 'desc' => esc_html( $this->get_deletion_warning( $is_running_deletion ) ),
471 'default' => 0,
472 'sticky' => 'bottom',
473 );
474 }
475
476 /**
477 * Build the "Clean Orphaned Meta" settings field definition.
478 *
479 * Extracted so the auto-purge running-state check
480 * ({@see Admin::is_running_auto_purge()}) is evaluated once per render
481 * instead of once per field property, and only in admin context — the
482 * field is never rendered outside admin, so the Action Scheduler query
483 * is skipped on front-end pageloads.
484 *
485 * @return array
486 */
487 private function build_clean_orphan_meta_field() {
488 $is_running = is_admin() ? Admin::is_running_auto_purge() : false;
489
490 return array(
491 'name' => 'clean_orphan_meta',
492 'title' => esc_html__( 'Clean Orphaned Meta', 'stream' ),
493 'type' => $is_running ? 'none' : 'link',
494 'href' => add_query_arg(
495 array(
496 'action' => 'wp_stream_clean_orphan_meta',
497 'wp_stream_nonce_clean_orphan_meta' => wp_create_nonce( 'stream_nonce_clean_orphan_meta' ),
498 ),
499 admin_url( 'admin-ajax.php' )
500 ),
501 'desc' => $is_running
502 ? esc_html__( 'Auto-purge is currently running. The orphan reaper will execute as part of that cycle; the manual cleanup link is hidden to avoid duplicating the work.', 'stream' )
503 : esc_html__( 'Schedules an immediate background cleanup of stream_meta rows whose parent record is missing. Safe to run while Stream is in use; runs once via Action Scheduler.', 'stream' ),
504 'default' => 0,
505 'sticky' => 'bottom',
506 );
507 }
508
509 /**
510 * Returns a single setting value, reading the network-level option when
511 * Stream is network-activated on multisite.
512 *
513 * Settings::get_options() only loads from get_site_option() inside
514 * is_network_admin() screens. In REST and frontend contexts on a
515 * network-activated install, $this->options reflects the (typically empty)
516 * per-site option, which would silently mask a network-admin-controlled
517 * setting. This accessor handles that case so callers don't have to
518 * duplicate the multisite branching.
519 *
520 * @param string $key Fully-qualified setting key (e.g. "advanced_enable_abilities_api").
521 * @param mixed $default_value Value returned when the setting is not present.
522 *
523 * @return mixed
524 */
525 public function get_setting_value( $key, $default_value = null ) {
526 if (
527 is_multisite()
528 && isset( $this->plugin )
529 && $this->plugin->is_network_activated()
530 ) {
531 $options = (array) get_site_option( $this->network_options_key, array() );
532 } else {
533 $options = (array) $this->options;
534 }
535
536 return isset( $options[ $key ] ) ? $options[ $key ] : $default_value;
537 }
538
539 /**
540 * Returns the full options array, reading the network-level option when
541 * Stream is network-activated on multisite. Mirrors get_setting_value()
542 * but returns the entire array.
543 *
544 * @return array
545 */
546 public function get_all_setting_values() {
547 if (
548 is_multisite()
549 && isset( $this->plugin )
550 && $this->plugin->is_network_activated()
551 ) {
552 return (array) get_site_option( $this->network_options_key, array() );
553 }
554
555 return (array) $this->options;
556 }
557
558 /**
559 * Persists the options array, writing to the network-level option when
560 * Stream is network-activated on multisite. Used by REST/ability writers
561 * which run outside is_network_admin() but must respect the authoritative
562 * store. Refreshes $this->options afterwards so in-request reads see the
563 * new values.
564 *
565 * @param array $options Full options array to persist (caller is responsible
566 * for merging over existing values when desired).
567 *
568 * @return bool True on a successful write, false on no-op or failure.
569 */
570 public function update_all_setting_values( array $options ) {
571 $is_network = (
572 is_multisite()
573 && isset( $this->plugin )
574 && $this->plugin->is_network_activated()
575 );
576
577 if ( $is_network ) {
578 $result = update_site_option( $this->network_options_key, $options );
579 } else {
580 $result = update_option( $this->option_key, $options );
581 }
582
583 // Refresh the in-memory copy so subsequent reads in the same request
584 // see the updated values. On network-activated installs we re-read
585 // from the network option directly because Settings::get_options()
586 // gates on is_network_admin() and would return the (now-stale)
587 // per-site option in REST contexts. Merge defaults on top so callers
588 // reading $plugin->settings->options keep seeing a fully-populated
589 // array (matches get_options()'s historical contract).
590 if ( $is_network ) {
591 $defaults = $this->get_defaults( $this->option_key );
592 $this->options = wp_parse_args(
593 (array) get_site_option( $this->network_options_key, array() ),
594 $defaults
595 );
596 } else {
597 $this->options = $this->get_options();
598 }
599
600 return (bool) $result;
601 }
602
603 /**
604 * Returns a list of options based on the current screen.
605 *
606 * @return array
607 */
608 public function get_options() {
609 $option_key = $this->option_key;
610 $defaults = $this->get_defaults( $option_key );
611
612 /**
613 * Filter allows for modification of options
614 *
615 * @param array
616 *
617 * @return array Updated array of options
618 */
619 return apply_filters(
620 'wp_stream_settings_options',
621 wp_parse_args(
622 is_network_admin() ? (array) get_site_option( $option_key, array() ) : (array) get_option( $option_key, array() ),
623 $defaults
624 ),
625 $option_key
626 );
627 }
628
629 /**
630 * Iterate through registered fields and extract default values
631 *
632 * @return array
633 */
634 public function get_defaults() {
635 $fields = $this->get_fields();
636 $defaults = array();
637
638 foreach ( $fields as $section_name => $section ) {
639 foreach ( $section['fields'] as $field ) {
640 $defaults[ $section_name . '_' . $field['name'] ] = isset( $field['default'] ) ? $field['default'] : null;
641 }
642 }
643
644 return (array) $defaults;
645 }
646
647 /**
648 * Retrieves the deletion warning message based on the site type
649 * and whether or not there is currently a process running to delete the tables.
650 *
651 * @param bool|null $is_running_deletion Optional pre-computed deletion state.
652 * Pass to avoid a duplicate Action Scheduler
653 * query when the caller has already checked.
654 * Defaults to checking only in admin context.
655 * Untyped parameter to remain compatible with
656 * phpcs.xml.dist testVersion=7.0- (nullable
657 * type declarations require PHP 7.1+).
658 * @return string The deletion warning message.
659 */
660 public function get_deletion_warning( $is_running_deletion = null ): string {
661
662 if ( null === $is_running_deletion ) {
663 $is_running_deletion = is_admin() ? Admin::is_running_async_deletion() : false;
664 }
665
666 if ( $is_running_deletion ) {
667
668 $warning = __( 'Currently deleting records. Please be patient, this can take a while.', 'stream' );
669
670 } elseif ( $this->plugin->is_multisite_network_activated() ) {
671
672 $warning = __( 'Warning: This will delete all activity records from the database for all sites.', 'stream' );
673
674 } elseif ( $this->plugin->is_multisite_not_network_activated() ) {
675
676 $warning = __( 'Warning: This will delete all activity records from the database for this site.', 'stream' );
677
678 } else {
679
680 $warning = __( 'Warning: This will delete all activity records from the database.', 'stream' );
681 }
682
683 return $warning;
684 }
685
686 /**
687 * Registers settings fields and sections
688 *
689 * @return void
690 */
691 public function register_settings() {
692 $sections = $this->get_fields();
693
694 register_setting(
695 $this->option_key,
696 $this->option_key,
697 array(
698 $this,
699 'sanitize_settings',
700 )
701 );
702
703 foreach ( $sections as $section_name => $section ) {
704 add_settings_section(
705 $section_name,
706 null,
707 '__return_false',
708 $this->option_key
709 );
710
711 foreach ( $section['fields'] as $field_idx => $field ) {
712 // No field type associated, skip, no GUI.
713 if ( ! isset( $field['type'] ) ) {
714 continue;
715 }
716
717 add_settings_field(
718 $field['name'],
719 $field['title'],
720 ( isset( $field['callback'] ) ? $field['callback'] : array(
721 $this,
722 'output_field',
723 ) ),
724 $this->option_key,
725 $section_name,
726 $field + array(
727 'section' => $section_name,
728 'label_for' => sprintf( '%s_%s_%s', $this->option_key, $section_name, $field['name'] ),
729 )
730 );
731 }
732 }
733 }
734
735 /**
736 * Sanitization callback for settings field values before save
737 *
738 * @param array $input Raw input.
739 *
740 * @return array
741 */
742 public function sanitize_settings( $input ) {
743 $output = array();
744 $sections = $this->get_fields();
745
746 foreach ( $sections as $section => $data ) {
747 if ( empty( $data['fields'] ) || ! is_array( $data['fields'] ) ) {
748 continue;
749 }
750
751 foreach ( $data['fields'] as $field ) {
752 $type = ! empty( $field['type'] ) ? $field['type'] : null;
753 $name = ! empty( $field['name'] ) ? sprintf( '%s_%s', $section, $field['name'] ) : null;
754
755 if ( empty( $type ) || ! isset( $input[ $name ] ) || '' === $input[ $name ] ) {
756 continue;
757 }
758
759 $output[ $name ] = $this->sanitize_setting_by_field_type( $input[ $name ], $type );
760 }
761 }
762
763 return $output;
764 }
765
766 /**
767 * Sanitizes a setting value based on the field type.
768 *
769 * @param mixed $value The value to be sanitized.
770 * @param string $field_type The type of field.
771 *
772 * @return mixed The sanitized value.
773 */
774 public function sanitize_setting_by_field_type( $value, $field_type ) {
775
776 // Sanitize depending on the type of field.
777 switch ( $field_type ) {
778 case 'number':
779 $sanitized_value = is_numeric( $value ) ? intval( trim( $value ) ) : '';
780 break;
781 case 'checkbox':
782 $sanitized_value = is_numeric( $value ) ? absint( trim( $value ) ) : '';
783 break;
784 default:
785 if ( is_array( $value ) ) {
786 $sanitized_value = $value;
787
788 // Support all values in multidimentional arrays too.
789 array_walk_recursive(
790 $sanitized_value,
791 function ( &$v ) {
792 $v = sanitize_text_field( trim( $v ) );
793 }
794 );
795 } else {
796 $sanitized_value = sanitize_text_field( trim( $value ) );
797 }
798 }
799
800 return $sanitized_value;
801 }
802
803 /**
804 * Compile HTML needed for displaying the field
805 *
806 * @param array $field Field settings.
807 *
808 * @return string HTML to be displayed
809 */
810 public function render_field( $field ) {
811 $output = null;
812 $type = isset( $field['type'] ) ? $field['type'] : null;
813 $section = isset( $field['section'] ) ? $field['section'] : null;
814 $name = isset( $field['name'] ) ? $field['name'] : null;
815 $class = isset( $field['class'] ) ? $field['class'] : null;
816 $placeholder = isset( $field['placeholder'] ) ? $field['placeholder'] : null;
817 $description = isset( $field['desc'] ) ? $field['desc'] : null;
818 $href = isset( $field['href'] ) ? $field['href'] : null;
819 $rows = isset( $field['rows'] ) ? $field['rows'] : 10;
820 $cols = isset( $field['cols'] ) ? $field['cols'] : 50;
821 $after_field = isset( $field['after_field'] ) ? $field['after_field'] : null;
822 $default = isset( $field['default'] ) ? $field['default'] : null;
823 $min = isset( $field['min'] ) ? $field['min'] : 0;
824 $max = isset( $field['max'] ) ? $field['max'] : 999;
825 $step = isset( $field['step'] ) ? $field['step'] : 1;
826 $title = isset( $field['title'] ) ? $field['title'] : null;
827 $nonce = isset( $field['nonce'] ) ? $field['nonce'] : null;
828
829 if ( isset( $field['value'] ) ) {
830 $current_value = $field['value'];
831 } elseif ( isset( $this->options[ $section . '_' . $name ] ) ) {
832 $current_value = $this->options[ $section . '_' . $name ];
833 } else {
834 $current_value = null;
835 }
836
837 $option_key = $this->option_key;
838
839 if ( is_callable( $current_value ) ) {
840 $current_value = call_user_func( $current_value );
841 }
842
843 if ( ! $type || ! $section || ! $name ) {
844 return '';
845 }
846
847 if ( 'multi_checkbox' === $type && ( empty( $field['choices'] ) || ! is_array( $field['choices'] ) ) ) {
848 return '';
849 }
850
851 switch ( $type ) {
852 case 'text':
853 case 'number':
854 $output = sprintf(
855 '<input type="%1$s" name="%2$s[%3$s_%4$s]" id="%2$s_%3$s_%4$s" class="%5$s" placeholder="%6$s" min="%7$d" max="%8$d" step="%9$d" value="%10$s" /> %11$s',
856 esc_attr( $type ),
857 esc_attr( $option_key ),
858 esc_attr( $section ),
859 esc_attr( $name ),
860 esc_attr( $class ),
861 esc_attr( $placeholder ),
862 esc_attr( $min ),
863 esc_attr( $max ),
864 esc_attr( $step ),
865 esc_attr( $current_value ),
866 wp_kses_post( $after_field )
867 );
868 break;
869 case 'textarea':
870 $output = sprintf(
871 '<textarea name="%1$s[%2$s_%3$s]" id="%1$s_%2$s_%3$s" class="%4$s" placeholder="%5$s" rows="%6$d" cols="%7$d">%8$s</textarea> %9$s',
872 esc_attr( $option_key ),
873 esc_attr( $section ),
874 esc_attr( $name ),
875 esc_attr( $class ),
876 esc_attr( $placeholder ),
877 absint( $rows ),
878 absint( $cols ),
879 esc_textarea( $current_value ),
880 wp_kses_post( $after_field )
881 );
882 break;
883 case 'checkbox':
884 if ( isset( $current_value ) ) {
885 $value = $current_value;
886 } elseif ( isset( $default ) ) {
887 $value = $default;
888 } else {
889 $value = 0;
890 }
891
892 $output = sprintf(
893 '<label><input type="checkbox" name="%1$s[%2$s_%3$s]" id="%1$s[%2$s_%3$s]" value="1" %4$s /> %5$s</label>',
894 esc_attr( $option_key ),
895 esc_attr( $section ),
896 esc_attr( $name ),
897 checked( $value, 1, false ),
898 wp_kses_post( $after_field )
899 );
900 break;
901 case 'multi_checkbox':
902 $output = sprintf(
903 '<div id="%1$s[%2$s_%3$s]"><fieldset>',
904 esc_attr( $option_key ),
905 esc_attr( $section ),
906 esc_attr( $name )
907 );
908 // Fallback if nothing is selected.
909 $output .= sprintf(
910 '<input type="hidden" name="%1$s[%2$s_%3$s][]" value="__placeholder__" />',
911 esc_attr( $option_key ),
912 esc_attr( $section ),
913 esc_attr( $name )
914 );
915 $current_value = (array) $current_value;
916 $choices = $field['choices'];
917 if ( is_callable( $choices ) ) {
918 $choices = call_user_func( $choices );
919 }
920 foreach ( $choices as $value => $label ) {
921 $output .= sprintf(
922 '<label>%1$s <span>%2$s</span></label><br />',
923 sprintf(
924 '<input type="checkbox" name="%1$s[%2$s_%3$s][]" value="%4$s" %5$s />',
925 esc_attr( $option_key ),
926 esc_attr( $section ),
927 esc_attr( $name ),
928 esc_attr( $value ),
929 checked( in_array( $value, $current_value, true ), true, false )
930 ),
931 esc_html( $label )
932 );
933 }
934 $output .= '</fieldset></div>';
935 break;
936 case 'select':
937 $current_value = $this->options[ $section . '_' . $name ];
938 $default_value = isset( $default['value'] ) ? $default['value'] : '-1';
939 $default_name = isset( $default['name'] ) ? $default['name'] : 'Choose Setting';
940
941 $output = sprintf(
942 '<select name="%1$s[%2$s_%3$s]" class="%1$s_%2$s_%3$s">',
943 esc_attr( $option_key ),
944 esc_attr( $section ),
945 esc_attr( $name )
946 );
947 $output .= sprintf(
948 '<option value="%1$s" %2$s>%3$s</option>',
949 esc_attr( $default_value ),
950 checked( $default_value === $current_value, true, false ),
951 esc_html( $default_name )
952 );
953 foreach ( $field['choices'] as $value => $label ) {
954 $output .= sprintf(
955 '<option value="%1$s" %2$s>%3$s</option>',
956 esc_attr( $value ),
957 checked( $value === $current_value, true, false ),
958 esc_html( $label )
959 );
960 }
961 $output .= '</select>';
962 break;
963 case 'file':
964 $output = sprintf(
965 '<input type="file" name="%1$s[%2$s_%3$s]" class="%4$s">',
966 esc_attr( $option_key ),
967 esc_attr( $section ),
968 esc_attr( $name ),
969 esc_attr( $class )
970 );
971 break;
972 case 'link':
973 $output = sprintf(
974 '<a id="%1$s_%2$s_%3$s" class="%4$s" href="%5$s">%6$s</a>',
975 esc_attr( $option_key ),
976 esc_attr( $section ),
977 esc_attr( $name ),
978 esc_attr( $class ),
979 esc_attr( $href ),
980 esc_attr( $title )
981 );
982 break;
983 case 'none':
984 // Intentional no-op: callers set 'none' to hide a control's value
985 // column while still letting the row label + description render
986 // (e.g. Reset Stream Database while a deletion is running, or
987 // Clean Orphaned Meta while the auto-purge chain is active).
988 // The description string carries the running-state message.
989 $output = '';
990 break;
991 case 'select2':
992 if ( ! isset( $current_value ) ) {
993 $current_value = '';
994 }
995
996 $data_values = array();
997
998 if ( isset( $field['choices'] ) ) {
999 $choices = $field['choices'];
1000 if ( is_callable( $choices ) ) {
1001 $param = ( isset( $field['param'] ) ) ? $field['param'] : null;
1002 $choices = call_user_func( $choices, $param );
1003 }
1004 foreach ( $choices as $key => $value ) {
1005 if ( is_array( $value ) ) {
1006 $child_values = array();
1007 if ( isset( $value['children'] ) ) {
1008 $child_values = array();
1009 foreach ( $value['children'] as $child_key => $child_value ) {
1010 $child_values[] = array(
1011 'id' => $child_key,
1012 'text' => $child_value,
1013 );
1014 }
1015 }
1016 if ( isset( $value['label'] ) ) {
1017 $data_values[] = array(
1018 'id' => $key,
1019 'text' => $value['label'],
1020 'children' => $child_values,
1021 );
1022 }
1023 } else {
1024 $data_values[] = array(
1025 'id' => $key,
1026 'text' => $value,
1027 );
1028 }
1029 }
1030 $class .= ' with-source';
1031 }
1032
1033 $input_html = sprintf(
1034 '<input type="hidden" name="%1$s[%2$s_%3$s]" data-values=\'%4$s\' value="%5$s" class="select2-select %6$s" data-placeholder="%7$s" />',
1035 esc_attr( $option_key ),
1036 esc_attr( $section ),
1037 esc_attr( $name ),
1038 esc_attr( wp_json_encode( $data_values ) ),
1039 esc_attr( $current_value ),
1040 esc_attr( $class ),
1041 /* translators: %s: the title of the dropdown menu (e.g. "users") */
1042 sprintf( esc_html__( 'Any %s', 'stream' ), $title )
1043 );
1044
1045 $output = sprintf(
1046 '<div class="%1$s_%2$s_%3$s">%4$s</div>',
1047 esc_attr( $option_key ),
1048 esc_attr( $section ),
1049 esc_attr( $name ),
1050 $input_html
1051 );
1052
1053 break;
1054 case 'rule_list':
1055 $users = count_users();
1056 $form = new Form_Generator();
1057 $output = '<p class="description">' . esc_html( $description ) . '</p>';
1058
1059 $actions_top = sprintf( '<input type="button" class="button" id="%1$s_new_rule" value="&#43; %2$s" />', esc_attr( $section . '_' . $name ), esc_html__( 'Add New Rule', 'stream' ) );
1060 $actions_bottom = sprintf( '<input type="button" class="button" id="%1$s_remove_rules" value="%2$s" />', esc_attr( $section . '_' . $name ), esc_html__( 'Delete Selected Rules', 'stream' ) );
1061
1062 $output .= sprintf( '<div class="tablenav top">%1$s</div>', $actions_top );
1063 $output .= '<table class="wp-list-table widefat fixed stream-exclude-list">';
1064
1065 unset( $description );
1066
1067 $heading_row = sprintf(
1068 '<tr>
1069 <td scope="col" class="manage-column column-cb check-column">%1$s</td>
1070 <th scope="col" class="manage-column">%2$s</th>
1071 <th scope="col" class="manage-column">%3$s</th>
1072 <th scope="col" class="manage-column">%4$s</th>
1073 <th scope="col" class="manage-column">%5$s</th>
1074 <th scope="col" class="actions-column manage-column"><span class="hidden">%6$s</span></th>
1075 </tr>',
1076 '<input class="cb-select" type="checkbox" />',
1077 esc_html__( 'Author or Role', 'stream' ),
1078 esc_html__( 'Context', 'stream' ),
1079 esc_html__( 'Action', 'stream' ),
1080 esc_html__( 'IP Address', 'stream' ),
1081 esc_html__( 'Filters', 'stream' )
1082 );
1083
1084 $exclude_rows = array();
1085
1086 // Account for when no rules have been added yet.
1087 if ( ! is_array( $current_value ) ) {
1088 $current_value = array();
1089 }
1090
1091 // Prepend an empty row.
1092 $current_value['exclude_row'] = ( isset( $current_value['exclude_row'] ) ? $current_value['exclude_row'] : array() ) + array( 'helper' => '' );
1093
1094 foreach ( $current_value['exclude_row'] as $key => $value ) {
1095 // Prepare values.
1096 $author_or_role = isset( $current_value['author_or_role'][ $key ] ) ? $current_value['author_or_role'][ $key ] : '';
1097 $connector = isset( $current_value['connector'][ $key ] ) ? $current_value['connector'][ $key ] : '';
1098 $context = isset( $current_value['context'][ $key ] ) ? $current_value['context'][ $key ] : '';
1099 $action = isset( $current_value['action'][ $key ] ) ? $current_value['action'][ $key ] : '';
1100 $ip_address = isset( $current_value['ip_address'][ $key ] ) ? $current_value['ip_address'][ $key ] : '';
1101
1102 // Author or Role dropdown menu.
1103 $author_or_role_values = array();
1104 $author_or_role_selected = array();
1105
1106 foreach ( $this->get_roles() as $role_id => $role ) {
1107 $args = array(
1108 'value' => $role_id,
1109 'text' => $role,
1110 );
1111 $count = isset( $users['avail_roles'][ $role_id ] ) ? $users['avail_roles'][ $role_id ] : 0;
1112
1113 if ( ! empty( $count ) ) {
1114 /* translators: %d: a number of users (e.g. "42") */
1115 $args['user_count'] = sprintf( _n( '%d user', '%d users', absint( $count ), 'stream' ), absint( $count ) );
1116 }
1117
1118 if ( $role_id === $author_or_role ) {
1119 $author_or_role_selected['value'] = $role_id;
1120 $author_or_role_selected['text'] = $role;
1121 }
1122
1123 $author_or_role_values[] = $args;
1124 }
1125
1126 if ( empty( $author_or_role_selected ) && is_numeric( $author_or_role ) ) {
1127 $user = new WP_User( $author_or_role );
1128 $display_name = ( 0 === $user->ID ) ? esc_html__( 'N/A', 'stream' ) : $user->display_name;
1129 $author_or_role_selected = array(
1130 'value' => $user->ID,
1131 'text' => $display_name,
1132 );
1133 $author_or_role_values[] = $author_or_role_selected;
1134 }
1135
1136 $author_or_role_input = $form->render_field(
1137 'select2',
1138 array(
1139 'name' => esc_attr( sprintf( '%1$s[%2$s_%3$s][%4$s][]', $option_key, $section, $name, 'author_or_role' ) ),
1140 'options' => $author_or_role_values,
1141 'classes' => 'author_or_role',
1142 // Data attributes are escaped in Form_Generator::prepare_data_attributes_string().
1143 'data' => array(
1144 'placeholder' => __( 'Any Author or Role', 'stream' ),
1145 'nonce' => wp_create_nonce( 'stream_get_users' ),
1146 'selected-id' => isset( $author_or_role_selected['value'] ) ? $author_or_role_selected['value'] : '',
1147 'selected-text' => isset( $author_or_role_selected['text'] ) ? $author_or_role_selected['text'] : '',
1148 ),
1149 ),
1150 false
1151 );
1152
1153 // Context dropdown menu.
1154 $context_values = array();
1155
1156 foreach ( $this->get_terms_labels( 'context' ) as $context_id => $context_data ) {
1157 if ( is_array( $context_data ) ) {
1158 $child_values = array();
1159 if ( isset( $context_data['children'] ) ) {
1160 $child_values = array();
1161 foreach ( $context_data['children'] as $child_id => $child_value ) {
1162 $child_values[] = array(
1163 'value' => $context_id . '-' . $child_id,
1164 'text' => $child_value,
1165 'parent' => $context_id,
1166 );
1167 }
1168 }
1169 if ( isset( $context_data['label'] ) ) {
1170 $context_values[] = array(
1171 'value' => $context_id,
1172 'text' => $context_data['label'],
1173 'children' => $child_values,
1174 );
1175 }
1176 } else {
1177 $context_values[] = array(
1178 'value' => $context_id,
1179 'text' => $context_data,
1180 );
1181 }
1182 }
1183
1184 $connector_or_context_input = $form->render_field(
1185 'select2',
1186 array(
1187 'name' => esc_attr( sprintf( '%1$s[%2$s_%3$s][%4$s][]', $option_key, $section, $name, 'connector_or_context' ) ),
1188 'options' => $context_values,
1189 'classes' => 'connector_or_context',
1190 // Data attributes are escaped in Form_Generator::prepare_data_attributes_string().
1191 'data' => array(
1192 'group' => 'connector',
1193 'placeholder' => __( 'Any Context', 'stream' ),
1194 ),
1195 ),
1196 false
1197 );
1198
1199 $connector_input = $form->render_field(
1200 'hidden',
1201 array(
1202 'name' => esc_attr( sprintf( '%1$s[%2$s_%3$s][%4$s][]', $option_key, $section, $name, 'connector' ) ),
1203 'value' => $connector,
1204 'classes' => 'connector',
1205 ),
1206 false
1207 );
1208
1209 $context_input = $form->render_field(
1210 'hidden',
1211 array(
1212 'name' => esc_attr( sprintf( '%1$s[%2$s_%3$s][%4$s][]', $option_key, $section, $name, 'context' ) ),
1213 'value' => $context,
1214 'classes' => 'context',
1215 ),
1216 false
1217 );
1218
1219 // Action dropdown menu.
1220 $action_values = array();
1221
1222 foreach ( $this->get_terms_labels( 'action' ) as $action_id => $action_data ) {
1223 $action_values[] = array(
1224 'value' => $action_id,
1225 'text' => $action_data,
1226 );
1227 }
1228
1229 $action_input = $form->render_field(
1230 'select2',
1231 array(
1232 'name' => esc_attr( sprintf( '%1$s[%2$s_%3$s][%4$s][]', $option_key, $section, $name, 'action' ) ),
1233 'value' => $action,
1234 'options' => $action_values,
1235 'classes' => 'action',
1236 // Data attributes are escaped in Form_Generator::prepare_data_attributes_string().
1237 'data' => array(
1238 'placeholder' => __( 'Any Action', 'stream' ),
1239 ),
1240 ),
1241 false
1242 );
1243
1244 // IP Address input.
1245 $ip_address_input = $form->render_field(
1246 'select2',
1247 array(
1248 'name' => esc_attr( sprintf( '%1$s[%2$s_%3$s][%4$s][]', $option_key, $section, $name, 'ip_address' ) ),
1249 'value' => $ip_address,
1250 'classes' => 'ip_address',
1251 // Data attributes are escaped in Form_Generator::prepare_data_attributes_string().
1252 'data' => array(
1253 'placeholder' => __( 'Any IP Address', 'stream' ),
1254 'nonce' => wp_create_nonce( 'stream_get_ips' ),
1255 ),
1256 'multiple' => true,
1257 ),
1258 false
1259 );
1260
1261 // Hidden helper input.
1262 $helper_input = sprintf(
1263 '<input type="hidden" name="%1$s[%2$s_%3$s][%4$s][]" value="" />',
1264 esc_attr( $option_key ),
1265 esc_attr( $section ),
1266 esc_attr( $name ),
1267 'exclude_row'
1268 );
1269
1270 $exclude_rows[] = sprintf(
1271 '<tr class="%1$s %2$s">
1272 <th scope="row" class="check-column">%3$s %4$s</th>
1273 <td>%5$s</td>
1274 <td>%6$s %7$s %8$s</td>
1275 <td>%9$s</td>
1276 <td>%10$s</td>
1277 <th scope="row" class="actions-column">
1278 <a href="#" class="exclude_rules_remove_rule_row">%11$s</a>
1279 </th>
1280 </tr>',
1281 ( 0 !== (int) $key % 2 ) ? 'alternate' : '',
1282 ( 'helper' === (string) $key ) ? 'hidden helper' : '',
1283 '<input class="cb-select" type="checkbox" />',
1284 $helper_input,
1285 $author_or_role_input,
1286 $connector_or_context_input,
1287 $connector_input,
1288 $context_input,
1289 $action_input,
1290 $ip_address_input,
1291 esc_html__( 'Delete', 'stream' )
1292 );
1293 }
1294
1295 $no_rules_found_row = sprintf(
1296 '<tr class="no-items hidden"><td class="colspanchange" colspan="5">%1$s</td></tr>',
1297 esc_html__( 'No rules found.', 'stream' )
1298 );
1299
1300 $output .= '<thead>' . $heading_row . '</thead>';
1301 $output .= '<tfoot>' . $heading_row . '</tfoot>';
1302 $output .= '<tbody>' . $no_rules_found_row . implode( '', $exclude_rows ) . '</tbody>';
1303
1304 $output .= '</table>';
1305
1306 $output .= sprintf( '<div class="tablenav bottom">%1$s</div>', $actions_bottom );
1307
1308 break;
1309 }
1310 $output .= ! empty( $description ) ? wp_kses_post( sprintf( '<p class="description">%s</p>', $description ) ) : null;
1311
1312 return $output;
1313 }
1314
1315 /**
1316 * Render Callback for post_types field
1317 *
1318 * @param array $field Field to be rendered.
1319 *
1320 * @return string
1321 */
1322 public function output_field( $field ) {
1323 $method = 'output_' . $field['name'];
1324
1325 if ( method_exists( $this, $method ) ) {
1326 return call_user_func( array( $this, $method ), $field );
1327 }
1328
1329 $output = $this->render_field( $field );
1330
1331 echo $output; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1332 }
1333
1334 /**
1335 * Get an array of user roles
1336 *
1337 * @return array
1338 */
1339 public function get_roles() {
1340 $wp_roles = new WP_Roles();
1341 $roles = array();
1342
1343 foreach ( $wp_roles->get_names() as $role => $label ) {
1344 $roles[ $role ] = translate_user_role( $label );
1345 }
1346
1347 return $roles;
1348 }
1349
1350 /**
1351 * Function will return all terms labels of given column
1352 *
1353 * @param string $column Name of the column.
1354 *
1355 * @return array
1356 */
1357 public function get_terms_labels( $column ) {
1358 $return_labels = array();
1359
1360 if ( isset( $this->plugin->connectors->term_labels[ 'stream_' . $column ] ) ) {
1361 if ( 'context' === $column && isset( $this->plugin->connectors->term_labels['stream_connector'] ) ) {
1362 $connectors = $this->plugin->connectors->term_labels['stream_connector'];
1363 $contexts = $this->plugin->connectors->term_labels['stream_context'];
1364
1365 foreach ( $connectors as $connector => $connector_label ) {
1366 $return_labels[ $connector ]['label'] = $connector_label;
1367 foreach ( $contexts as $context => $context_label ) {
1368 if ( isset( $this->plugin->connectors->contexts[ $connector ] ) && array_key_exists( $context, $this->plugin->connectors->contexts[ $connector ] ) ) {
1369 $return_labels[ $connector ]['children'][ $context ] = $context_label;
1370 }
1371 }
1372 }
1373 } else {
1374 $return_labels = $this->plugin->connectors->term_labels[ 'stream_' . $column ];
1375 }
1376
1377 ksort( $return_labels );
1378 }
1379
1380 return $return_labels;
1381 }
1382
1383 /**
1384 * Remove records when records TTL is shortened
1385 *
1386 * @action update_option_wp_stream
1387 *
1388 * @param array $old_value Old value.
1389 * @param array $new_value New value.
1390 */
1391 public function updated_option_ttl_remove_records( $old_value, $new_value ) {
1392 $ttl_before = isset( $old_value['general_records_ttl'] ) ? (int) $old_value['general_records_ttl'] : - 1;
1393 $ttl_after = isset( $new_value['general_records_ttl'] ) ? (int) $new_value['general_records_ttl'] : - 1;
1394
1395 if ( $ttl_after < $ttl_before ) {
1396 /**
1397 * Fires when the records TTL is shortened.
1398 *
1399 * Preserved for backward compatibility with third-party code that
1400 * hooked this action in Stream <= 4.1.x. The auto-purge itself
1401 * no longer listens to this hook (it was migrated to Action
1402 * Scheduler), so trigger the purge directly below.
1403 */
1404 do_action( 'wp_stream_auto_purge' );
1405
1406 // Trigger an immediate auto-purge cycle so the shortened TTL
1407 // takes effect now instead of at the next 12h recurring tick.
1408 //
1409 // Enqueue the recurring action as a one-shot async action so the
1410 // work serializes through the scheduler. Calling
1411 // purge_scheduled_action() inline here would bypass the overlap
1412 // guard's view of "in-flight" work (the current request is not a
1413 // scheduled action) and could stack a parallel chain when a
1414 // real chain is already running. Falls back to inline if no
1415 // scheduler is available (defensive — Plugin::__construct() sets it).
1416 if ( ! empty( $this->plugin->scheduler ) ) {
1417 if ( ! \WP_Stream\Admin::is_running_auto_purge() ) {
1418 $this->plugin->scheduler->enqueue_async(
1419 \WP_Stream\Admin::AUTO_PURGE_ACTION,
1420 array(),
1421 \WP_Stream\Admin::AUTO_PURGE_GROUP
1422 );
1423 }
1424 } elseif ( isset( $this->plugin->admin ) ) {
1425 $this->plugin->admin->purge_scheduled_action();
1426 }
1427 }
1428 }
1429
1430 /**
1431 * Get translations of serialized Stream settings
1432 *
1433 * @filter wp_stream_serialized_labels
1434 *
1435 * @param array $labels Setting labels.
1436 *
1437 * @return array Multidimensional array of fields
1438 */
1439 public function get_settings_translations( $labels ) {
1440 if ( ! isset( $labels[ $this->option_key ] ) ) {
1441 $labels[ $this->option_key ] = array();
1442 }
1443
1444 foreach ( $this->get_fields() as $section_slug => $section ) {
1445 foreach ( $section['fields'] as $field ) {
1446 $labels[ $this->option_key ][ sprintf( '%s_%s', $section_slug, $field['name'] ) ] = $field['title'];
1447 }
1448 }
1449
1450 return $labels;
1451 }
1452 }
1453