locate_plugin(); $this->locations = array( 'plugin' => $locate['plugin_basename'], 'dir' => $locate['dir_path'], 'url' => $locate['dir_url'], 'inc_dir' => $locate['dir_path'] . 'includes/', 'class_dir' => $locate['dir_path'] . 'classes/', ); spl_autoload_register( array( $this, 'autoload' ) ); // Determine the scheduler backend, then load Action Scheduler only if // it is the selected backend. AS remains a hard, bundled dependency // for the WordPress.org build (see issue #1907), but integrators who // opt into the WP-Cron fallback via `wp_stream_use_action_scheduler` // pay neither the require_once nor AS's own `init` bootstrap. // // Availability is tracked from a file_exists() check rather than // function_exists(): AS only declares its as_*() API on `init`, but // this constructor runs at plugin-file inclusion time (before the // `plugins_loaded` action fires), so the functions are not defined // yet. Scheduler methods are only ever called on/after `init` // (wp_loaded, AJAX, cron), by which point the API is loaded. $action_scheduler = $this->locations['dir'] . '/vendor/woocommerce/action-scheduler/action-scheduler.php'; $this->action_scheduler_available = file_exists( $action_scheduler ); // Select the scheduler backend for Stream's deferred work, loading the // bundled AS library only if it is the chosen backend. $this->scheduler = $this->create_scheduler(); // Load helper functions. require_once $this->locations['inc_dir'] . 'functions.php'; // Load DB helper interface/class. $driver_class = apply_filters( 'wp_stream_db_driver', '\WP_Stream\DB_Driver_WPDB' ); $driver = null; if ( class_exists( $driver_class ) ) { $driver = new $driver_class(); $this->db = new DB( $driver ); } $error = false; if ( ! $this->db ) { $error = esc_html__( 'Stream: Could not load chosen DB driver.', 'stream' ); } elseif ( ! $driver instanceof DB_Driver ) { $error = esc_html__( 'Stream: DB driver must implement DB Driver interface.', 'stream' ); } if ( $error ) { wp_die( esc_html( $error ), esc_html__( 'Stream DB Error', 'stream' ) ); } // Load languages. add_action( 'plugins_loaded', array( $this, 'i18n' ) ); // Load logger class. $this->log = apply_filters( 'wp_stream_log_handler', new Log( $this ) ); // Set the IP address for the current request. $this->client_ip_address = wp_stream_filter_input( INPUT_SERVER, 'REMOTE_ADDR', FILTER_VALIDATE_IP ); // Load settings and connectors after widgets_init and before the default init priority. add_action( 'init', array( $this, 'init' ), 9 ); // Add frontend indicator. add_action( 'wp_head', array( $this, 'frontend_indicator' ) ); // Change DB driver after plugin loaded if any add-ons want to replace. add_action( 'plugins_loaded', array( $this, 'plugins_loaded' ), 20 ); // Load admin area classes. if ( is_admin() || ( defined( 'WP_STREAM_DEV_DEBUG' ) && WP_STREAM_DEV_DEBUG ) || ( defined( 'WP_CLI' ) && WP_CLI ) ) { $this->admin = new Admin( $this ); $this->install = $driver->setup_storage( $this ); } elseif ( defined( 'DOING_CRON' ) && DOING_CRON ) { $this->admin = new Admin( $this, $driver ); } // Load WP-CLI command. if ( defined( 'WP_CLI' ) && WP_CLI ) { \WP_CLI::add_command( self::WP_CLI_COMMAND, 'WP_Stream\CLI' ); } } /** * Build the scheduler backend for Stream's deferred work (purge / reset). * * Defaults to Action Scheduler when its bundled library is present. * Integrators that bundle Stream and run reliable cron (e.g. Cavalcade) * can force the WP-Cron fallback by returning false from * `wp_stream_use_action_scheduler`. When the fallback is chosen, the * bundled AS library is not loaded at all (no require_once, no AS `init` * bootstrap). See issue #1907. * * @return Scheduler */ public function create_scheduler() { /** * Filter whether Stream uses Action Scheduler for its deferred work. * * IMPORTANT — timing: this filter is applied in Plugin::__construct(), * which runs when the Stream plugin file is included, i.e. BEFORE the * `plugins_loaded` action. Callbacks must therefore be registered from * code that loads before Stream: an mu-plugin, wp-config.php, or a * plugin guaranteed to load earlier. Registering it from a regular * plugin's `plugins_loaded` hook is too late and will be ignored. * * @param bool $use_action_scheduler Whether to use Action Scheduler. * Defaults to true when the bundled * AS library is present. Return a * real boolean: the value is cast * with (bool), so PHP string * truthiness applies to strings * (e.g. 'false' is truthy). */ $use_action_scheduler = (bool) apply_filters( 'wp_stream_use_action_scheduler', $this->action_scheduler_available ); if ( ! $use_action_scheduler ) { return new Cron_Scheduler(); } // Guard a forced `__return_true` override when the bundled AS library // is absent: returning AS_Scheduler without loading AS would fatal on // the first unguarded as_*() call. Fall back to the cron scheduler // instead. The default path never hits this — the filter defaults to // $this->action_scheduler_available. if ( ! $this->action_scheduler_available ) { return new Cron_Scheduler(); } // Load the bundled AS library before instantiating its scheduler. // AS's own ActionScheduler_Versions arbitration handles a host that // already provides its own copy. require_once $this->locations['dir'] . '/vendor/woocommerce/action-scheduler/action-scheduler.php'; return new AS_Scheduler(); } /** * Autoloader for classes * * @param string $class_name Fully qualified classname to be loaded. */ public function autoload( $class_name ) { if ( ! preg_match( '/^(?P.+)\\\\(?P[^\\\\]+)$/', $class_name, $matches ) ) { return; } static $reflection; if ( empty( $reflection ) ) { $reflection = new \ReflectionObject( $this ); } if ( $reflection->getNamespaceName() !== $matches['namespace'] ) { return; } $autoload_name = $matches['autoload']; $autoload_dir = \trailingslashit( $this->locations['class_dir'] ); $autoload_path = sprintf( '%sclass-%s.php', $autoload_dir, strtolower( str_replace( '_', '-', $autoload_name ) ) ); if ( is_readable( $autoload_path ) ) { require_once $autoload_path; } } /** * Loads the translation files. * * @action plugins_loaded */ public function i18n() { load_plugin_textdomain( 'stream', false, dirname( $this->locations['plugin'] ) . '/languages/' ); } /** * Load Settings, Notifications, and Connectors * * @action init */ public function init() { $this->settings = new Settings( $this ); $this->connectors = new Connectors( $this ); $this->alerts = new Alerts( $this ); $this->alerts_list = new Alerts_List( $this ); $this->abilities = new Abilities( $this ); } /** * Displays an HTML comment in the frontend head to indicate that Stream is activated, * and which version of Stream is currently in use. * * @action wp_head * * @return string|void An HTML comment, or nothing if the value is filtered out. */ public function frontend_indicator() { /* translators: Localization not needed */ $comment = sprintf( 'Stream WordPress user activity plugin v%s', esc_html( $this->get_version() ) ); /** * Filter allows the HTML output of the frontend indicator comment * to be altered or removed, if desired. * * @return string The content of the HTML comment */ $comment = apply_filters( 'wp_stream_frontend_indicator', $comment ); if ( ! empty( $comment ) ) { printf( "\n", esc_html( $comment ) ); } } /** * Version of plugin_dir_url() which works for plugins installed in the plugins directory, * and for plugins bundled with themes. * * @return array */ private function locate_plugin() { $dir_url = trailingslashit( plugins_url( '', __DIR__ ) ); $dir_path = plugin_dir_path( __DIR__ ); $dir_basename = basename( $dir_path ); $plugin_basename = trailingslashit( $dir_basename ) . 'stream.php'; return compact( 'dir_url', 'dir_path', 'dir_basename', 'plugin_basename' ); } /** * Getter for the version number. * * @return string */ public function get_version() { return self::VERSION; } /** * Change plugin database driver in case driver plugin loaded after stream */ public function plugins_loaded() { // Load DB helper interface/class. $driver_class = apply_filters( 'wp_stream_db_driver', '\WP_Stream\DB_Driver_WPDB' ); if ( class_exists( $driver_class ) ) { $driver = new $driver_class(); $this->db = new DB( $driver ); } } /** * Returns true if Stream is network activated, otherwise false * * @return bool */ public function is_network_activated() { $is_network_activated = false; if ( $this->is_mustuse() ) { $is_network_activated = true; } else { if ( ! function_exists( 'is_plugin_active_for_network' ) ) { require_once ABSPATH . '/wp-admin/includes/plugin.php'; } $is_network_activated = is_plugin_active_for_network( $this->locations['plugin'] ); } /** * Filter allows the network activated detection to be overridden. * * @param string $is_network_activated Whether the plugin is network activated. * @param WP_Stream\Plugin $plugin The stream plugin object. */ return apply_filters( 'wp_stream_is_network_activated', $is_network_activated, $this ); } /** * Returns true if Stream is a must-use plugin, otherwise false * * @return bool */ public function is_mustuse() { $stream_php = trailingslashit( WPMU_PLUGIN_DIR ) . $this->locations['plugin']; if ( file_exists( $stream_php ) && class_exists( 'WP_Stream\Plugin' ) ) { return true; } return false; } /** * Get the IP address for the current request. * * @return false|null|string Valid IP address, null if not set, false if invalid. */ public function get_client_ip_address() { return apply_filters( 'wp_stream_client_ip_address', $this->client_ip_address ); } /** * Get the site type. * * This function determines the type of site based on whether it is a single site or a multisite. * If it is a multisite, it also checks if it is network activated or not. * * @return string The site type */ public function get_site_type(): string { // If it's a multisite, is it network activated or not? if ( is_multisite() ) { return $this->is_network_activated() ? self::MULTI_NETWORK : self::MULTI_NOT_NETWORK; } return self::SINGLE_SITE; } /** * Should the number of records which need to be processed be considered "large"? * * @param int $record_number The number of rows in the {$wpdb->prefix}_stream table to be processed. * @return bool Whether or not this should be considered large. */ public function is_large_records_table( int $record_number ): bool { /** * Filters whether or not the number of records should be considered a large table. * * @since 4.1.0 * * @param bool $is_large_table Whether or not the number of records should be considered large. * @param int $record_number The number of records being checked. */ return apply_filters( 'wp_stream_is_large_records_table', $record_number > 1000000, $record_number ); } /** * Checks if the plugin is running on a single site installation. * * @return bool True if the plugin is running on a single site installation, false otherwise. */ public function is_single_site() { return self::SINGLE_SITE === $this->get_site_type(); } /** * Check if the plugin is activated on a multisite installation but not network activated. * * @return bool True if the plugin is activated on a multisite installation but not network activated, false otherwise. */ public function is_multisite_not_network_activated() { return self::MULTI_NOT_NETWORK === $this->get_site_type(); } /** * Check if the plugin is activated on a multisite network. * * @return bool True if the plugin is network activated on a multisite, false otherwise. */ public function is_multisite_network_activated() { return self::MULTI_NETWORK === $this->get_site_type(); } /** * Enqueue a script along with a stylesheet if it exists. * * @param string $handle Script handle. * @param array $additional_dependencies Additional dependencies. * @param array $data Data to pass to the script. * * @throws RuntimeException If built JavaScript assets are not found. * @return void */ public function enqueue_asset( $handle, $additional_dependencies = array(), $data = array() ) { // If is enqueued already, bail out. if ( wp_script_is( $handle ) ) { return; } $path = untrailingslashit( $this->locations['dir'] ); $url = untrailingslashit( $this->locations['url'] ); $script_asset_path = "$path/build/$handle.asset.php"; if ( ! file_exists( $script_asset_path ) ) { throw new RuntimeException( 'Built JavaScript assets not found. Please run `npm run build`' ); } $script_asset = require $script_asset_path; // phpcs:disable WPThemeReview.CoreFunctionality.FileInclude.FileIncludeFound wp_enqueue_script( "wp-stream-$handle", "$url/build/$handle.js", array_merge( $script_asset['dependencies'], (array) $additional_dependencies ), $script_asset['version'], true ); if ( file_exists( "$path/build/$handle.css" ) ) { wp_enqueue_style( "wp-stream-$handle", "$url/build/$handle.css", array(), $script_asset['version'] ); } if ( ! empty( $data ) ) { wp_add_inline_script( "wp-stream-$handle", sprintf( 'window["%s"] = %s;', esc_attr( "wp-stream-$handle" ), wp_json_encode( $data ) ), 'before' ); } } /** * Enqueue select2 script and locale file if exists. * * @return string Script handle. */ public function with_select2() { $handle = 'wp-stream-select2'; // If is enqueued already, bail out. if ( wp_script_is( $handle ) ) { return $handle; } $path = untrailingslashit( $this->locations['dir'] ); $url = untrailingslashit( $this->locations['url'] ); wp_enqueue_script( $handle, "$url/build/select2/js/select2.full.min.js", array( 'jquery' ), filemtime( "$path/build/select2/js/select2.full.min.js" ), true ); wp_enqueue_style( $handle, "$url/build/select2/css/select2.min.css", array(), filemtime( "$path/build/select2/css/select2.min.css" ) ); $locale = get_locale(); $lang = substr( $locale, 0, 2 ); $search_files = array( $locale, $lang, 'en' ); foreach ( $search_files as $search_file ) { if ( file_exists( "$path/build/select2/js/i18n/$search_file.js" ) ) { wp_enqueue_script( sanitize_title( "$handle-$search_file" ), "$url/build/select2/js/i18n/$search_file.js", array( $handle ), filemtime( "$path/build/select2/js/i18n/$search_file.js" ), true ); break; } } return $handle; } /** * Enqueue jquery.timeago script and locale file if exists. * * @return string Script handle. */ public function with_jquery_timeago() { $handle = 'wp-stream-jquery-timeago'; // If is enqueued already, bail out. if ( wp_script_is( $handle ) ) { return $handle; } $path = untrailingslashit( $this->locations['dir'] ); $url = untrailingslashit( $this->locations['url'] ); wp_enqueue_script( $handle, "$url/build/timeago/js/jquery.timeago.js", array( 'jquery' ), filemtime( "$path/build/timeago/js/jquery.timeago.js" ), true ); $locale = get_locale(); $lang = substr( $locale, 0, 2 ); $search_files = array( $locale, $lang, 'en' ); foreach ( $search_files as $search_file ) { if ( file_exists( "$path/build/timeago/js/locales/jquery.timeago.$search_file.js" ) ) { wp_enqueue_script( sanitize_title( "$handle-$search_file" ), "$url/build/timeago/js/locales/jquery.timeago.$search_file.js", array( $handle ), filemtime( "$path/build/timeago/js/locales/jquery.timeago.$search_file.js" ), true ); break; } } return $handle; } }