/** * General API functions for scheduling actions * * @package ActionScheduler. */ /** * Enqueue an action to run one time, as soon as possible * * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. * * @return int The action ID. */ function as_enqueue_async_action( $hook, $args = array(), $group = '', $unique = false ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } /** * Provides an opportunity to short-circuit the default process for enqueuing async * actions. * * Returning a value other than null from the filter will short-circuit the normal * process. The expectation in such a scenario is that callbacks will return an integer * representing the enqueued action ID (enqueued using some alternative process) or else * zero. * * @param int|null $pre_option The value to return instead of the option value. * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. */ $pre = apply_filters( 'pre_as_enqueue_async_action', null, $hook, $args, $group ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } return ActionScheduler::factory()->async_unique( $hook, $args, $group, $unique ); } /** * Schedule an action to run one time * * @param int $timestamp When the job will run. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. * * @return int The action ID. */ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = '', $unique = false ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } /** * Provides an opportunity to short-circuit the default process for enqueuing single * actions. * * Returning a value other than null from the filter will short-circuit the normal * process. The expectation in such a scenario is that callbacks will return an integer * representing the scheduled action ID (scheduled using some alternative process) or else * zero. * * @param int|null $pre_option The value to return instead of the option value. * @param int $timestamp When the action will run. * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. */ $pre = apply_filters( 'pre_as_schedule_single_action', null, $timestamp, $hook, $args, $group ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } return ActionScheduler::factory()->single_unique( $hook, $args, $timestamp, $group, $unique ); } /** * Schedule a recurring action * * @param int $timestamp When the first instance of the job will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. * * @return int The action ID. */ function as_schedule_recurring_action( $timestamp, $interval_in_seconds, $hook, $args = array(), $group = '', $unique = false ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } /** * Provides an opportunity to short-circuit the default process for enqueuing recurring * actions. * * Returning a value other than null from the filter will short-circuit the normal * process. The expectation in such a scenario is that callbacks will return an integer * representing the scheduled action ID (scheduled using some alternative process) or else * zero. * * @param int|null $pre_option The value to return instead of the option value. * @param int $timestamp When the action will run. * @param int $interval_in_seconds How long to wait between runs. * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. */ $pre = apply_filters( 'pre_as_schedule_recurring_action', null, $timestamp, $interval_in_seconds, $hook, $args, $group ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } return ActionScheduler::factory()->recurring_unique( $hook, $args, $timestamp, $interval_in_seconds, $group, $unique ); } /** * Schedule an action that recurs on a cron-like schedule. * * @param int $timestamp The first instance of the action will be scheduled * to run at a time calculated after this timestamp matching the cron * expression. This can be used to delay the first instance of the action. * @param string $schedule A cron-link schedule string. * @see http://en.wikipedia.org/wiki/Cron * * * * * * * * ┬ ┬ ┬ ┬ ┬ ┬ * | | | | | | * | | | | | + year [optional] * | | | | +----- day of week (0 - 7) (Sunday=0 or 7) * | | | +---------- month (1 - 12) * | | +--------------- day of month (1 - 31) * | +-------------------- hour (0 - 23) * +------------------------- min (0 - 59) * @param string $hook The hook to trigger. * @param array $args Arguments to pass when the hook triggers. * @param string $group The group to assign this job to. * @param bool $unique Whether the action should be unique. * * @return int The action ID. */ function as_schedule_cron_action( $timestamp, $schedule, $hook, $args = array(), $group = '', $unique = false ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } /** * Provides an opportunity to short-circuit the default process for enqueuing cron * actions. * * Returning a value other than null from the filter will short-circuit the normal * process. The expectation in such a scenario is that callbacks will return an integer * representing the scheduled action ID (scheduled using some alternative process) or else * zero. * * @param int|null $pre_option The value to return instead of the option value. * @param int $timestamp When the action will run. * @param string $schedule Cron-like schedule string. * @param string $hook Action hook. * @param array $args Action arguments. * @param string $group Action group. */ $pre = apply_filters( 'pre_as_schedule_cron_action', null, $timestamp, $schedule, $hook, $args, $group ); if ( null !== $pre ) { return is_int( $pre ) ? $pre : 0; } return ActionScheduler::factory()->cron_unique( $hook, $args, $timestamp, $schedule, $group, $unique ); } /** * Cancel the next occurrence of a scheduled action. * * While only the next instance of a recurring or cron action is unscheduled by this method, that will also prevent * all future instances of that recurring or cron action from being run. Recurring and cron actions are scheduled in * a sequence instead of all being scheduled at once. Each successive occurrence of a recurring action is scheduled * only after the former action is run. If the next instance is never run, because it's unscheduled by this function, * then the following instance will never be scheduled (or exist), which is effectively the same as being unscheduled * by this method also. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. * @param string $group The group the job is assigned to. * * @return int|null The scheduled action ID if a scheduled action was found, or null if no matching action found. */ function as_unschedule_action( $hook, $args = array(), $group = '' ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return 0; } $params = array( 'hook' => $hook, 'status' => ActionScheduler_Store::STATUS_PENDING, 'orderby' => 'date', 'order' => 'ASC', 'group' => $group, ); if ( is_array( $args ) ) { $params['args'] = $args; } $action_id = ActionScheduler::store()->query_action( $params ); if ( $action_id ) { try { ActionScheduler::store()->cancel_action( $action_id ); } catch ( Exception $exception ) { ActionScheduler::logger()->log( $action_id, sprintf( /* translators: %s is the name of the hook to be cancelled. */ __( 'Caught exception while cancelling action: %s', 'woocommerce' ), esc_attr( $hook ) ) ); $action_id = null; } } return $action_id; } /** * Cancel all occurrences of a scheduled action. * * @param string $hook The hook that the job will trigger. * @param array $args Args that would have been passed to the job. * @param string $group The group the job is assigned to. */ function as_unschedule_all_actions( $hook, $args = array(), $group = '' ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return; } if ( empty( $args ) ) { if ( ! empty( $hook ) && empty( $group ) ) { ActionScheduler_Store::instance()->cancel_actions_by_hook( $hook ); return; } if ( ! empty( $group ) && empty( $hook ) ) { ActionScheduler_Store::instance()->cancel_actions_by_group( $group ); return; } } do { $unscheduled_action = as_unschedule_action( $hook, $args, $group ); } while ( ! empty( $unscheduled_action ) ); } /** * Check if there is an existing action in the queue with a given hook, args and group combination. * * An action in the queue could be pending, in-progress or async. If the is pending for a time in * future, its scheduled date will be returned as a timestamp. If it is currently being run, or an * async action sitting in the queue waiting to be processed, in which case boolean true will be * returned. Or there may be no async, in-progress or pending action for this hook, in which case, * boolean false will be the return value. * * @param string $hook Name of the hook to search for. * @param array $args Arguments of the action to be searched. * @param string $group Group of the action to be searched. * * @return int|bool The timestamp for the next occurrence of a pending scheduled action, true for an async or in-progress action or false if there is no matching action. */ function as_next_scheduled_action( $hook, $args = null, $group = '' ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return false; } $params = array( 'hook' => $hook, 'orderby' => 'date', 'order' => 'ASC', 'group' => $group, ); if ( is_array( $args ) ) { $params['args'] = $args; } $params['status'] = ActionScheduler_Store::STATUS_RUNNING; $action_id = ActionScheduler::store()->query_action( $params ); if ( $action_id ) { return true; } $params['status'] = ActionScheduler_Store::STATUS_PENDING; $action_id = ActionScheduler::store()->query_action( $params ); if ( null === $action_id ) { return false; } $action = ActionScheduler::store()->fetch_action( $action_id ); $scheduled_date = $action->get_schedule()->get_date(); if ( $scheduled_date ) { return (int) $scheduled_date->format( 'U' ); } elseif ( null === $scheduled_date ) { // pending async action with NullSchedule. return true; } return false; } /** * Check if there is a scheduled action in the queue but more efficiently than as_next_scheduled_action(). * * It's recommended to use this function when you need to know whether a specific action is currently scheduled * (pending or in-progress). * * @since 3.3.0 * * @param string $hook The hook of the action. * @param array $args Args that have been passed to the action. Null will matches any args. * @param string $group The group the job is assigned to. * * @return bool True if a matching action is pending or in-progress, false otherwise. */ function as_has_scheduled_action( $hook, $args = null, $group = '' ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return false; } $query_args = array( 'hook' => $hook, 'status' => array( ActionScheduler_Store::STATUS_RUNNING, ActionScheduler_Store::STATUS_PENDING ), 'group' => $group, 'orderby' => 'none', ); if ( null !== $args ) { $query_args['args'] = $args; } $action_id = ActionScheduler::store()->query_action( $query_args ); return null !== $action_id; } /** * Find scheduled actions * * @param array $args Possible arguments, with their default values. * 'hook' => '' - the name of the action that will be triggered. * 'args' => NULL - the args array that will be passed with the action. * 'date' => NULL - the scheduled date of the action. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'date_compare' => '<=' - operator for testing "date". accepted values are '!=', '>', '>=', '<', '<=', '='. * 'modified' => NULL - the date the action was last updated. Expects a DateTime object, a unix timestamp, or a string that can parsed with strtotime(). Used in UTC timezone. * 'modified_compare' => '<=' - operator for testing "modified". accepted values are '!=', '>', '>=', '<', '<=', '='. * 'group' => '' - the group the action belongs to. * 'status' => '' - ActionScheduler_Store::STATUS_COMPLETE or ActionScheduler_Store::STATUS_PENDING. * 'claimed' => NULL - TRUE to find claimed actions, FALSE to find unclaimed actions, a string to find a specific claim ID. * 'per_page' => 5 - Number of results to return. * 'offset' => 0. * 'orderby' => 'date' - accepted values are 'hook', 'group', 'modified', 'date' or 'none'. * 'order' => 'ASC'. * * @param string $return_format OBJECT, ARRAY_A, or ids. * * @return array */ function as_get_scheduled_actions( $args = array(), $return_format = OBJECT ) { if ( ! ActionScheduler::is_initialized( __FUNCTION__ ) ) { return array(); } $store = ActionScheduler::store(); foreach ( array( 'date', 'modified' ) as $key ) { if ( isset( $args[ $key ] ) ) { $args[ $key ] = as_get_datetime_object( $args[ $key ] ); } } $ids = $store->query_actions( $args ); if ( 'ids' === $return_format || 'int' === $return_format ) { return $ids; } $actions = array(); foreach ( $ids as $action_id ) { $actions[ $action_id ] = $store->fetch_action( $action_id ); } if ( ARRAY_A == $return_format ) { foreach ( $actions as $action_id => $action_object ) { $actions[ $action_id ] = get_object_vars( $action_object ); } } return $actions; } /** * Helper function to create an instance of DateTime based on a given * string and timezone. By default, will return the current date/time * in the UTC timezone. * * Needed because new DateTime() called without an explicit timezone * will create a date/time in PHP's timezone, but we need to have * assurance that a date/time uses the right timezone (which we almost * always want to be UTC), which means we need to always include the * timezone when instantiating datetimes rather than leaving it up to * the PHP default. * * @param mixed $date_string A date/time string. Valid formats are explained in http://php.net/manual/en/datetime.formats.php. * @param string $timezone A timezone identifier, like UTC or Europe/Lisbon. The list of valid identifiers is available http://php.net/manual/en/timezones.php. * * @return ActionScheduler_DateTime */ function as_get_datetime_object( $date_string = null, $timezone = 'UTC' ) { if ( is_object( $date_string ) && $date_string instanceof DateTime ) { $date = new ActionScheduler_DateTime( $date_string->format( 'Y-m-d H:i:s' ), new DateTimeZone( $timezone ) ); } elseif ( is_numeric( $date_string ) ) { $date = new ActionScheduler_DateTime( '@' . $date_string, new DateTimeZone( $timezone ) ); } else { $date = new ActionScheduler_DateTime( null === $date_string ? 'now' : $date_string, new DateTimeZone( $timezone ) ); } return $date; } /** * Template Kit Import: Template Kits Import * * API for handling template kit imports * * @package Envato/Template_Kit_Import * @since 2.0.0 */ namespace Template_Kit_Import\API; use Template_Kit_Import\Backend\Template_Kits; use Template_Kit_Import\Utils\Limits; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * API for handling template kit imports * * @since 2.0.0 */ class Requirements extends API { /** * @return \WP_REST_Response */ public function install_requirement( $request ) { Limits::get_instance()->raise_limits(); $requirement = json_decode( $request->get_param( 'requirement' ), true ); if ( ! empty( $requirement ) && ! empty( $requirement['setting'] ) ) { // The front end has sent an API request to set any default settings. if ( ! current_user_can( 'manage_options' ) ) { return $this->format_error( 'installRequirement', 'install_failed', 'Please contact your administrator to setup settings correctly.' ); } $settings_allowlist = array( 'elementor_disable_color_schemes' => 'yes', 'elementor_disable_typography_schemes' => 'yes', ); $setting_name = $requirement['setting']['setting_name']; // Check if our setting is in the allowlist if ( isset( $settings_allowlist[ $setting_name ] ) ) { // Set our setting to the permitted value in the allowlist update_option( $setting_name, $settings_allowlist[ $setting_name ] ); // Tell our front end it worked: return $this->format_success( array( 'success' => $setting_name, ) ); } else { // If our front end tries to set a not allowed setting we return an error. return $this->format_error( 'installRequirement', 'install_failed', 'This setting is not allowed.' ); } } if ( ! empty( $requirement ) && ! empty( $requirement['plugin'] ) ) { if ( ! current_user_can( 'install_plugins' ) ) { return $this->format_error( 'installRequirement', 'install_failed', 'Please contact your administrator to install plugins.' ); } $plugin_slug = $requirement['plugin']['slug']; if ( $plugin_slug === 'elementor-pro' ) { return $this->format_error( 'installRequirement', 'install_failed', 'Please purchase Elementor Pro from here first', array( 'url' => 'https://elementor.com/pro/?ref=2837', ) ); } $install_status = $this->install_plugin( $requirement['plugin'] ); if ( ! $install_status['success'] ) { return $this->format_error( 'installRequirement', 'install_failed', 'Plugin install failed: ' . $install_status['errorMessage'] ); } return $this->format_success( array( 'success' => $plugin_slug, ) ); } if ( ! empty( $requirement ) && ! empty( $requirement['theme'] ) ) { if ( ! current_user_can( 'install_themes' ) ) { return $this->format_error( 'installRequirement', 'install_failed', 'Please contact your administrator to install themes.' ); } if ( is_multisite() ) { return $this->format_error( 'installRequirement', 'install_failed', 'Please contact your administrator to activate this theme in the global network settings.' ); } // current_user_can( 'switch_themes' ) $theme_slug = $requirement['theme']['slug']; $theme = wp_get_theme( $theme_slug ); if ( ! $theme->exists() ) { // install ! $install_status = $this->install_theme( $theme_slug ); if ( ! $install_status['success'] ) { return $this->format_error( 'installRequirement', 'install_failed', 'Theme install failed: ' . $install_status['errorMessage'] ); } } $theme = wp_get_theme( $theme_slug ); if ( ! $theme->exists() || ! $theme->is_allowed() ) { return $this->format_error( 'installRequirement', 'install_failed', 'Unable to activate this theme, please install manually.' ); } switch_theme( $theme->get_stylesheet() ); return $this->format_success( array( 'success' => 'theme', ) ); } if ( ! empty( $requirement ) && ! empty( $requirement['requiredCss'] ) ) { // The front end has sent an API request to set some default CSS if ( ! current_user_can( 'manage_options' ) ) { return $this->format_error( 'installRequirement', 'install_failed', 'Please contact your administrator to install Custom CSS correctly.' ); } $template_kit_id = $requirement['requiredCss']['templateKitId']; $css_filename = $requirement['requiredCss']['file']; try { Template_Kits::get_instance()->install_custom_css_into_customizer( $template_kit_id, $css_filename ); } catch ( \Exception $e ) { } return $this->format_success( array( 'success' => 'customCss', ) ); } return $this->format_error( 'installRequirement', 'generic_api_error', 'Requirement installation failed' ); } private function install_plugin( $plugin_details ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; include_once ABSPATH . 'wp-admin/includes/plugin-install.php'; $all_plugins = get_plugins(); $is_plugin_already_installed = false; foreach ( array_keys( $all_plugins ) as $plugin ) { if ( strpos( $plugin, $plugin_details['file'] ) !== false ) { $is_plugin_already_installed = true; } } if ( $is_plugin_already_installed ) { // just activate it: $activate_status = $this->activate_plugin( $plugin_details['file'] ); } else { $status = array( 'success' => false, ); $api = plugins_api( 'plugin_information', array( 'slug' => sanitize_key( wp_unslash( $plugin_details['slug'] ) ), 'fields' => array( 'sections' => false, ), ) ); if ( is_wp_error( $api ) ) { $status['errorMessage'] = $api->get_error_message(); return $status; } $status['pluginName'] = $api->name; $skin = new \WP_Ajax_Upgrader_Skin(); $upgrader = new \Plugin_Upgrader( $skin ); $result = $upgrader->install( $api->download_link ); if ( is_wp_error( $result ) ) { $status['errorCode'] = $result->get_error_code(); $status['errorMessage'] = $result->get_error_message(); return $status; } elseif ( is_wp_error( $skin->result ) ) { $status['errorCode'] = $skin->result->get_error_code(); $status['errorMessage'] = $skin->result->get_error_message(); return $status; } elseif ( $skin->get_errors()->has_errors() ) { $status['errorMessage'] = $skin->get_error_messages(); return $status; } elseif ( is_null( $result ) ) { global $wp_filesystem; $status['errorCode'] = 'unable_to_connect_to_filesystem'; $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); // Pass through the error from WP_Filesystem if one was raised. if ( $wp_filesystem instanceof \WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); } return $status; } $install_status = install_plugin_install_status( $api ); $activate_status = $this->activate_plugin( $install_status['file'] ); } if ( $activate_status && ! is_wp_error( $activate_status ) ) { $status['success'] = true; } return $status; } private function activate_plugin( $file ) { if ( current_user_can( 'activate_plugin', $file ) && is_plugin_inactive( $file ) ) { $result = activate_plugin( $file, false, false ); if ( is_wp_error( $result ) ) { return $result; } else { return true; } } return false; } private function install_theme( $slug ) { $status = array( 'success' => false, ); if ( ! current_user_can( 'install_themes' ) ) { $status['errorMessage'] = __( 'Sorry, you are not allowed to install themes on this site.' ); return $status; } require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; include_once ABSPATH . 'wp-admin/includes/theme.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; include_once ABSPATH . 'wp-admin/includes/theme-install.php'; $api = themes_api( 'theme_information', array( 'slug' => $slug, 'fields' => array( 'sections' => false ), ) ); if ( is_wp_error( $api ) ) { $status['errorMessage'] = $api->get_error_message(); return $status; } $skin = new \WP_Ajax_Upgrader_Skin(); $upgrader = new \Theme_Upgrader( $skin ); $result = $upgrader->install( $api->download_link ); if ( is_wp_error( $result ) ) { $status['errorCode'] = $result->get_error_code(); $status['errorMessage'] = $result->get_error_message(); return $status; } elseif ( is_wp_error( $skin->result ) ) { $status['errorCode'] = $skin->result->get_error_code(); $status['errorMessage'] = $skin->result->get_error_message(); return $status; } elseif ( $skin->get_errors()->has_errors() ) { $status['errorMessage'] = $skin->get_error_messages(); return $status; } elseif ( is_null( $result ) ) { global $wp_filesystem; $status['errorCode'] = 'unable_to_connect_to_filesystem'; $status['errorMessage'] = __( 'Unable to connect to the filesystem. Please confirm your credentials.' ); // Pass through the error from WP_Filesystem if one was raised. if ( $wp_filesystem instanceof \WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { $status['errorMessage'] = esc_html( $wp_filesystem->errors->get_error_message() ); } return $status; } $status['themeName'] = wp_get_theme( $slug )->get( 'Name' ); $status['success'] = true; return $status; } public function register_api_endpoints() { $this->register_endpoint( 'installRequirement', array( $this, 'install_requirement' ) ); } }