Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ Please make sure you make a backup of your database before updating any version
= 2.6.3 =
* Improved: the tracker script now loads asynchronously (like Plausible Analytics' own snippet) instead of deferred, so it can no longer hold up the rest of the page when it can't be reached.
* Added: Hummingbird compatibility. Its Asset Optimization and Delay JavaScript no longer touch the plugin's scripts, which could cause a "plausible is not a function" error (e.g. on search results pages).
* Fixed: with the proxy enabled, the locally hosted tracker script kept loading from the site's previous location after moving the site to another host, domain or path. If the old server was offline, pages could hang while waiting for it. The script's location now follows the site automatically.
* Improved: when the locally hosted tracker script is missing (e.g. after moving the site without its uploads), it's loaded from Plausible Analytics until it's downloaded again, which now happens right away instead of on the next daily run.

= 2.6.2 =
* *Important!* This release rebuilds the WooCommerce (and EDD) purchase funnel for sites using WPML with a translated product base. If you built your own funnels using the same goals, please rebuild them after installing this update.
Expand Down
95 changes: 71 additions & 24 deletions src/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class Helpers {

const MULTILANG_PLUGIN_TRANSLATEPRESS = 'translatepress';

/**
* @var array|null The proxy resources as stored in the DB, retrieved once per request. @see self::get_proxy_resources()
*/
protected static $stored_proxy_resources;

/**
* Returns the language codes of all languages the active multilingual plugin serves to the public.
*
Expand Down Expand Up @@ -401,7 +406,7 @@ public static function get_proxy_resource( $resource_name = '' ) {
* Create the cache directory if it doesn't exist.
*/
if ( ( $resource_name === 'cache_dir' || $resource_name === 'cache_url' ) && ! is_dir( $resources['cache_dir'] ) ) {
wp_mkdir_p( $resources[ $resource_name ] );
wp_mkdir_p( $resources['cache_dir'] );
}

return $resources[ $resource_name ] ?? '';
Expand All @@ -410,40 +415,63 @@ public static function get_proxy_resource( $resource_name = '' ) {
/**
* Get (and generate/store if non-existent) proxy resources.
*
* @since 2.6.3 Only the cache directory's name is stored: its path and URL are derived from the current uploads
* directory on every request. They used to be stored as absolute values, so after moving the site to another
* host, domain or path the local tracker script was still loaded from the old location, which stalls every
* page (the script is deferred, so DOMContentLoaded waits for it) when the old server is offline.
*
* @return array
* @throws Exception
*
* @codeCoverageIgnore
*/
public static function get_proxy_resources() {
static $resources;
if ( static::$stored_proxy_resources === null ) {
static::$stored_proxy_resources = self::get_stored_proxy_resources();
}

$resources = static::$stored_proxy_resources;
$upload_dir = wp_get_upload_dir();

$resources['cache_dir'] = trailingslashit( $upload_dir['basedir'] ) . trailingslashit( $resources['cache_folder'] );
// set_url_scheme() matches the URL to the current request, e.g. after switching the site to SSL.
$resources['cache_url'] = trailingslashit( set_url_scheme( $upload_dir['baseurl'] ) ) . trailingslashit( $resources['cache_folder'] );

return $resources;
}

/**
* Returns the proxy resources stored in the DB, generating (and storing) them when there are none yet.
*
* @since 2.6.3
*
* @return array
* @throws Exception
*
* @codeCoverageIgnore
*/
private static function get_stored_proxy_resources() {
$stored = get_option( 'plausible_analytics_proxy_resources', [] );

if ( $resources === null ) {
$resources = get_option( 'plausible_analytics_proxy_resources', [] );
// Installs from before 2.6.3 stored the absolute path of the cache directory, of which only the name is kept.
if ( empty( $stored['cache_folder'] ) && ! empty( $stored['cache_dir'] ) ) {
$stored['cache_folder'] = basename( untrailingslashit( $stored['cache_dir'] ) );
}

/**
* Force a refresh of our resources if the user recently switched to SSL and we still have non-SSL resources stored.
*/
if ( ! empty( $resources ) && is_ssl() && isset( $resources['cache_url'] ) && ( strpos( $resources['cache_url'], 'http:' ) !== false ) ) {
$resources = [];
if ( ! empty( $stored['namespace'] ) && ! empty( $stored['cache_folder'] ) ) {
return $stored;
}

if ( empty( $resources ) ) {
$cache_dir = bin2hex( random_bytes( 5 ) );
$upload_dir = wp_get_upload_dir();
$resources = [
'namespace' => bin2hex( random_bytes( 3 ) ),
'base' => bin2hex( random_bytes( 2 ) ),
'endpoint' => bin2hex( random_bytes( 4 ) ),
'cache_dir' => trailingslashit( $upload_dir['basedir'] ) . trailingslashit( $cache_dir ),
'cache_url' => trailingslashit( $upload_dir['baseurl'] ) . trailingslashit( $cache_dir ),
];
$stored = [
'namespace' => bin2hex( random_bytes( 3 ) ),
'base' => bin2hex( random_bytes( 2 ) ),
'endpoint' => bin2hex( random_bytes( 4 ) ),
'cache_folder' => bin2hex( random_bytes( 5 ) ),
];

update_option( 'plausible_analytics_proxy_resources', $resources );
}
update_option( 'plausible_analytics_proxy_resources', $stored );

return $resources;
return $stored;
}

/**
Expand Down Expand Up @@ -538,8 +566,27 @@ public static function get_js_url( $local = false ) {
*/
if ( $local && static::proxy_enabled() ) {
/**
* The cache URL is stored as an absolute URL on the main domain, so it needs to be moved to the
* language domain we're currently on.
* The local file may not exist (yet), e.g. right after moving the site to another host without its uploads.
* Load the script from Plausible Analytics until the cron has downloaded it, instead of from a URL that 404s.
*/
if ( ! file_exists( static::get_proxy_resource( 'cache_dir' ) . $file_name . '.js' ) ) {
/**
* Download it right away, rather than on the next daily run. At most once per 15 minutes: if the
* download keeps failing (e.g. the uploads directory isn't writable), every request would schedule a
* new attempt as soon as the previous one has run. wp_next_scheduled() can't be used for this, as the
* daily event uses the same hook.
*/
if ( ! get_transient( 'plausible_analytics_js_download_attempt' ) ) {
set_transient( 'plausible_analytics_js_download_attempt', 1, 15 * MINUTE_IN_SECONDS );
wp_schedule_single_event( time(), Cron::TASK_NAME );
}

return esc_url( static::get_hosted_domain_url() . "/js/$file_name.js" );
}

/**
* The cache URL is built on the main domain, so it needs to be moved to the language domain we're currently
* on.
*/
return esc_url( static::maybe_use_current_language_domain( static::get_proxy_resource( 'cache_url' ) . $file_name . '.js' ) );
}
Expand Down
60 changes: 60 additions & 0 deletions tests/integration/HelpersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Exception;
use Plausible\Analytics\Tests\TestableHelpers;
use Plausible\Analytics\Tests\TestCase;
use Plausible\Analytics\WP\Cron;
use Plausible\Analytics\WP\Helpers;
use function Brain\Monkey\Functions\when;

Expand Down Expand Up @@ -237,11 +238,32 @@ public function testGetJsUrl() {
try {
add_filter( 'plausible_analytics_settings', [ $this, 'enableProxy' ] );

$local_file = TestableHelpers::get_js_path();

// Until the cron has downloaded the local file, the script is loaded from Plausible Analytics.
wp_delete_file( $local_file );
wp_clear_scheduled_hook( Cron::TASK_NAME );
delete_transient( 'plausible_analytics_js_download_attempt' );

$this->assertEquals( 'https://plausible.io/js/pa-test-tracker-id.js', TestableHelpers::get_js_url( true ) );
$this->assertNotFalse( wp_next_scheduled( Cron::TASK_NAME ) );

// Once that attempt has run (and failed), no new one is scheduled until the backoff has expired.
wp_clear_scheduled_hook( Cron::TASK_NAME );

$this->assertEquals( 'https://plausible.io/js/pa-test-tracker-id.js', TestableHelpers::get_js_url( true ) );
$this->assertFalse( wp_next_scheduled( Cron::TASK_NAME ) );

file_put_contents( $local_file, '// test' );

$url = TestableHelpers::get_js_url( true );

$this->assertMatchesRegularExpression( '~http://example.org/wp-content/uploads/.*?/.*?.js~', $url );
} finally {
remove_filter( 'plausible_analytics_settings', [ $this, 'enableProxy' ] );
wp_delete_file( $local_file ?? '' );
wp_clear_scheduled_hook( Cron::TASK_NAME );
delete_transient( 'plausible_analytics_js_download_attempt' );
}

try {
Expand Down Expand Up @@ -269,6 +291,44 @@ public function testGetPostSettings() {
$this->assertArrayNotHasKey( 'post_test', $settings );
}

/**
* After moving the site to another host, domain or path, the cache directory's path and URL should follow the
* current uploads directory, not the (absolute) ones stored before 2.6.3.
*
* @see Helpers::get_proxy_resources()
* @return void
* @throws Exception
*/
public function testGetProxyResourcesAfterMovingTheSite() {
$stored = new \ReflectionProperty( Helpers::class, 'stored_proxy_resources' );
$stored->setAccessible( true );
$backup = get_option( 'plausible_analytics_proxy_resources' );

update_option(
'plausible_analytics_proxy_resources',
[
'namespace' => 'abcdef',
'base' => 'abcd',
'endpoint' => 'abcdefgh',
'cache_dir' => '/home/old-host/public_html/wp-content/uploads/0123456789/',
'cache_url' => 'https://old-host.example/wp-content/uploads/0123456789/',
]
);
$stored->setValue( null, null );

try {
$upload_dir = wp_get_upload_dir();

$this->assertEquals( trailingslashit( $upload_dir['basedir'] ) . '0123456789/', Helpers::get_proxy_resource( 'cache_dir' ) );
$this->assertEquals( trailingslashit( $upload_dir['baseurl'] ) . '0123456789/', Helpers::get_proxy_resource( 'cache_url' ) );
// The REST route stays the same.
$this->assertEquals( 'abcdef', Helpers::get_proxy_resource( 'namespace' ) );
} finally {
update_option( 'plausible_analytics_proxy_resources', $backup );
$stored->setValue( null, null );
}
}

/**
* @see Helpers::get_proxy_resource()
* @return void
Expand Down
Loading