oont-contents/plugins/wp-rocket/inc/deprecated/classes/busting/class-facebook-pickles.php
2025-02-08 15:10:23 +01:00

1214 lines
32 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace WP_Rocket\Busting;
use WP_Rocket\deprecated\DeprecatedClassTrait;
use WP_Rocket\Logger\Logger;
/**
* Manages the cache busting of the Facebook Pixel files.
*
* @since 3.9 deprecated
* @since 3.2
* @author Grégory Viguier
*/
class Facebook_Pickles {
use DeprecatedClassTrait;
/**
* Regex pattern to capture a locale.
*
* @var string
* @since 3.2
* @author Grégory Viguier
*/
const LOCALE_CAPTURE = '(?<locale>[a-zA-Z_-]+)';
/**
* Regex pattern to capture a version.
*
* @var string
* @since 3.2
* @author Grégory Viguier
*/
const VERSION_CAPTURE = '(?<version>[\d\.]+)';
/**
* Regex pattern to capture an app ID.
*
* @var string
* @since 3.2
* @author Grégory Viguier
*/
const APP_ID_CAPTURE = '(?<app_id>\d+)';
/**
* Cache busting files base path.
*
* @var string
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $busting_path;
/**
* Cache busting base URL.
*
* @var string
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $busting_url;
/**
* Main file URL (remote).
* %s is a locale like "en_US".
*
* @var string
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $main_file_url = 'https://connect.facebook.net/%s/fbevents.js';
/**
* Main file name (local).
* %s is like "{{locale}}-{{version}}".
*
* @var string
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $main_file_name = 'fbpix-events-%s.js';
/**
* Config file URL (remote).
* %d is an app ID (a number), %s is a version like "2.8.30".
* The "r" argument is the release segment: it is considered "stable".
*
* @var string
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $config_file_url = 'https://connect.facebook.net/signals/config/%s?v=%s&r=stable';
/**
* Config file name (local).
* %s is like "{{app_id}}-{{version}}".
*
* @var string
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $config_file_name = 'fbpix-config-%s.js';
/**
* Plugins file URL (remote).
* 1st %s is a plugin name like "identity", 2nd %s is a version like "2.8.30".
*
* @var string
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $plugins_file_url = 'https://connect.facebook.net/signals/plugins/%s?v=%s';
/**
* Plugins file name (local).
* %s is like "{{plugin_name}}-{{version}}".
*
* @var string
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $plugins_file_name = 'fbpix-plugin-%s.js';
/**
* Flag to track the replacement.
*
* @var bool
* @since 3.2
* @access private
* @author Grégory Viguier
*/
private $is_replaced = false;
/**
* Constructor.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $busting_path Path to the busting directory.
* @param string $busting_url URL of the busting directory.
*/
public function __construct( $busting_path, $busting_url ) {
self::deprecated_class( '3.9' );
/** Warning: all file names and script URLs are dynamic, and must be run through sprintf(). */
$this->busting_path = $busting_path . 'facebook-tracking/';
$this->busting_url = $busting_url . 'facebook-tracking/';
}
/** ----------------------------------------------------------------------------------------- */
/** MAIN PROCESS ============================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Perform the URL replacement process.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $html HTML contents.
* @return string HTML contents.
*/
public function replace_url( $html ) {
$this->is_replaced = false;
$tags = $this->find_tags( $html );
if ( ! $tags ) {
return $html;
}
Logger::info(
'FACEBOOK PIXEL CACHING PROCESS STARTED.',
[
'fb pixel',
'tag' => $tags['tag_to_search'],
]
);
$all_files = [];
/**
* Fetch the main file: https://connect.facebook.net/{{locale}}/fbevents.js.
*/
$version = $this->get_most_recent_local_version();
$locale = $this->get_locale_from_url( $tags['tag_to_search'] );
$main_file_url = $this->get_main_file_url( $locale );
if ( $version ) {
// At least 1 main file exists locally (but maybe not in the right locale).
$main_file_path = $this->get_busting_file_path( $locale, $version );
$main_file_contents = $this->get_file_contents( $main_file_path, $main_file_url );
} else {
// No cached files yet.
$main_file_contents = $this->get_remote_contents( $main_file_url );
}
if ( ! $main_file_contents ) {
return $html;
}
/**
* Grab some data from the main file and the inline tag: app_id and version.
*/
$variables = $this->get_variables( $main_file_contents, $tags['tag_to_search'] );
if ( ! $variables ) {
return $html;
}
if ( ! $version ) {
// The local file doesn't exist yet, so we couldn't get its version (and so, can't know its path yet) until we fetch a fresh copy.
$main_file_path = $this->get_busting_file_path( $locale, $variables['version'] );
}
$all_files[] = $main_file_path;
unset( $version );
/**
* Fetch the config file: https://connect.facebook.net/signals/config/{{app_id}}?v={{version}}&r={{release_segment}}.
*/
$config_file_path = $this->get_config_file_path( $variables );
if ( ! $config_file_path ) {
return $html;
}
$all_files[] = $config_file_path;
/**
* Fetch all plugin files: https://connect.facebook.net/signals/plugins/{{pluginName}}.js?v={{version}}.
*/
$plugin_file_paths = $this->get_plugin_file_paths( $variables );
if ( ! $plugin_file_paths ) {
return $html;
}
$all_files = array_merge( $all_files, $plugin_file_paths );
/**
* Modify the main file contents.
*/
$busting_file_url = $this->get_busting_file_url( $locale, $variables['version'] );
$busting_dir_url = dirname( $busting_file_url ) . '/';
$main_file_contents = $this->replace_main_file_contents( $main_file_contents, $busting_dir_url );
if ( ! $main_file_contents ) {
return $html;
}
/**
* Save all the changes to the main file.
*/
$updated = $this->update_file_contents( $main_file_path, $main_file_contents );
if ( ! $updated ) {
return $html;
}
/**
* Finally, replace the main file URL by the local one in the inline script tag.
*/
$replace_tag = preg_replace( '@(?:https?:)?//connect\.facebook\.net/[a-zA-Z_-]+/fbevents\.js@i', $busting_file_url, $tags['tag_to_replace'], -1, $count );
if ( ! $count || false === strpos( $html, $tags['tag_to_replace'] ) ) {
Logger::error( 'The local file URL could not be replaced in the page contents.', [ 'fb pixel' ] );
return $html;
}
$html = str_replace( $tags['tag_to_replace'], $replace_tag, $html );
$this->is_replaced = true;
/**
* Triggered once the Facebook pixel URL has been replaced in the page contents.
*
* @since 3.2
* @author Grégory Viguier
*
* @param string $busting_file_url URL of the local main file.
* @param array $all_files An array of all file paths.
*/
do_action( 'rocket_after_facebook_pixel_url_replaced', $busting_file_url, $all_files );
Logger::info(
'Facebook pixel caching process succeeded.',
[
'fb pixel',
'files' => $all_files,
]
);
return $html;
}
/**
* Tell if the replacement was sucessful or not.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @return bool
*/
public function is_replaced() {
return $this->is_replaced;
}
/** ----------------------------------------------------------------------------------------- */
/** GRAB/MANIPULATE DATA IN CONTENTS ======================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Search for elements in the DOM.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $html HTML contents.
* @return array|bool {
* An array on success, described as below. False if nothing is found.
*
* @type string $tag_to_replace The script tag that contains the facebook.net URL: this is the tag that will be replaced in the page HTML.
* @type string $tag_to_search It contains both app ID and facebook.net URL: this is what will be searched in for this data.
*
* When the app ID and the URL are in the same tag, $tag_to_replace and $tag_to_search are the same.
* }
*/
private function find_tags( $html ) {
preg_match_all( '@<script[^>]*?>(.*)</script>@Umsi', $html, $matches, PREG_SET_ORDER );
if ( empty( $matches ) ) {
return false;
}
$tags = [
'app_id' => [],
'url' => [],
'both' => [],
];
foreach ( $matches as $match ) {
list( $tag, $script ) = $match;
if ( ! trim( $script ) ) {
continue;
}
$has_app_id = false;
$has_url = false;
if ( preg_match( '@fbq\s*\(\s*["\']init["\']\s*,\s*["\']' . self::APP_ID_CAPTURE . '["\']@', $script, $matches_init ) ) {
if ( (int) $matches_init['app_id'] > 0 ) {
$has_app_id = true;
}
}
$has_url = (bool) $this->get_locale_from_url( $script );
if ( $has_app_id && $has_url ) {
// OK we have both.
$tags['both'] = $tag;
break;
}
if ( $has_app_id ) {
$tags['app_id'] = $tag;
if ( $tags['url'] ) {
// OK we have both.
break;
}
} elseif ( $has_url ) {
$tags['url'] = $tag;
if ( $tags['app_id'] ) {
// OK we have both.
break;
}
}
}
if ( ! empty( $tags['both'] ) ) {
return [
'tag_to_replace' => $tags['both'],
'tag_to_search' => $tags['both'],
];
}
if ( ! empty( $tags['app_id'] ) && ! empty( $tags['url'] ) ) {
return [
'tag_to_replace' => $tags['url'],
'tag_to_search' => $tags['url'] . $tags['app_id'],
];
}
return false;
}
/**
* Get some values from the main file and the inline script contents.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $main_file_contents Main file contents.
* @param string $tag_contents Inline script contents.
* @return array|bool {
* An array of values. False on failure.
*
* @type string $app_id The app ID.
* @type string $version The file version.
* }
*/
private function get_variables( $main_file_contents = null, $tag_contents = null ) {
$variables = [];
if ( isset( $tag_contents ) ) {
// Retrieve the app ID from the tag contents.
preg_match( '@fbq\s*\(\s*["\']init["\']\s*,\s*["\']' . self::APP_ID_CAPTURE . '["\']@', $tag_contents, $matches );
if ( empty( $matches['app_id'] ) ) {
Logger::error( 'The app ID could not be retrieved from the inline script contents.', [ 'fb pixel' ] );
return false;
}
$variables['app_id'] = $matches['app_id'];
}
if ( isset( $main_file_contents ) ) {
// Retrieve the version from the main file contents.
preg_match( '@fbq\.version\s*=\s*["\']' . self::VERSION_CAPTURE . '["\']\s*;@', $main_file_contents, $matches );
if ( empty( $matches['version'] ) ) {
Logger::error( 'The version could not be retrieved from the main file contents.', [ 'fb pixel' ] );
return false;
}
$variables['version'] = $matches['version'];
}
return $variables;
}
/**
* Perform some replacements in the main file contents. Will be replaced:
* - the CDN_BASE_URL value,
* - the config file URL,
* - the plugins file URL.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $main_file_contents The file contents.
* @param string $busting_dir_url URL of the folder containing the files.
* @return string|bool The new contents on success. False on failure.
*/
private function replace_main_file_contents( $main_file_contents, $busting_dir_url ) {
/**
* Replace the CDN_BASE_URL value.
* From: CDN_BASE_URL:"https://connect.facebook.net/"
* To: CDN_BASE_URL:"https://example.com/wp-content/cache/busting/facebook-tracking/"
*/
$replacement = 'CDN_BASE_URL:"' . $busting_dir_url . '"';
if ( ! strpos( $main_file_contents, $replacement ) ) {
$main_file_contents = preg_replace( '@CDN_BASE_URL\s*:\s*["\'][^"\']+["\']@', $replacement, $main_file_contents, -1, $count );
if ( ! $count ) {
Logger::error( 'The CDN_BASE_URL could not be replaced in the main file contents.', [ 'fb pixel' ] );
return false;
}
}
/**
* Replace the config file URL (https://connect.facebook.net/signals/config/{{app_id}}?v={{version}}&r={{release_segment}}).
* From: CDN_BASE_URL+"signals/config/"+a+"?v="+b+"&r="+c
* To: CDN_BASE_URL+"fbpix-config-"+a+"-"+b+".js" (the release segment is not taken into account, we consider it "stable")
*/
$replacement_pattern = $this->escape_file_name( $this->config_file_name );
$replacement_pattern = sprintf( $replacement_pattern, '"\+[a-zA-Z._]+\+"\-"\+[a-zA-Z._]+\+"' );
$replacement_pattern = 'CDN_BASE_URL\+"' . $replacement_pattern . '"';
if ( ! preg_match( '/' . $replacement_pattern . '/', $main_file_contents ) ) {
$pattern = '@CDN_BASE_URL\s*\+\s*["\']signals/config/["\']\s*\+\s*([a-zA-Z._]+)\s*\+\s*["\']\?v=["\']\s*\+\s*([a-zA-Z._]+)\s*\+\s*["\']&r=["\']\s*\+\s*[a-zA-Z._]+@';
$replacement = 'CDN_BASE_URL+"' . sprintf( $this->config_file_name, '"+$1+"-"+$2+"' ) . '"';
$main_file_contents = preg_replace( $pattern, $replacement, $main_file_contents, -1, $count );
if ( ! $count ) {
Logger::error( 'The config file URL could not be replaced in the main file contents.', [ 'fb pixel' ] );
return false;
}
}
/**
* Replace the plugins file URL (https://connect.facebook.net/signals/plugins/{{plugin_name}}.js?v={{version}}).
* From: CDN_BASE_URL+"signals/plugins/"+b+".js?v="+a.version
* To : CDN_BASE_URL+"fbpix-plugin-"+b+"-"+a.version+".js"
*/
$replacement_pattern = $this->escape_file_name( $this->plugins_file_name );
$replacement_pattern = sprintf( $replacement_pattern, '"\+[a-zA-Z._]+\+"-"\+[a-zA-Z._]+\+"' );
$replacement_pattern = 'CDN_BASE_URL\+"' . $replacement_pattern . '"';
if ( ! preg_match( '/' . $replacement_pattern . '/', $main_file_contents ) ) {
$pattern = '@CDN_BASE_URL\s*\+\s*["\']signals/plugins/["\']\s*\+\s*([a-zA-Z._]+)\s*\+\s*["\']\.js\?v=["\']\s*\+\s*([a-zA-Z._]+)@';
$replacement = 'CDN_BASE_URL+"' . sprintf( $this->plugins_file_name, '"+$1+"-"+$2+"' ) . '"';
$main_file_contents = preg_replace( $pattern, $replacement, $main_file_contents, -1, $count );
if ( ! $count ) {
Logger::error( 'The plugins file URL could not be replaced in the main file contents.', [ 'fb pixel' ] );
return false;
}
}
return $main_file_contents;
}
/** ----------------------------------------------------------------------------------------- */
/** UPDATE/SAVE A LOCAL FILE ================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Save the contents of a URL into a local file if it doesn't exist yet.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $url URL to get the contents from.
* @param string $path Path to the file that will store the URL contents.
* @return bool True on success. False on failure.
*/
private function maybe_save( $url, $path ) {
$filesystem = \rocket_direct_filesystem();
if ( $filesystem->exists( $path ) ) {
// If a previous version is present, keep it.
return true;
}
return (bool) $this->save( $url, $path );
}
/**
* Save the contents of a URL into a local file.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $url URL to get the contents from.
* @param string $path Path to the file that will store the URL contents.
* @return string|bool The file contents on success. False on failure.
*/
private function save( $url, $path ) {
$contents = $this->get_remote_contents( $url );
if ( ! $contents ) {
// Error, we couldn't fetch the file contents.
return false;
}
return $this->update_file_contents( $path, $contents );
}
/**
* Add new contents to a file. If the file doesn't exist, it is created.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $file_path Path to the file to update.
* @param string $file_contents New contents.
* @return string|bool The file contents on success. False on failure.
*/
private function update_file_contents( $file_path, $file_contents ) {
if ( ! \rocket_direct_filesystem()->exists( $this->busting_path ) ) {
\rocket_mkdir_p( $this->busting_path );
}
if ( ! \rocket_put_content( $file_path, $file_contents ) ) {
Logger::error(
'Contents could not be written into file.',
[
'fb pixel',
'path' => $file_path,
]
);
return false;
}
/**
* Triggered once a file contents have been updated.
*
* @since 3.2
* @author Grégory Viguier
*
* @param string $file_path Path to the file to update.
* @param string $file_contents The file contents.
*/
do_action( 'rocket_after_facebook_pixel_file_updated', $file_path, $file_contents );
return $file_contents;
}
/** ----------------------------------------------------------------------------------------- */
/** PUBLIC BULK ACTIONS ON LOCAL FILES ====================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Look for existing local files and update their contents if there's a new version available.
* Actually, if a more recent version exists on the FB side, it will delete all local files and hit the home page to recreate them.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @return bool True on success. False on failure.
*/
public function refresh_all() {
// Get all local main files.
$main_files = $this->get_all_main_files();
if ( ! $main_files ) {
// No files (or there's an error).
return false !== $main_files;
}
$updated = false;
foreach ( $main_files as $local_main_file ) {
$remote_file_contents = $this->get_remote_contents( $this->get_main_file_url( $local_main_file['locale'] ) );
if ( ! $remote_file_contents ) {
continue;
}
$variables = $this->get_variables( $remote_file_contents );
if ( ! $variables ) {
unset( $remote_file_contents, $variables );
continue;
}
if ( version_compare( $local_main_file['version'], $variables['version'] ) >= 0 ) {
unset( $remote_file_contents, $variables );
continue;
}
unset( $remote_file_contents );
$updated = true;
break;
}
if ( ! $updated ) {
return true;
}
// Delete all local files.
$deleted = $this->delete_all();
// Purge all cache files (the URL of the new files changed).
\rocket_clean_domain();
// Preload the home page to recreate the files.
$home_url = user_trailingslashit( home_url(), 'home' );
wp_remote_get(
$home_url,
[
'user-agent' => 'WP Rocket/Homepage Preload',
'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
]
);
/**
* Triggered once the local files have been refreshed.
*
* @since 3.2
* @author Grégory Viguier
*
* @param string $version The new version.
*/
do_action( 'rocket_after_facebook_pixel_files_refreshed', $variables['version'] );
return true;
}
/**
* Delete all Facebook Pixel busting files.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @return bool True on success. False on failure.
*/
public function delete_all() {
$filesystem = \rocket_direct_filesystem();
$files = $this->get_all_files();
if ( ! $files ) {
// No files (or there's an error).
return false !== $files;
}
$error_paths = [];
foreach ( $files as $file_name ) {
if ( ! $filesystem->delete( $this->busting_path . $file_name, false, 'f' ) ) {
$error_paths[] = $this->busting_path . $file_name;
}
}
if ( $error_paths ) {
Logger::error(
'Local file(s) could not be deleted.',
[
'fb pixel',
'paths' => $error_paths,
]
);
}
/**
* Triggered once all local files have been deleted (or not).
*
* @since 3.2
* @author Grégory Viguier
*
* @param array $files An array of file names.
* @param array $error_paths Paths to the files that couldn't be deleted. An empty array if everything is fine.
*/
do_action( 'rocket_after_facebook_pixel_files_deleted', $files, $error_paths );
return ! $error_paths;
}
/** ----------------------------------------------------------------------------------------- */
/** SCAN FOR LOCAL FILES ==================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get all cached files in the directory.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @return array|bool A list of file names. False on failure.
*/
private function get_all_files() {
$filesystem = \rocket_direct_filesystem();
$dir_path = rtrim( $this->busting_path, '\\/' );
if ( ! $filesystem->exists( $dir_path ) ) {
return [];
}
if ( ! $filesystem->is_writable( $dir_path ) ) {
Logger::error(
'Directory is not writable.',
[
'fb pixel',
'path' => $dir_path,
]
);
return false;
}
$dir = $filesystem->dirlist( $dir_path );
if ( false === $dir ) {
Logger::error(
'Could not get the directory contents.',
[
'fb pixel',
'path' => $dir_path,
]
);
return false;
}
if ( ! $dir ) {
return [];
}
$list = [];
foreach ( $dir as $entry ) {
if ( 'f' !== $entry['type'] ) {
continue;
}
if ( preg_match( '@^fbpix-(?:config|events|plugin)-.+\.js$@', $entry['name'], $matches ) ) {
$list[ $entry['name'] ] = $entry['name'];
}
}
return $list;
}
/**
* Get all main files in the directory.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @return array|bool {
* An array of file names (array keys) with following data as values. False on failure.
*
* @type string $locale The locale, like "en_US".
* @type string $version The file version.
* }
*/
private function get_all_main_files() {
$filesystem = \rocket_direct_filesystem();
$dir_path = rtrim( $this->busting_path, '\\/' );
if ( ! $filesystem->exists( $dir_path ) ) {
return [];
}
if ( ! $filesystem->is_writable( $dir_path ) ) {
Logger::error(
'Directory is not writable.',
[
'fb pixel',
'path' => $dir_path,
]
);
return false;
}
$dir = $filesystem->dirlist( $dir_path );
if ( false === $dir ) {
Logger::error(
'could not get the directory contents.',
[
'fb pixel',
'path' => $dir_path,
]
);
return false;
}
if ( ! $dir ) {
return [];
}
$list = [];
$pattern = $this->escape_file_name( $this->main_file_name );
$pattern = sprintf( $pattern, self::LOCALE_CAPTURE . '-' . self::VERSION_CAPTURE );
foreach ( $dir as $entry ) {
if ( 'f' !== $entry['type'] ) {
continue;
}
if ( preg_match( '@^' . $pattern . '$@', $entry['name'], $matches ) ) {
unset( $matches[0] );
$list[ $entry['name'] ] = $matches;
}
}
return $list;
}
/**
* Get the most recent "version" of the main file cached locally.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @return string|bool The version on success. False on failure.
*/
private function get_most_recent_local_version() {
$main_files = $this->get_all_main_files();
if ( ! $main_files ) {
return false;
}
$version = false;
foreach ( $main_files as $file_name => $data ) {
if ( ! $version || version_compare( $data['version'], $version ) > 0 ) {
$version = $data['version'];
}
}
return $version;
}
/**
* Get the oldest "version" of the main file cached locally.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @return string|bool The version on success. False on failure.
*/
private function get_oldest_local_version() {
$main_files = $this->get_all_main_files();
if ( ! $main_files ) {
return false;
}
$version = false;
foreach ( $main_files as $file_name => $data ) {
if ( ! $version || version_compare( $data['version'], $version ) < 0 ) {
$version = $data['version'];
}
}
return $version;
}
/** ----------------------------------------------------------------------------------------- */
/** REMOTE MAIN FILE ======================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the remote Facebook Pixel URL.
*
* @since 3.2
* @access public
* @author Grégory Viguier
*
* @param string $locale A locale string, like 'en_US'.
* @return string
*/
public function get_main_file_url( $locale ) {
return sprintf( $this->main_file_url, $locale );
}
/**
* Extract the locale from a URL to bust.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $url Any string containing the URL to bust.
* @return string|bool The locale on success. False on failure.
*/
private function get_locale_from_url( $url ) {
$pattern = '@//connect\.facebook\.net/' . self::LOCALE_CAPTURE . '/fbevents\.js@i';
if ( ! preg_match( $pattern, $url, $matches ) ) {
return false;
}
return $matches['locale'];
}
/** ----------------------------------------------------------------------------------------- */
/** BUSTING FILE (aka: cached copy of the main file) ======================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the local Facebook Pixel URL (the "main" file).
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $locale A locale string, like 'en_US'.
* @param string $version The script version.
* @return string
*/
private function get_busting_file_url( $locale, $version ) {
$filename = $this->get_busting_file_name( $locale, $version );
// This filter is documented in inc/functions/minify.php.
return apply_filters( 'rocket_js_url', $this->busting_url . $filename );
}
/**
* Get the local Facebook Pixel file name.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $locale A locale string, like 'en_US'.
* @param string $version The script version.
* @return string
*/
private function get_busting_file_name( $locale, $version ) {
return sprintf( $this->main_file_name, $locale . '-' . $version );
}
/**
* Get the local Facebook Pixel file path.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $locale A locale string, like 'en_US'.
* @param string $version The script version.
* @return string
*/
private function get_busting_file_path( $locale, $version ) {
return $this->busting_path . $this->get_busting_file_name( $locale, $version );
}
/** ----------------------------------------------------------------------------------------- */
/** CONFIG FILE ============================================================================= */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the path to the local "config" file. If the file doesn't exist, it is created by fetching its contents remotely, then saved locally.
*
* @since 3.2
* @access private
* @see $this->get_variables()
* @author Grégory Viguier
*
* @param array $variables {
* An array of variable values.
*
* @type int $app_id The app ID.
* @type string $version The file version.
* }
* @return string|bool The file path on success. False on failure.
*/
private function get_config_file_path( $variables ) {
$config_file_url = sprintf( $this->config_file_url, $variables['app_id'], $variables['version'] );
$config_file_name = sprintf( $this->config_file_name, $variables['app_id'] . '-' . $variables['version'] );
$config_file_path = $this->busting_path . $config_file_name;
if ( ! $this->maybe_save( $config_file_url, $config_file_path ) ) {
return false;
}
return $config_file_path;
}
/** ----------------------------------------------------------------------------------------- */
/** PLUGIN FILES ============================================================================ */
/** ----------------------------------------------------------------------------------------- */
/**
* Get the paths to all local "plugin" files. If the files don't exist, they are created by fetching their contents remotely, then saved locally.
*
* @since 3.2
* @access private
* @see $this->get_variables()
* @author Grégory Viguier
*
* @param array $variables {
* An array of variable values.
*
* @type string $app_id The app ID.
* @type string $version The file version.
* }
* @return array|bool An array of file paths on success. False on failure.
*/
private function get_plugin_file_paths( $variables ) {
$paths = [];
$plugin_names = [
'identity',
'microdata',
'inferredEvents',
'dwell',
'sessions',
'timespent',
'ga2fbq',
];
foreach ( $plugin_names as $plugin_name ) {
$plugin_file_url = sprintf( $this->plugins_file_url, $plugin_name, $variables['version'] );
$plugin_file_name = sprintf( $this->plugins_file_name, $plugin_name . '-' . $variables['version'] );
$plugin_file_path = $this->busting_path . $plugin_file_name;
if ( ! $this->maybe_save( $plugin_file_url, $plugin_file_path ) ) {
return false;
}
$paths[] = $plugin_file_path;
}
return $paths;
}
/** ----------------------------------------------------------------------------------------- */
/** TOOLS =================================================================================== */
/** ----------------------------------------------------------------------------------------- */
/**
* Get a file contents. If the file doesn't exist or is not writtable, new contents are fetched remotely.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $file_path Path to the file.
* @param string $file_url URL to the remote file.
* @return string|bool The contents on success, false on failure.
*/
private function get_file_contents( $file_path, $file_url = false ) {
$filesystem = \rocket_direct_filesystem();
if ( $filesystem->is_writable( $file_path ) ) {
// If a previous version is present, return its contents.
$contents = $filesystem->get_contents( $file_path );
if ( $contents ) {
return $contents;
}
// In case the file is empty or we could not get its contents, try to get a fresh copy from remote location.
}
if ( ! $file_url ) {
return false;
}
return $this->get_remote_contents( $file_url );
}
/**
* Get the contents of a URL.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $url The URL to request.
* @return string|bool The contents on success. False on failure.
*/
private function get_remote_contents( $url ) {
try {
$response = wp_remote_get( $url );
} catch ( \Exception $e ) {
Logger::error(
'Remote file could not be fetched.',
[
'fb pixel',
'url' => $url,
'response' => $e->getMessage(),
]
);
return false;
}
if ( is_wp_error( $response ) ) {
Logger::error(
'Remote file could not be fetched.',
[
'fb pixel',
'url' => $url,
'response' => $response->get_error_message(),
]
);
return false;
}
$contents = wp_remote_retrieve_body( $response );
if ( ! $contents ) {
Logger::error(
'Remote file could not be fetched.',
[
'fb pixel',
'url' => $url,
'response' => $response,
]
);
return false;
}
return $contents;
}
/**
* Escape a file name, to be used in a regex pattern (delimiter is `/`).
* `%s` conversion specifications are protected.
*
* @since 3.2
* @access private
* @author Grégory Viguier
*
* @param string $file_name The file name.
* @return string
*/
private function escape_file_name( $file_name ) {
$file_name = explode( '%s', $file_name );
$file_name = array_map( 'preg_quote', $file_name );
return implode( '%s', $file_name );
}
}