From 9d4123cee1867ee7199b06bdc92d40611f547ecc Mon Sep 17 00:00:00 2001 From: Zach van Rijn Date: Wed, 21 Jul 2021 14:54:07 -0500 Subject: Initial unmodified import from Astra (Version: 3.6.5) @ /wp-content/themes/astra/. --- .../class-astra-wp-async-request.php | 163 +++++ .../class-astra-wp-background-process.php | 506 +++++++++++++++ inc/lib/notices/class-astra-notices.php | 365 +++++++++++ inc/lib/notices/notices.js | 95 +++ inc/lib/webfont/class-astra-webfont-loader.php | 685 +++++++++++++++++++++ 5 files changed, 1814 insertions(+) create mode 100644 inc/lib/batch-processing/class-astra-wp-async-request.php create mode 100644 inc/lib/batch-processing/class-astra-wp-background-process.php create mode 100644 inc/lib/notices/class-astra-notices.php create mode 100644 inc/lib/notices/notices.js create mode 100644 inc/lib/webfont/class-astra-webfont-loader.php (limited to 'inc/lib') diff --git a/inc/lib/batch-processing/class-astra-wp-async-request.php b/inc/lib/batch-processing/class-astra-wp-async-request.php new file mode 100644 index 0000000..eee0d6e --- /dev/null +++ b/inc/lib/batch-processing/class-astra-wp-async-request.php @@ -0,0 +1,163 @@ +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(); + + } +} diff --git a/inc/lib/batch-processing/class-astra-wp-background-process.php b/inc/lib/batch-processing/class-astra-wp-background-process.php new file mode 100644 index 0000000..98c7486 --- /dev/null +++ b/inc/lib/batch-processing/class-astra-wp-background-process.php @@ -0,0 +1,506 @@ +cron_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 ); + + } +} diff --git a/inc/lib/notices/class-astra-notices.php b/inc/lib/notices/class-astra-notices.php new file mode 100644 index 0000000..3b5743d --- /dev/null +++ b/inc/lib/notices/class-astra-notices.php @@ -0,0 +1,365 @@ + 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() { + + if ( ! apply_filters( 'astra_notices_user_cap_check', current_user_can( 'manage_options' ) ) ) { + return; + } + + $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']}" ); + + ?> +
+
+ + +
+
+ remote_url = $url; + + // Add a cleanup routine. + $this->schedule_cleanup(); + add_action( 'astra_delete_fonts_folder', array( $this, 'astra_delete_fonts_folder' ) ); + } + + /** + * Get the local URL which contains the styles. + * + * Fallback to the remote URL if we were unable to write the file locally. + * + * @access public + * @since 3.6.0 + * @return string + */ + public function get_url() { + + // Check if the local stylesheet exists. + if ( $this->local_file_exists() ) { + + // Attempt to update the stylesheet. Return the local URL on success. + if ( $this->write_stylesheet() ) { + return $this->get_local_stylesheet_url(); + } + } + + $astra_font_url = file_exists( $this->get_local_stylesheet_path() ) ? $this->get_local_stylesheet_url() : $this->remote_url; + + // If the local file exists, return its URL, with a fallback to the remote URL. + astra_update_option( 'astra_font_url', wp_json_encode( $astra_font_url ) ); + + return $astra_font_url; + } + + /** + * Get the local stylesheet URL. + * + * @access public + * @since 3.6.0 + * @return string + */ + public function get_local_stylesheet_url() { + if ( ! $this->local_stylesheet_url ) { + $this->local_stylesheet_url = str_replace( + $this->get_base_path(), + $this->get_base_url(), + $this->get_local_stylesheet_path() + ); + } + return $this->local_stylesheet_url; + } + + /** + * Get styles with fonts downloaded locally. + * + * @access public + * @since 3.6.0 + * @return string + */ + public function get_styles() { + + // If we already have the local file, return its contents. + $local_stylesheet_contents = $this->get_local_stylesheet_contents(); + if ( $local_stylesheet_contents ) { + return $local_stylesheet_contents; + } + + // Get the remote URL contents. + $this->remote_styles = $this->get_remote_url_contents(); + + // Get an array of locally-hosted files. + $files = $this->get_local_files_from_css(); + + // Convert paths to URLs. + foreach ( $files as $remote => $local ) { + $files[ $remote ] = str_replace( + $this->get_base_path(), + $this->get_base_url(), + $local + ); + } + + $this->css = str_replace( + array_keys( $files ), + array_values( $files ), + $this->remote_styles + ); + + $this->write_stylesheet(); + + return $this->css; + } + + /** + * Get local stylesheet contents. + * + * @access public + * @since 3.6.0 + * @return string|false Returns the remote URL contents. + */ + public function get_local_stylesheet_contents() { + $local_path = $this->get_local_stylesheet_path(); + + // Check if the local stylesheet exists. + if ( $this->local_file_exists() ) { + + // Attempt to update the stylesheet. Return false on fail. + if ( ! $this->write_stylesheet() ) { + return false; + } + } + + ob_start(); + include $local_path; // PHPCS:ignore WPThemeReview.CoreFunctionality.FileInclude.FileIncludeFound + return ob_get_clean(); + } + + /** + * Get remote file contents. + * + * @access public + * @since 3.6.0 + * @return string Returns the remote URL contents. + */ + public function get_remote_url_contents() { + + /** + * The user-agent we want to use. + * + * The default user-agent is the only one compatible with woff (not woff2) + * which also supports unicode ranges. + */ + $user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/603.3.8 (KHTML, like Gecko) Version/10.1.2 Safari/603.3.8'; + + // Switch to a user-agent supporting woff2 if we don't need to support IE. + if ( 'woff2' === $this->font_format ) { + $user_agent = 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:73.0) Gecko/20100101 Firefox/73.0'; + } + + // Get the response. + $response = wp_remote_get( $this->remote_url, array( 'user-agent' => $user_agent ) ); + + // Early exit if there was an error. + if ( is_wp_error( $response ) ) { + return ''; + } + + // Get the CSS from our response. + $contents = wp_remote_retrieve_body( $response ); + + return $contents; + } + + /** + * Download files mentioned in our CSS locally. + * + * @access public + * @since 3.6.0 + * @return array Returns an array of remote URLs and their local counterparts. + */ + public function get_local_files_from_css() { + $font_files = $this->get_remote_files_from_css(); + $stored = get_site_option( 'ast_downloaded_font_files', array() ); + $change = false; // If in the end this is true, we need to update the cache option. + + if ( ! defined( 'FS_CHMOD_DIR' ) ) { + define( 'FS_CHMOD_DIR', ( 0755 & ~ umask() ) ); + } + + // If the fonts folder don't exist, create it. + if ( ! file_exists( $this->get_fonts_folder() ) ) { + $this->get_filesystem()->mkdir( $this->get_fonts_folder(), FS_CHMOD_DIR ); + } + + foreach ( $font_files as $font_family => $files ) { + + // The folder path for this font-family. + $folder_path = $this->get_fonts_folder() . '/' . $font_family; + + // If the folder doesn't exist, create it. + if ( ! file_exists( $folder_path ) ) { + $this->get_filesystem()->mkdir( $folder_path, FS_CHMOD_DIR ); + } + + foreach ( $files as $url ) { + + // Get the filename. + $filename = basename( wp_parse_url( $url, PHP_URL_PATH ) ); + $font_path = $folder_path . '/' . $filename; + + // Check if the file already exists. + if ( file_exists( $font_path ) ) { + + // Skip if already cached. + if ( isset( $stored[ $url ] ) ) { + continue; + } + + // Add file to the cache and change the $changed var to indicate we need to update the option. + $stored[ $url ] = $font_path; + $change = true; + + // Since the file exists we don't need to proceed with downloading it. + continue; + } + + /** + * If we got this far, we need to download the file. + */ + // require file.php if the download_url function doesn't exist. + if ( ! function_exists( 'download_url' ) ) { + require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' ); // PHPCS:ignore WPThemeReview.CoreFunctionality.FileInclude.FileIncludeFound + } + + // Download file to temporary location. + $tmp_path = download_url( $url ); + + // Make sure there were no errors. + if ( is_wp_error( $tmp_path ) ) { + continue; + } + + // Move temp file to final destination. + $success = $this->get_filesystem()->move( $tmp_path, $font_path, true ); + if ( $success ) { + $stored[ $url ] = $font_path; + $change = true; + } + } + } + + // If there were changes, update the option. + if ( $change ) { + + // Cleanup the option and then save it. + foreach ( $stored as $url => $path ) { + if ( ! file_exists( $path ) ) { + unset( $stored[ $url ] ); + } + } + + update_site_option( 'ast_downloaded_font_files', $stored ); + } + + return $stored; + } + + /** + * Get the font files and preload them. + * + * @access public + */ + public function preload_local_fonts() { + // Make sure variables are set. + // Get the remote URL contents. + $styles = $this->get_styles(); + + // Get an array of locally-hosted files. + $local_font = array(); + $font_files = $this->get_remote_files_from_css( $styles ); + + foreach ( $font_files as $font_family => $files ) { + if ( is_array( $files ) ) { + $local_font[] = end( $files ); + } + } + + // Caching this for further optimization. + update_site_option( 'astra_local_font_files', $local_font ); + + foreach ( $local_font as $key => $local_font ) { + if ( $local_font ) { + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + } + + /** + * Get font files from the CSS. + * + * @access public + * @since 3.6.0 + * @param string $remote_styles Remote stylesheet data. + * + * @return array Returns an array of font-families and the font-files used. + */ + public function get_remote_files_from_css( $remote_styles = '' ) { + + if ( '' === $remote_styles ) { + $remote_styles = $this->remote_styles; + } + + $font_faces = explode( '@font-face', $remote_styles ); + + $result = array(); + + // Loop all our font-face declarations. + foreach ( $font_faces as $font_face ) { + + // Make sure we only process styles inside this declaration. + $style = explode( '}', $font_face ); + $style = isset( $style[0] ) ? $style[0] : ''; + + // Sanity check. + if ( false === strpos( $style, 'font-family' ) ) { + continue; + } + + // Get an array of our font-families. + preg_match_all( '/font-family.*?\;/', $style, $matched_font_families ); + + // Get an array of our font-files. + preg_match_all( '/url\(.*?\)/i', $style, $matched_font_files ); + + // Get the font-family name. + $font_family = 'unknown'; + if ( isset( $matched_font_families[0] ) && isset( $matched_font_families[0][0] ) ) { + $font_family = rtrim( ltrim( $matched_font_families[0][0], 'font-family:' ), ';' ); + $font_family = trim( str_replace( array( "'", ';' ), '', $font_family ) ); + $font_family = sanitize_key( strtolower( str_replace( ' ', '-', $font_family ) ) ); + } + + // Make sure the font-family is set in our array. + if ( ! isset( $result[ $font_family ] ) ) { + $result[ $font_family ] = array(); + } + + // Get files for this font-family and add them to the array. + foreach ( $matched_font_files as $match ) { + + // Sanity check. + if ( ! isset( $match[0] ) ) { + continue; + } + + // Add the file URL. + $result[ $font_family ][] = rtrim( ltrim( $match[0], 'url(' ), ')' ); + } + + // Make sure we have unique items. + // We're using array_flip here instead of array_unique for improved performance. + $result[ $font_family ] = array_flip( array_flip( $result[ $font_family ] ) ); + } + + return $result; + } + + /** + * Write the CSS to the filesystem. + * + * @access protected + * @since 3.6.0 + * @return string|false Returns the absolute path of the file on success, or false on fail. + */ + protected function write_stylesheet() { + $file_path = $this->get_local_stylesheet_path(); + $filesystem = $this->get_filesystem(); + + if ( ! defined( 'FS_CHMOD_DIR' ) ) { + define( 'FS_CHMOD_DIR', ( 0755 & ~ umask() ) ); + } + + // If the folder doesn't exist, create it. + if ( ! file_exists( $this->get_fonts_folder() ) ) { + $this->get_filesystem()->mkdir( $this->get_fonts_folder(), FS_CHMOD_DIR ); + } + + // If the file doesn't exist, create it. Return false if it can not be created. + if ( ! $filesystem->exists( $file_path ) && ! $filesystem->touch( $file_path ) ) { + return false; + } + + // If we got this far, we need to write the file. + // Get the CSS. + if ( ! $this->css ) { + $this->get_styles(); + } + + // Put the contents in the file. Return false if that fails. + if ( ! $filesystem->put_contents( $file_path, $this->css ) ) { + return false; + } + + return $file_path; + } + + /** + * Get the stylesheet path. + * + * @access public + * @since 3.6.0 + * @return string + */ + public function get_local_stylesheet_path() { + if ( ! $this->local_stylesheet_path ) { + $this->local_stylesheet_path = $this->get_fonts_folder() . '/' . $this->get_local_stylesheet_filename() . '.css'; + } + return $this->local_stylesheet_path; + } + + /** + * Get the local stylesheet filename. + * + * This is a hash, generated from the site-URL, the wp-content path and the URL. + * This way we can avoid issues with sites changing their URL, or the wp-content path etc. + * + * @access public + * @since 3.6.0 + * @return string + */ + public function get_local_stylesheet_filename() { + return apply_filters( 'astra_local_font_file_name', 'astra-local-fonts' ); + } + + /** + * Set the font-format to be used. + * + * @access public + * @since 3.6.0 + * @param string $format The format to be used. Use "woff" or "woff2". + * @return void + */ + public function set_font_format( $format = 'woff2' ) { + $this->font_format = apply_filters( 'astra_local_google_fonts_format', $format ); + } + + /** + * Check if the local stylesheet exists. + * + * @access public + * @since 3.6.0 + * @return bool + */ + public function local_file_exists() { + return ( ! file_exists( $this->get_local_stylesheet_path() ) ); + } + + /** + * Get the base path. + * + * @access public + * @since 3.6.0 + * @return string + */ + public function get_base_path() { + if ( ! $this->base_path ) { + $this->base_path = apply_filters( 'astra_local_fonts_base_path', $this->get_filesystem()->wp_content_dir() ); + } + return $this->base_path; + } + + /** + * Get the base URL. + * + * @access public + * @since 3.6.0 + * @return string + */ + public function get_base_url() { + if ( ! $this->base_url ) { + $this->base_url = apply_filters( 'astra_local_fonts_base_url', content_url() ); + } + return $this->base_url; + } + + /** + * Get the subfolder name. + * + * @access public + * @since 3.6.0 + * @return string + */ + public function get_subfolder_name() { + if ( ! $this->subfolder_name ) { + $this->subfolder_name = apply_filters( 'astra_local_fonts_directory_name', 'astra-local-fonts' ); + } + return $this->subfolder_name; + } + + /** + * Get the folder for fonts. + * + * @access public + * @return string + */ + public function get_fonts_folder() { + if ( ! $this->fonts_folder ) { + $this->fonts_folder = $this->get_base_path(); + if ( $this->get_subfolder_name() ) { + $this->fonts_folder .= '/' . $this->get_subfolder_name(); + } + } + return $this->fonts_folder; + } + + /** + * Schedule a cleanup. + * + * Deletes the fonts files on a regular basis. + * This way font files will get updated regularly, + * and we avoid edge cases where unused files remain in the server. + * + * @access public + * @since 3.6.0 + * @return void + */ + public function schedule_cleanup() { + if ( ! is_multisite() || ( is_multisite() && is_main_site() ) ) { + if ( ! wp_next_scheduled( 'astra_delete_fonts_folder' ) && ! wp_installing() ) { + wp_schedule_event( time(), self::CLEANUP_FREQUENCY, 'astra_delete_fonts_folder' ); // phpcs:ignore WPThemeReview.PluginTerritory.ForbiddenFunctions.cron_functionality_wp_schedule_event + } + } + } + + /** + * Delete the fonts folder. + * + * This runs as part of a cleanup routine. + * + * @access public + * @since 3.6.0 + * @return bool + */ + public function astra_delete_fonts_folder() { + // Delete previously created supportive options. + astra_delete_option( 'astra_font_url' ); + delete_site_option( 'astra_local_font_files' ); + return $this->get_filesystem()->delete( $this->get_fonts_folder(), true ); + } + + /** + * Get the filesystem. + * + * @access protected + * @since 3.6.0 + * @return \WP_Filesystem_Base + */ + protected function get_filesystem() { + global $wp_filesystem; + + // If the filesystem has not been instantiated yet, do it here. + if ( ! $wp_filesystem ) { + if ( ! function_exists( 'WP_Filesystem' ) ) { + require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' ); // PHPCS:ignore WPThemeReview.CoreFunctionality.FileInclude.FileIncludeFound + } + WP_Filesystem(); + } + return $wp_filesystem; + } +} + +/** + * Create instance of Astra_WebFont_Loader class. + * + * @param string $font_url Google font URL to set data. + * @return object + * @since 3.6.0 + */ +function astra_webfont_loader_instance( $font_url = '' ) { + return new Astra_WebFont_Loader( $font_url ); +} -- cgit v1.2.3-70-g09d2