notices/class-astra-notices.php000066600000023231152103702400012577 0ustar00 Create custom close notice link in the notice markup. E.g. * `` * It close the notice for 30 days. * * @package Astra Sites * @since 1.4.0 */ if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } if ( ! class_exists( 'Astra_Notices' ) ) : /** * Astra_Notices * * @since 1.4.0 */ class Astra_Notices { /** * Notices * * @access private * @var array Notices. * @since 1.4.0 */ private static $version = '1.1.5'; /** * Notices * * @access private * @var array Notices. * @since 1.4.0 */ private static $notices = array(); /** * Instance * * @access private * @var object Class object. * @since 1.4.0 */ private static $instance; /** * Initiator * * @since 1.4.0 * @return object initialized object of class. */ public static function get_instance() { if ( ! isset( self::$instance ) ) { self::$instance = new self(); } return self::$instance; } /** * Constructor * * @since 1.4.0 */ public function __construct() { add_action( 'admin_notices', array( $this, 'show_notices' ), 30 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); add_action( 'wp_ajax_astra-notice-dismiss', array( $this, 'dismiss_notice' ) ); add_filter( 'wp_kses_allowed_html', array( $this, 'add_data_attributes' ), 10, 2 ); } /** * Filters and Returns a list of allowed tags and attributes for a given context. * * @param Array $allowedposttags Array of allowed tags. * @param String $context Context type (explicit). * @since 1.4.0 * @return Array */ public function add_data_attributes( $allowedposttags, $context ) { $allowedposttags['a']['data-repeat-notice-after'] = true; return $allowedposttags; } /** * Add Notice. * * @since 1.4.0 * @param array $args Notice arguments. * @return void */ public static function add_notice( $args = array() ) { self::$notices[] = $args; } /** * Dismiss Notice. * * @since 1.4.0 * @return void */ public function dismiss_notice() { $notice_id = ( isset( $_POST['notice_id'] ) ) ? sanitize_key( $_POST['notice_id'] ) : ''; $repeat_notice_after = ( isset( $_POST['repeat_notice_after'] ) ) ? absint( $_POST['repeat_notice_after'] ) : ''; $nonce = ( isset( $_POST['nonce'] ) ) ? sanitize_key( $_POST['nonce'] ) : ''; if ( false === wp_verify_nonce( $nonce, 'astra-notices' ) ) { wp_send_json_error( esc_html_e( 'WordPress Nonce not validated.', 'astra' ) ); } // Valid inputs? if ( ! empty( $notice_id ) ) { if ( ! empty( $repeat_notice_after ) ) { set_transient( $notice_id, true, $repeat_notice_after ); } else { update_user_meta( get_current_user_id(), $notice_id, 'notice-dismissed' ); } wp_send_json_success(); } wp_send_json_error(); } /** * Enqueue Scripts. * * @since 1.4.0 * @return void */ public function enqueue_scripts() { wp_register_script( 'astra-notices', self::_get_uri() . 'notices.js', array( 'jquery' ), self::$version, true ); wp_localize_script( 'astra-notices', 'astraNotices', array( '_notice_nonce' => wp_create_nonce( 'astra-notices' ), ) ); } /** * Rating priority sort * * @since 1.5.2 * @param array $array1 array one. * @param array $array2 array two. * @return array */ public function sort_notices( $array1, $array2 ) { if ( ! isset( $array1['priority'] ) ) { $array1['priority'] = 10; } if ( ! isset( $array2['priority'] ) ) { $array2['priority'] = 10; } return $array1['priority'] - $array2['priority']; } /** * Notice Types * * @since 1.4.0 * @return void */ public function show_notices() { $defaults = array( 'id' => '', // Optional, Notice ID. If empty it set `astra-notices-id-<$array-index>`. 'type' => 'info', // Optional, Notice type. Default `info`. Expected [info, warning, notice, error]. 'message' => '', // Optional, Message. 'show_if' => true, // Optional, Show notice on custom condition. E.g. 'show_if' => if( is_admin() ) ? true, false, . 'repeat-notice-after' => '', // Optional, Dismiss-able notice time. It'll auto show after given time. 'display-notice-after' => false, // Optional, Dismiss-able notice time. It'll auto show after given time. 'class' => '', // Optional, Additional notice wrapper class. 'priority' => 10, // Priority of the notice. 'display-with-other-notices' => true, // Should the notice be displayed if other notices are being displayed from Astra_Notices. 'is_dismissible' => true, ); // Count for the notices that are rendered. $notices_displayed = 0; // sort the array with priority. usort( self::$notices, array( $this, 'sort_notices' ) ); foreach ( self::$notices as $key => $notice ) { $notice = wp_parse_args( $notice, $defaults ); $notice['id'] = self::get_notice_id( $notice, $key ); $notice['classes'] = self::get_wrap_classes( $notice ); // Notices visible after transient expire. if ( isset( $notice['show_if'] ) && true === $notice['show_if'] ) { // don't display the notice if it is not supposed to be displayed with other notices. if ( 0 !== $notices_displayed && false === $notice['display-with-other-notices'] ) { continue; } if ( self::is_expired( $notice ) ) { self::markup( $notice ); ++$notices_displayed; } } } } /** * Markup Notice. * * @since 1.4.0 * @param array $notice Notice markup. * @return void */ public static function markup( $notice = array() ) { wp_enqueue_script( 'astra-notices' ); do_action( 'astra_notice_before_markup' ); do_action( "astra_notice_before_markup_{$notice['id']}" ); ?>
identifier = $this->prefix . '_' . $this->action; add_action( 'wp_ajax_' . $this->identifier, array( $this, 'maybe_handle' ) ); add_action( 'wp_ajax_nopriv_' . $this->identifier, array( $this, 'maybe_handle' ) ); } /** * Set data used during the request * * @param array $data Data. * * @return $this */ public function data( $data ) { $this->data = $data; return $this; } /** * Dispatch the async request * * @return array|WP_Error */ public function dispatch() { $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); $args = $this->get_post_args(); return wp_remote_post( esc_url_raw( $url ), $args ); } /** * Get query args * * @return array */ protected function get_query_args() { if ( property_exists( $this, 'query_args' ) ) { return $this->query_args; } return array( 'action' => $this->identifier, 'nonce' => wp_create_nonce( $this->identifier ), ); } /** * Get query URL * * @return string */ protected function get_query_url() { if ( property_exists( $this, 'query_url' ) ) { return $this->query_url; } return admin_url( 'admin-ajax.php' ); } /** * Get post args * * @return array */ protected function get_post_args() { if ( property_exists( $this, 'post_args' ) ) { return $this->post_args; } return array( 'timeout' => 0.01, 'blocking' => false, 'body' => $this->data, 'cookies' => $_COOKIE, 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), ); } /** * Maybe handle * * Check for correct nonce and pass to handler. */ public function maybe_handle() { // Don't lock up other requests while processing session_write_close(); check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); wp_die(); } /** * Handle * * Override this method to perform any actions required * during the async request. */ abstract protected function handle(); } } batch-processing/class-wp-background-process.php000066600000025176152103702400016043 0ustar00cron_hook_identifier = $this->identifier . '_cron'; $this->cron_interval_identifier = $this->identifier . '_cron_interval'; add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) ); add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) ); } /** * Dispatch * * @access public * @return void */ public function dispatch() { // Schedule the cron healthcheck. $this->schedule_event(); // Perform remote post. return parent::dispatch(); } /** * Push to queue * * @param mixed $data Data. * * @return $this */ public function push_to_queue( $data ) { $this->data[] = $data; return $this; } /** * Save queue * * @return $this */ public function save() { $key = $this->generate_key(); if ( ! empty( $this->data ) ) { update_site_option( $key, $this->data ); } return $this; } /** * Update queue * * @param string $key Key. * @param array $data Data. * * @return $this */ public function update( $key, $data ) { if ( ! empty( $data ) ) { update_site_option( $key, $data ); } return $this; } /** * Delete queue * * @param string $key Key. * * @return $this */ public function delete( $key ) { delete_site_option( $key ); return $this; } /** * Generate key * * Generates a unique key based on microtime. Queue items are * given a unique key so that they can be merged upon save. * * @param int $length Length. * * @return string */ protected function generate_key( $length = 64 ) { $unique = md5( microtime() . rand() ); $prepend = $this->identifier . '_batch_'; return substr( $prepend . $unique, 0, $length ); } /** * Maybe process queue * * Checks whether data exists within the queue and that * the process is not already running. */ public function maybe_handle() { // Don't lock up other requests while processing session_write_close(); if ( $this->is_process_running() ) { // Background process already running. wp_die(); } if ( $this->is_queue_empty() ) { // No data to process. wp_die(); } check_ajax_referer( $this->identifier, 'nonce' ); $this->handle(); wp_die(); } /** * Is queue empty * * @return bool */ protected function is_queue_empty() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; } $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; $count = $wpdb->get_var( $wpdb->prepare( " SELECT COUNT(*) FROM {$table} WHERE {$column} LIKE %s ", $key ) ); return ( $count > 0 ) ? false : true; } /** * Is process running * * Check whether the current process is already running * in a background process. */ protected function is_process_running() { if ( get_site_transient( $this->identifier . '_process_lock' ) ) { // Process already running. return true; } return false; } /** * Lock process * * Lock the process so that multiple instances can't run simultaneously. * Override if applicable, but the duration should be greater than that * defined in the time_exceeded() method. */ protected function lock_process() { $this->start_time = time(); // Set start time of current process. $lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration ); set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); } /** * Unlock process * * Unlock the process so that other instances can spawn. * * @return $this */ protected function unlock_process() { delete_site_transient( $this->identifier . '_process_lock' ); return $this; } /** * Get batch * * @return stdClass Return the first batch from the queue */ protected function get_batch() { global $wpdb; $table = $wpdb->options; $column = 'option_name'; $key_column = 'option_id'; $value_column = 'option_value'; if ( is_multisite() ) { $table = $wpdb->sitemeta; $column = 'meta_key'; $key_column = 'meta_id'; $value_column = 'meta_value'; } $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; $query = $wpdb->get_row( $wpdb->prepare( " SELECT * FROM {$table} WHERE {$column} LIKE %s ORDER BY {$key_column} ASC LIMIT 1 ", $key ) ); $batch = new stdClass(); $batch->key = $query->$column; $batch->data = maybe_unserialize( $query->$value_column ); return $batch; } /** * Handle * * Pass each queue item to the task handler, while remaining * within server memory and time limit constraints. */ protected function handle() { $this->lock_process(); do { $batch = $this->get_batch(); foreach ( $batch->data as $key => $value ) { $task = $this->task( $value ); if ( false !== $task ) { $batch->data[ $key ] = $task; } else { unset( $batch->data[ $key ] ); } if ( $this->time_exceeded() || $this->memory_exceeded() ) { // Batch limits reached. break; } } // Update or delete current batch. if ( ! empty( $batch->data ) ) { $this->update( $batch->key, $batch->data ); } else { $this->delete( $batch->key ); } } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); $this->unlock_process(); // Start next batch or complete process. if ( ! $this->is_queue_empty() ) { $this->dispatch(); } else { $this->complete(); } wp_die(); } /** * Memory exceeded * * Ensures the batch process never exceeds 90% * of the maximum WordPress memory. * * @return bool */ protected function memory_exceeded() { $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory $current_memory = memory_get_usage( true ); $return = false; if ( $current_memory >= $memory_limit ) { $return = true; } return apply_filters( $this->identifier . '_memory_exceeded', $return ); } /** * Get memory limit * * @return int */ protected function get_memory_limit() { if ( function_exists( 'ini_get' ) ) { $memory_limit = ini_get( 'memory_limit' ); } else { // Sensible default. $memory_limit = '128M'; } if ( ! $memory_limit || -1 === intval( $memory_limit ) ) { // Unlimited, set to 32GB. $memory_limit = '32000M'; } return intval( $memory_limit ) * 1024 * 1024; } /** * Time exceeded. * * Ensures the batch never exceeds a sensible time limit. * A timeout limit of 30s is common on shared hosting. * * @return bool */ protected function time_exceeded() { $finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds $return = false; if ( time() >= $finish ) { $return = true; } return apply_filters( $this->identifier . '_time_exceeded', $return ); } /** * Complete. * * Override if applicable, but ensure that the below actions are * performed, or, call parent::complete(). */ protected function complete() { // Unschedule the cron healthcheck. $this->clear_scheduled_event(); } /** * Schedule cron healthcheck * * @access public * @param mixed $schedules Schedules. * @return mixed */ public function schedule_cron_healthcheck( $schedules ) { $interval = apply_filters( $this->identifier . '_cron_interval', 5 ); if ( property_exists( $this, 'cron_interval' ) ) { $interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval ); } // Adds every 5 minutes to the existing schedules. $schedules[ $this->identifier . '_cron_interval' ] = array( 'interval' => MINUTE_IN_SECONDS * $interval, 'display' => sprintf( __( 'Every %d Minutes', 'astra' ), $interval ), ); return $schedules; } /** * Handle cron healthcheck * * Restart the background process if not already running * and data exists in the queue. */ public function handle_cron_healthcheck() { if ( $this->is_process_running() ) { // Background process already running. exit; } if ( $this->is_queue_empty() ) { // No data to process. $this->clear_scheduled_event(); exit; } $this->handle(); exit; } /** * Schedule event */ protected function schedule_event() { if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) { wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier ); } } /** * Clear scheduled event */ protected function clear_scheduled_event() { $timestamp = wp_next_scheduled( $this->cron_hook_identifier ); if ( $timestamp ) { wp_unschedule_event( $timestamp, $this->cron_hook_identifier ); } } /** * Cancel Process * * Stop processing queue items, clear cronjob and delete batch. * */ public function cancel_process() { if ( ! $this->is_queue_empty() ) { $batch = $this->get_batch(); $this->delete( $batch->key ); wp_clear_scheduled_hook( $this->cron_hook_identifier ); } } /** * Task * * Override this method to perform any actions required on each * queue item. Return the modified item for further processing * in the next pass through. Or, return false to remove the * item from the queue. * * @param mixed $item Queue item to iterate over. * * @return mixed */ abstract protected function task( $item ); } }