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-plugin.php

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

680 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Initializes plugin
4 *
5 * @package WP_Stream;
6 */
7
8 namespace WP_Stream;
9
10 use RuntimeException;
11
12 /**
13 * Class Plugin
14 */
15 class Plugin {
16 /**
17 * Plugin version number.
18 *
19 * TODO Maybe pass this as a constructor dependency?
20 *
21 * @const string
22 */
23 const VERSION = '4.3.0';
24
25 /**
26 * WP-CLI command
27 *
28 * @const string
29 */
30 const WP_CLI_COMMAND = 'stream';
31
32
33 /**
34 * Used to check if it's a single site, not multisite.
35 *
36 * @const string
37 */
38 const SINGLE_SITE = 'single';
39
40 /**
41 * Used to check if it's a multisite with the plugin network enabled.
42 *
43 * @const string
44 */
45 const MULTI_NETWORK = 'multisite-network';
46
47 /**
48 * Used to check if it's a multisite with the plugin not network enabled.
49 *
50 * @const string
51 */
52 const MULTI_NOT_NETWORK = 'multisite-not-network';
53
54 /**
55 * Holds and manages WordPress Admin configurations.
56 *
57 * @var Admin
58 */
59 public $admin;
60
61 /**
62 * Holds and manages alerts.
63 *
64 * @var Alerts
65 */
66 public $alerts;
67
68 /**
69 * Holds and manages alerts lists.
70 *
71 * @var Alerts_List
72 */
73 public $alerts_list;
74
75 /**
76 * Holds and manages WordPress Abilities API integration.
77 *
78 * @var Abilities
79 */
80 public $abilities;
81
82 /**
83 * Holds and manages connectors
84 *
85 * @var Connectors
86 */
87 public $connectors;
88
89 /**
90 * Holds and manages DB connections.
91 *
92 * @var DB
93 */
94 public $db;
95
96 /**
97 * Holds and manages records.
98 *
99 * @var Log
100 */
101 public $log;
102
103 /**
104 * Stores and manages WordPress settings.
105 *
106 * @var Settings
107 */
108 public $settings;
109
110 /**
111 * Process DB migrations.
112 *
113 * @var Install
114 */
115 public $install;
116
117 /**
118 * Backend used to schedule Stream's deferred work (purge / reset).
119 *
120 * Either an {@see AS_Scheduler} (Action Scheduler, default) or a
121 * {@see Cron_Scheduler} (WP-Cron fallback), selected at construction via
122 * the `wp_stream_use_action_scheduler` filter.
123 *
124 * @var Scheduler
125 */
126 public $scheduler;
127
128 /**
129 * Whether the bundled Action Scheduler library was loaded.
130 *
131 * Set from a file_exists() check at construction (see __construct), so it
132 * is reliable on `plugins_loaded` even though AS only declares its as_*()
133 * API later on `init`.
134 *
135 * @var bool
136 */
137 protected $action_scheduler_available = false;
138
139 /**
140 * URLs and Paths used by the plugin
141 *
142 * @var array
143 */
144 public $locations = array();
145
146 /**
147 * IP address for the current request to be associated with the log entry.
148 *
149 * @var null|false|string Valid IP address, null if not set, false if invalid.
150 */
151 protected $client_ip_address;
152
153 /**
154 * Class constructor
155 */
156 public function __construct() {
157 $locate = $this->locate_plugin();
158
159 $this->locations = array(
160 'plugin' => $locate['plugin_basename'],
161 'dir' => $locate['dir_path'],
162 'url' => $locate['dir_url'],
163 'inc_dir' => $locate['dir_path'] . 'includes/',
164 'class_dir' => $locate['dir_path'] . 'classes/',
165 );
166
167 spl_autoload_register( array( $this, 'autoload' ) );
168
169 // Determine the scheduler backend, then load Action Scheduler only if
170 // it is the selected backend. AS remains a hard, bundled dependency
171 // for the WordPress.org build (see issue #1907), but integrators who
172 // opt into the WP-Cron fallback via `wp_stream_use_action_scheduler`
173 // pay neither the require_once nor AS's own `init` bootstrap.
174 //
175 // Availability is tracked from a file_exists() check rather than
176 // function_exists(): AS only declares its as_*() API on `init`, but
177 // this constructor runs at plugin-file inclusion time (before the
178 // `plugins_loaded` action fires), so the functions are not defined
179 // yet. Scheduler methods are only ever called on/after `init`
180 // (wp_loaded, AJAX, cron), by which point the API is loaded.
181 $action_scheduler = $this->locations['dir'] . '/vendor/woocommerce/action-scheduler/action-scheduler.php';
182 $this->action_scheduler_available = file_exists( $action_scheduler );
183
184 // Select the scheduler backend for Stream's deferred work, loading the
185 // bundled AS library only if it is the chosen backend.
186 $this->scheduler = $this->create_scheduler();
187
188 // Load helper functions.
189 require_once $this->locations['inc_dir'] . 'functions.php';
190
191 // Load DB helper interface/class.
192 $driver_class = apply_filters( 'wp_stream_db_driver', '\WP_Stream\DB_Driver_WPDB' );
193 $driver = null;
194
195 if ( class_exists( $driver_class ) ) {
196 $driver = new $driver_class();
197 $this->db = new DB( $driver );
198 }
199
200 $error = false;
201 if ( ! $this->db ) {
202 $error = esc_html__( 'Stream: Could not load chosen DB driver.', 'stream' );
203 } elseif ( ! $driver instanceof DB_Driver ) {
204 $error = esc_html__( 'Stream: DB driver must implement DB Driver interface.', 'stream' );
205 }
206
207 if ( $error ) {
208 wp_die(
209 esc_html( $error ),
210 esc_html__( 'Stream DB Error', 'stream' )
211 );
212 }
213
214 // Load languages.
215 add_action( 'plugins_loaded', array( $this, 'i18n' ) );
216
217 // Load logger class.
218 $this->log = apply_filters( 'wp_stream_log_handler', new Log( $this ) );
219
220 // Set the IP address for the current request.
221 $this->client_ip_address = wp_stream_filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP );
222
223 // Load settings and connectors after widgets_init and before the default init priority.
224 add_action( 'init', array( $this, 'init' ), 9 );
225
226 // Add frontend indicator.
227 add_action( 'wp_head', array( $this, 'frontend_indicator' ) );
228
229 // Change DB driver after plugin loaded if any add-ons want to replace.
230 add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ), 20 );
231
232 // Load admin area classes.
233 if ( is_admin() || ( defined( 'WP_STREAM_DEV_DEBUG' ) && WP_STREAM_DEV_DEBUG ) || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
234 $this->admin = new Admin( $this );
235 $this->install = $driver->setup_storage( $this );
236 } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) {
237 $this->admin = new Admin( $this, $driver );
238 }
239
240 // Load WP-CLI command.
241 if ( defined( 'WP_CLI' ) && WP_CLI ) {
242 \WP_CLI::add_command( self::WP_CLI_COMMAND, 'WP_Stream\CLI' );
243 }
244 }
245
246 /**
247 * Build the scheduler backend for Stream's deferred work (purge / reset).
248 *
249 * Defaults to Action Scheduler when its bundled library is present.
250 * Integrators that bundle Stream and run reliable cron (e.g. Cavalcade)
251 * can force the WP-Cron fallback by returning false from
252 * `wp_stream_use_action_scheduler`. When the fallback is chosen, the
253 * bundled AS library is not loaded at all (no require_once, no AS `init`
254 * bootstrap). See issue #1907.
255 *
256 * @return Scheduler
257 */
258 public function create_scheduler() {
259 /**
260 * Filter whether Stream uses Action Scheduler for its deferred work.
261 *
262 * IMPORTANT — timing: this filter is applied in Plugin::__construct(),
263 * which runs when the Stream plugin file is included, i.e. BEFORE the
264 * `plugins_loaded` action. Callbacks must therefore be registered from
265 * code that loads before Stream: an mu-plugin, wp-config.php, or a
266 * plugin guaranteed to load earlier. Registering it from a regular
267 * plugin's `plugins_loaded` hook is too late and will be ignored.
268 *
269 * @param bool $use_action_scheduler Whether to use Action Scheduler.
270 * Defaults to true when the bundled
271 * AS library is present. Return a
272 * real boolean: the value is cast
273 * with (bool), so PHP string
274 * truthiness applies to strings
275 * (e.g. 'false' is truthy).
276 */
277 $use_action_scheduler = (bool) apply_filters(
278 'wp_stream_use_action_scheduler',
279 $this->action_scheduler_available
280 );
281
282 if ( ! $use_action_scheduler ) {
283 return new Cron_Scheduler();
284 }
285
286 // Guard a forced `__return_true` override when the bundled AS library
287 // is absent: returning AS_Scheduler without loading AS would fatal on
288 // the first unguarded as_*() call. Fall back to the cron scheduler
289 // instead. The default path never hits this — the filter defaults to
290 // $this->action_scheduler_available.
291 if ( ! $this->action_scheduler_available ) {
292 return new Cron_Scheduler();
293 }
294
295 // Load the bundled AS library before instantiating its scheduler.
296 // AS's own ActionScheduler_Versions arbitration handles a host that
297 // already provides its own copy.
298 require_once $this->locations['dir'] . '/vendor/woocommerce/action-scheduler/action-scheduler.php';
299
300 return new AS_Scheduler();
301 }
302
303 /**
304 * Autoloader for classes
305 *
306 * @param string $class_name Fully qualified classname to be loaded.
307 */
308 public function autoload( $class_name ) {
309 if ( ! preg_match( '/^(?P<namespace>.+)\\\\(?P<autoload>[^\\\\]+)$/', $class_name, $matches ) ) {
310 return;
311 }
312
313 static $reflection;
314
315 if ( empty( $reflection ) ) {
316 $reflection = new \ReflectionObject( $this );
317 }
318
319 if ( $reflection->getNamespaceName() !== $matches['namespace'] ) {
320 return;
321 }
322
323 $autoload_name = $matches['autoload'];
324 $autoload_dir = \trailingslashit( $this->locations['class_dir'] );
325 $autoload_path = sprintf( '%sclass-%s.php', $autoload_dir, strtolower( str_replace( '_', '-', $autoload_name ) ) );
326
327 if ( is_readable( $autoload_path ) ) {
328 require_once $autoload_path;
329 }
330 }
331
332 /**
333 * Loads the translation files.
334 *
335 * @action plugins_loaded
336 */
337 public function i18n() {
338 load_plugin_textdomain( 'stream', false, dirname( $this->locations['plugin'] ) . '/languages/' );
339 }
340
341 /**
342 * Load Settings, Notifications, and Connectors
343 *
344 * @action init
345 */
346 public function init() {
347 $this->settings = new Settings( $this );
348 $this->connectors = new Connectors( $this );
349 $this->alerts = new Alerts( $this );
350 $this->alerts_list = new Alerts_List( $this );
351 $this->abilities = new Abilities( $this );
352 }
353
354 /**
355 * Displays an HTML comment in the frontend head to indicate that Stream is activated,
356 * and which version of Stream is currently in use.
357 *
358 * @action wp_head
359 *
360 * @return string|void An HTML comment, or nothing if the value is filtered out.
361 */
362 public function frontend_indicator() {
363 /* translators: Localization not needed */
364 $comment = sprintf( 'Stream WordPress user activity plugin v%s', esc_html( $this->get_version() ) );
365
366 /**
367 * Filter allows the HTML output of the frontend indicator comment
368 * to be altered or removed, if desired.
369 *
370 * @return string The content of the HTML comment
371 */
372 $comment = apply_filters( 'wp_stream_frontend_indicator', $comment );
373
374 if ( ! empty( $comment ) ) {
375 printf( "<!-- %s -->\n", esc_html( $comment ) );
376 }
377 }
378
379 /**
380 * Version of plugin_dir_url() which works for plugins installed in the plugins directory,
381 * and for plugins bundled with themes.
382 *
383 * @return array
384 */
385 private function locate_plugin() {
386 $dir_url = trailingslashit( plugins_url( '', __DIR__ ) );
387 $dir_path = plugin_dir_path( __DIR__ );
388 $dir_basename = basename( $dir_path );
389 $plugin_basename = trailingslashit( $dir_basename ) . 'stream.php';
390
391 return compact( 'dir_url', 'dir_path', 'dir_basename', 'plugin_basename' );
392 }
393
394 /**
395 * Getter for the version number.
396 *
397 * @return string
398 */
399 public function get_version() {
400 return self::VERSION;
401 }
402
403 /**
404 * Change plugin database driver in case driver plugin loaded after stream
405 */
406 public function plugins_loaded() {
407 // Load DB helper interface/class.
408 $driver_class = apply_filters( 'wp_stream_db_driver', '\WP_Stream\DB_Driver_WPDB' );
409
410 if ( class_exists( $driver_class ) ) {
411 $driver = new $driver_class();
412 $this->db = new DB( $driver );
413 }
414 }
415
416 /**
417 * Returns true if Stream is network activated, otherwise false
418 *
419 * @return bool
420 */
421 public function is_network_activated() {
422
423 $is_network_activated = false;
424
425 if ( $this->is_mustuse() ) {
426 $is_network_activated = true;
427 } else {
428 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
429 require_once ABSPATH . '/wp-admin/includes/plugin.php';
430 }
431 $is_network_activated = is_plugin_active_for_network( $this->locations['plugin'] );
432 }
433
434 /**
435 * Filter allows the network activated detection to be overridden.
436 *
437 * @param string $is_network_activated Whether the plugin is network activated.
438 * @param WP_Stream\Plugin $plugin The stream plugin object.
439 */
440 return apply_filters( 'wp_stream_is_network_activated', $is_network_activated, $this );
441 }
442
443 /**
444 * Returns true if Stream is a must-use plugin, otherwise false
445 *
446 * @return bool
447 */
448 public function is_mustuse() {
449 $stream_php = trailingslashit( WPMU_PLUGIN_DIR ) . $this->locations['plugin'];
450
451 if ( file_exists( $stream_php ) && class_exists( 'WP_Stream\Plugin' ) ) {
452 return true;
453 }
454
455 return false;
456 }
457
458 /**
459 * Get the IP address for the current request.
460 *
461 * @return false|null|string Valid IP address, null if not set, false if invalid.
462 */
463 public function get_client_ip_address() {
464 return apply_filters( 'wp_stream_client_ip_address', $this->client_ip_address );
465 }
466
467 /**
468 * Get the site type.
469 *
470 * This function determines the type of site based on whether it is a single site or a multisite.
471 * If it is a multisite, it also checks if it is network activated or not.
472 *
473 * @return string The site type
474 */
475 public function get_site_type(): string {
476
477 // If it's a multisite, is it network activated or not?
478 if ( is_multisite() ) {
479 return $this->is_network_activated() ? self::MULTI_NETWORK : self::MULTI_NOT_NETWORK;
480 }
481
482 return self::SINGLE_SITE;
483 }
484
485 /**
486 * Should the number of records which need to be processed be considered "large"?
487 *
488 * @param int $record_number The number of rows in the {$wpdb->prefix}_stream table to be processed.
489 * @return bool Whether or not this should be considered large.
490 */
491 public function is_large_records_table( int $record_number ): bool {
492 /**
493 * Filters whether or not the number of records should be considered a large table.
494 *
495 * @since 4.1.0
496 *
497 * @param bool $is_large_table Whether or not the number of records should be considered large.
498 * @param int $record_number The number of records being checked.
499 */
500 return apply_filters( 'wp_stream_is_large_records_table', $record_number > 1000000, $record_number );
501 }
502
503 /**
504 * Checks if the plugin is running on a single site installation.
505 *
506 * @return bool True if the plugin is running on a single site installation, false otherwise.
507 */
508 public function is_single_site() {
509 return self::SINGLE_SITE === $this->get_site_type();
510 }
511
512 /**
513 * Check if the plugin is activated on a multisite installation but not network activated.
514 *
515 * @return bool True if the plugin is activated on a multisite installation but not network activated, false otherwise.
516 */
517 public function is_multisite_not_network_activated() {
518 return self::MULTI_NOT_NETWORK === $this->get_site_type();
519 }
520
521 /**
522 * Check if the plugin is activated on a multisite network.
523 *
524 * @return bool True if the plugin is network activated on a multisite, false otherwise.
525 */
526 public function is_multisite_network_activated() {
527 return self::MULTI_NETWORK === $this->get_site_type();
528 }
529
530 /**
531 * Enqueue a script along with a stylesheet if it exists.
532 *
533 * @param string $handle Script handle.
534 * @param array $additional_dependencies Additional dependencies.
535 * @param array $data Data to pass to the script.
536 *
537 * @throws RuntimeException If built JavaScript assets are not found.
538 * @return void
539 */
540 public function enqueue_asset( $handle, $additional_dependencies = array(), $data = array() ) {
541 // If is enqueued already, bail out.
542 if ( wp_script_is( $handle ) ) {
543 return;
544 }
545
546 $path = untrailingslashit( $this->locations['dir'] );
547 $url = untrailingslashit( $this->locations['url'] );
548
549 $script_asset_path = "$path/build/$handle.asset.php";
550
551 if ( ! file_exists( $script_asset_path ) ) {
552 throw new RuntimeException( 'Built JavaScript assets not found. Please run `npm run build`' );
553 }
554
555 $script_asset = require $script_asset_path; // phpcs:disable WPThemeReview.CoreFunctionality.FileInclude.FileIncludeFound
556
557 wp_enqueue_script(
558 "wp-stream-$handle",
559 "$url/build/$handle.js",
560 array_merge(
561 $script_asset['dependencies'],
562 (array) $additional_dependencies
563 ),
564 $script_asset['version'],
565 true
566 );
567
568 if ( file_exists( "$path/build/$handle.css" ) ) {
569 wp_enqueue_style(
570 "wp-stream-$handle",
571 "$url/build/$handle.css",
572 array(),
573 $script_asset['version']
574 );
575 }
576
577 if ( ! empty( $data ) ) {
578 wp_add_inline_script(
579 "wp-stream-$handle",
580 sprintf( 'window["%s"] = %s;', esc_attr( "wp-stream-$handle" ), wp_json_encode( $data ) ),
581 'before'
582 );
583 }
584 }
585
586 /**
587 * Enqueue select2 script and locale file if exists.
588 *
589 * @return string Script handle.
590 */
591 public function with_select2() {
592 $handle = 'wp-stream-select2';
593
594 // If is enqueued already, bail out.
595 if ( wp_script_is( $handle ) ) {
596 return $handle;
597 }
598
599 $path = untrailingslashit( $this->locations['dir'] );
600 $url = untrailingslashit( $this->locations['url'] );
601
602 wp_enqueue_script(
603 $handle,
604 "$url/build/select2/js/select2.full.min.js",
605 array( 'jquery' ),
606 filemtime( "$path/build/select2/js/select2.full.min.js" ),
607 true
608 );
609 wp_enqueue_style(
610 $handle,
611 "$url/build/select2/css/select2.min.css",
612 array(),
613 filemtime( "$path/build/select2/css/select2.min.css" )
614 );
615
616 $locale = get_locale();
617 $lang = substr( $locale, 0, 2 );
618 $search_files = array( $locale, $lang, 'en' );
619
620 foreach ( $search_files as $search_file ) {
621 if ( file_exists( "$path/build/select2/js/i18n/$search_file.js" ) ) {
622 wp_enqueue_script(
623 sanitize_title( "$handle-$search_file" ),
624 "$url/build/select2/js/i18n/$search_file.js",
625 array( $handle ),
626 filemtime( "$path/build/select2/js/i18n/$search_file.js" ),
627 true
628 );
629 break;
630 }
631 }
632
633 return $handle;
634 }
635
636 /**
637 * Enqueue jquery.timeago script and locale file if exists.
638 *
639 * @return string Script handle.
640 */
641 public function with_jquery_timeago() {
642 $handle = 'wp-stream-jquery-timeago';
643
644 // If is enqueued already, bail out.
645 if ( wp_script_is( $handle ) ) {
646 return $handle;
647 }
648
649 $path = untrailingslashit( $this->locations['dir'] );
650 $url = untrailingslashit( $this->locations['url'] );
651
652 wp_enqueue_script(
653 $handle,
654 "$url/build/timeago/js/jquery.timeago.js",
655 array( 'jquery' ),
656 filemtime( "$path/build/timeago/js/jquery.timeago.js" ),
657 true
658 );
659
660 $locale = get_locale();
661 $lang = substr( $locale, 0, 2 );
662 $search_files = array( $locale, $lang, 'en' );
663
664 foreach ( $search_files as $search_file ) {
665 if ( file_exists( "$path/build/timeago/js/locales/jquery.timeago.$search_file.js" ) ) {
666 wp_enqueue_script(
667 sanitize_title( "$handle-$search_file" ),
668 "$url/build/timeago/js/locales/jquery.timeago.$search_file.js",
669 array( $handle ),
670 filemtime( "$path/build/timeago/js/locales/jquery.timeago.$search_file.js" ),
671 true
672 );
673 break;
674 }
675 }
676
677 return $handle;
678 }
679 }
680