678 lines
17 KiB
PHP
678 lines
17 KiB
PHP
<?php
|
|
namespace WP_Rocket\Busting;
|
|
|
|
use WP_Rocket\deprecated\DeprecatedClassTrait;
|
|
use WP_Rocket\Logger\Logger;
|
|
|
|
/**
|
|
* Manages the cache busting of the Facebook SDK file.
|
|
*
|
|
* @since 3.9 deprecated.
|
|
* @since 3.2
|
|
* @author Grégory Viguier
|
|
*/
|
|
class Facebook_SDK extends Abstract_Busting {
|
|
use DeprecatedClassTrait;
|
|
|
|
/**
|
|
* Facebook SDK URL.
|
|
* %s is a locale like "en_US".
|
|
*
|
|
* @var string
|
|
* @since 3.2
|
|
* @access protected
|
|
* @author Grégory Viguier
|
|
*/
|
|
protected $url = 'https://connect.facebook.net/%s/sdk.js';
|
|
|
|
/**
|
|
* Filename for the cache busting file.
|
|
* %s is a locale like "en_US".
|
|
*
|
|
* @var string
|
|
* @since 3.2
|
|
* @access protected
|
|
* @author Grégory Viguier
|
|
*/
|
|
protected $filename = 'fbsdk-%s.js';
|
|
|
|
/**
|
|
* Flag to track the replacement.
|
|
*
|
|
* @var bool
|
|
* @since 3.2
|
|
* @access private
|
|
* @author Grégory Viguier
|
|
*/
|
|
protected $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: the file name and script URL are dynamic, and must be run through sprintf(). */
|
|
$this->busting_path = $busting_path . 'facebook-tracking/';
|
|
$this->busting_url = $busting_url . 'facebook-tracking/';
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
$tag = $this->find( '<script[^>]*?>(.*)<\/script>', $html );
|
|
|
|
if ( ! $tag ) {
|
|
return $html;
|
|
}
|
|
|
|
Logger::info(
|
|
'FACEBOOK SDK CACHING PROCESS STARTED.',
|
|
[
|
|
'fb sdk',
|
|
'tag' => $tag,
|
|
]
|
|
);
|
|
|
|
$locale = $this->get_locale_from_url( $tag );
|
|
$remote_url = $this->get_url( $locale );
|
|
|
|
if ( ! $this->save( $remote_url ) ) {
|
|
return $html;
|
|
}
|
|
|
|
$file_url = $this->get_busting_file_url( $locale );
|
|
$replace_tag = preg_replace( '@(?:https?:)?//connect\.facebook\.net/[a-zA-Z_-]+/sdk\.js@i', $file_url, $tag, -1, $count );
|
|
|
|
if ( ! $count || false === strpos( $html, $tag ) ) {
|
|
Logger::error( 'The local file URL could not be replaced in the page contents.', [ 'fb sdk' ] );
|
|
return $html;
|
|
}
|
|
|
|
$html = str_replace( $tag, $replace_tag, $html );
|
|
$file_path = $this->get_busting_file_path( $locale );
|
|
$xfbml = $this->get_xfbml_from_url( $tag ); // Default value should be set to false.
|
|
$app_id = $this->get_appId_from_url( $tag ); // APP_ID is the only required value.
|
|
$url_version = $this->get_version_from_url( $tag );
|
|
$version = false === $url_version ? 'v5.0' : $url_version; // If version is not available set it to the latest: v.5.0.
|
|
|
|
if ( false !== $app_id ) {
|
|
// Add FB async init.
|
|
$fb_async_script = '<script>window.fbAsyncInit = function fbAsyncInit () {FB.init({appId: \'' . $app_id . '\',xfbml: ' . $xfbml . ',version: \'' . $version . '\'})}</script>';
|
|
$html = str_replace( '</body>', $fb_async_script . '</body>', $html );
|
|
}
|
|
|
|
$this->is_replaced = true;
|
|
|
|
/**
|
|
* Triggered once the Facebook SDK URL has been replaced in the page contents.
|
|
*
|
|
* @since 3.2
|
|
* @author Grégory Viguier
|
|
*
|
|
* @param string $file_url URL of the local main file.
|
|
* @param string $file_path Path to the local file.
|
|
*/
|
|
do_action( 'rocket_after_facebook_sdk_url_replaced', $file_url, $file_path );
|
|
|
|
Logger::info(
|
|
'Facebook SDK caching process succeeded.',
|
|
[
|
|
'fb sdk',
|
|
'file' => $file_path,
|
|
]
|
|
);
|
|
|
|
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 an element in the DOM.
|
|
*
|
|
* @since 3.2
|
|
* @access private
|
|
* @author Grégory Viguier
|
|
*
|
|
* @param string $pattern Pattern to match.
|
|
* @param string $html HTML contents.
|
|
* @return string|bool The matched HTML on success. False if nothing is found.
|
|
*/
|
|
protected function find( $pattern, $html ) {
|
|
preg_match_all( '/' . $pattern . '/Umsi', $html, $matches, PREG_SET_ORDER );
|
|
|
|
if ( empty( $matches ) ) {
|
|
return false;
|
|
}
|
|
|
|
foreach ( $matches as $match ) {
|
|
if ( trim( $match[1] ) && preg_match( '@//connect\.facebook\.net/[a-zA-Z_-]+/sdk\.js@i', $match[1] ) ) {
|
|
return $match[0];
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/** ----------------------------------------------------------------------------------------- */
|
|
/** 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 public
|
|
* @author Grégory Viguier
|
|
*
|
|
* @param string $url URL to get the contents from.
|
|
* @return bool True on success. False on failure.
|
|
*/
|
|
public function save( $url ) {
|
|
$locale = $this->get_locale_from_url( $url );
|
|
$path = $this->get_busting_file_path( $locale );
|
|
|
|
if ( \rocket_direct_filesystem()->exists( $path ) ) {
|
|
// If a previous version is present, keep it.
|
|
return true;
|
|
}
|
|
|
|
return $this->refresh_save( $url );
|
|
}
|
|
|
|
/**
|
|
* Save the contents of a URL into a local file.
|
|
*
|
|
* @since 3.2
|
|
* @access public
|
|
* @author Grégory Viguier
|
|
*
|
|
* @param string $url URL to get the contents from.
|
|
* @return bool True on success. False on failure.
|
|
*/
|
|
public function refresh_save( $url ) {
|
|
$content = $this->get_file_content( $url );
|
|
|
|
if ( ! $content ) {
|
|
// Error, we couldn't fetch the file contents.
|
|
return false;
|
|
}
|
|
|
|
$locale = $this->get_locale_from_url( $url );
|
|
$path = $this->get_busting_file_path( $locale );
|
|
|
|
return (bool) $this->update_file_contents( $path, $content );
|
|
}
|
|
|
|
/**
|
|
* 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 sdk',
|
|
'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_sdk_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() {
|
|
$files = $this->get_files();
|
|
|
|
if ( ! $files ) {
|
|
// No files (or there's an error).
|
|
return false !== $files;
|
|
}
|
|
|
|
$error_paths = [];
|
|
$pattern = $this->escape_file_name( $this->filename );
|
|
$pattern = sprintf( $pattern, '(?<locale>[a-zA-Z_-]+)' );
|
|
|
|
foreach ( $files as $file ) {
|
|
preg_match( '/^' . $pattern . '$/', $file, $matches );
|
|
|
|
$remote_url = $this->get_url( $matches['locale'] );
|
|
|
|
if ( ! $this->refresh_save( $remote_url ) ) {
|
|
$error_paths[] = $this->get_busting_file_path( $matches['locale'] );
|
|
}
|
|
}
|
|
|
|
if ( $error_paths ) {
|
|
Logger::error(
|
|
'Local file(s) could not be updated.',
|
|
[
|
|
'fb sdk',
|
|
'paths' => $error_paths,
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Triggered once all local files have been updated (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 updated. An empty array if everything is fine.
|
|
*/
|
|
do_action( 'rocket_after_facebook_sdk_files_refresh', $files, $error_paths );
|
|
|
|
return ! $error_paths;
|
|
}
|
|
|
|
/**
|
|
* Delete all Facebook SDK busting files.
|
|
*
|
|
* @since 3.2
|
|
* @access public
|
|
* @author Grégory Viguier
|
|
*
|
|
* @return bool True on success. False on failure.
|
|
*/
|
|
public function delete() {
|
|
$filesystem = \rocket_direct_filesystem();
|
|
$files = $this->get_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 sdk',
|
|
'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_sdk_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_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 sdk',
|
|
'path' => $dir_path,
|
|
]
|
|
);
|
|
return false;
|
|
}
|
|
|
|
$dir = $filesystem->dirlist( $dir_path );
|
|
|
|
if ( false === $dir ) {
|
|
Logger::error(
|
|
'Could not get the directory contents.',
|
|
[
|
|
'fb sdk',
|
|
'path' => $dir_path,
|
|
]
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if ( ! $dir ) {
|
|
return [];
|
|
}
|
|
|
|
$list = [];
|
|
$pattern = $this->escape_file_name( $this->filename );
|
|
$pattern = sprintf( $pattern, '[a-zA-Z_-]+' );
|
|
|
|
foreach ( $dir as $entry ) {
|
|
if ( 'f' !== $entry['type'] ) {
|
|
continue;
|
|
}
|
|
if ( preg_match( '/^' . $pattern . '$/', $entry['name'], $matches ) ) {
|
|
$list[ $entry['name'] ] = $entry['name'];
|
|
}
|
|
}
|
|
|
|
return $list;
|
|
}
|
|
|
|
/** ----------------------------------------------------------------------------------------- */
|
|
/** REMOTE SDK FILE ========================================================================= */
|
|
/** ----------------------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Get the remote Facebook SDK URL.
|
|
*
|
|
* @since 3.2
|
|
* @access private
|
|
* @author Grégory Viguier
|
|
*
|
|
* @param string $locale A locale string, like 'en_US'.
|
|
* @return string
|
|
*/
|
|
public function get_url( $locale ) {
|
|
return sprintf( $this->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/(?<locale>[a-zA-Z_-]+)/sdk\.js@i';
|
|
|
|
if ( ! preg_match( $pattern, $url, $matches ) ) {
|
|
return false;
|
|
}
|
|
|
|
return $matches['locale'];
|
|
}
|
|
|
|
/**
|
|
* Extract XFBML from a URL to bust.
|
|
*
|
|
* @since 3.4.3
|
|
* @access private
|
|
* @author Soponar Cristina
|
|
*
|
|
* @param string $url Any string containing the URL to bust.
|
|
* @return string|bool The XFBML on success. False on failure.
|
|
*/
|
|
private function get_xfbml_from_url( $url ) {
|
|
$pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js#(?:.+&)?xfbml=(?<xfbml>[0-9]+)@i';
|
|
|
|
if ( ! preg_match( $pattern, $url, $matches ) ) {
|
|
return false;
|
|
}
|
|
|
|
return $matches['xfbml'];
|
|
}
|
|
|
|
/**
|
|
* Extract appId from a URL to bust.
|
|
*
|
|
* @since 3.4.3
|
|
* @access private
|
|
* @author Soponar Cristina
|
|
*
|
|
* @param string $url Any string containing the URL to bust.
|
|
* @return string|bool The appId on success. False on failure.
|
|
*/
|
|
private function get_appId_from_url( $url ) {
|
|
$pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js#(?:.+&)?appId=(?<appId>[0-9]+)@i';
|
|
|
|
if ( ! preg_match( $pattern, $url, $matches ) ) {
|
|
return false;
|
|
}
|
|
|
|
return $matches['appId'];
|
|
}
|
|
|
|
/**
|
|
* Extract version from a URL to bust.
|
|
*
|
|
* @since 3.4.3
|
|
* @access private
|
|
* @author Soponar Cristina
|
|
*
|
|
* @param string $url Any string containing the URL to bust.
|
|
* @return string|bool The version on success. False on failure.
|
|
*/
|
|
private function get_version_from_url( $url ) {
|
|
$pattern = '@//connect\.facebook\.net/(?<locale>[a-zA-Z_-]+)/sdk\.js#(?:.+&)?version=(?<version>[a-zA-Z0-9.]+)@i';
|
|
|
|
if ( ! preg_match( $pattern, $url, $matches ) ) {
|
|
return false;
|
|
}
|
|
|
|
return $matches['version'];
|
|
}
|
|
|
|
/** ----------------------------------------------------------------------------------------- */
|
|
/** BUSTING FILE ============================================================================ */
|
|
/** ----------------------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Get the local Facebook SDK URL.
|
|
*
|
|
* @since 3.2
|
|
* @access private
|
|
* @author Grégory Viguier
|
|
*
|
|
* @param string $locale A locale string, like 'en_US'.
|
|
* @return string
|
|
*/
|
|
private function get_busting_file_url( $locale ) {
|
|
$filename = $this->get_busting_file_name( $locale );
|
|
|
|
// This filter is documented in inc/functions/minify.php.
|
|
return apply_filters( 'rocket_js_url', apply_filters( 'rocket_facebook_sdk_url', $this->busting_url . $filename ) );
|
|
}
|
|
|
|
/**
|
|
* Get the local Facebook SDK file name.
|
|
*
|
|
* @since 3.2
|
|
* @access private
|
|
* @author Grégory Viguier
|
|
*
|
|
* @param string $locale A locale string, like 'en_US'.
|
|
* @return string
|
|
*/
|
|
private function get_busting_file_name( $locale ) {
|
|
return sprintf( $this->filename, $locale );
|
|
}
|
|
|
|
/**
|
|
* Get the local Facebook SDK file path.
|
|
*
|
|
* @since 3.2
|
|
* @access private
|
|
* @author Grégory Viguier
|
|
*
|
|
* @param string $locale A locale string, like 'en_US'.
|
|
* @return string
|
|
*/
|
|
private function get_busting_file_path( $locale ) {
|
|
return $this->busting_path . $this->get_busting_file_name( $locale );
|
|
}
|
|
|
|
/** ----------------------------------------------------------------------------------------- */
|
|
/** TOOLS =================================================================================== */
|
|
/** ----------------------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Get the contents of a URL.
|
|
*
|
|
* @since 3.2
|
|
* @access protected
|
|
* @author Grégory Viguier
|
|
*
|
|
* @param string $url The URL to request.
|
|
* @return string|bool The contents on success. False on failure.
|
|
*/
|
|
protected function get_file_content( $url ) {
|
|
try {
|
|
$response = wp_remote_get( $url );
|
|
} catch ( \Exception $e ) {
|
|
Logger::error(
|
|
'Remote file could not be fetched.',
|
|
[
|
|
'fb sdk',
|
|
'url' => $url,
|
|
'response' => $e->getMessage(),
|
|
]
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
Logger::error(
|
|
'Remote file could not be fetched.',
|
|
[
|
|
'fb sdk',
|
|
'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 sdk',
|
|
'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 );
|
|
}
|
|
}
|