| 1 |
<?php |
| 2 |
/** |
| 3 |
* Plausible Analytics | Module. |
| 4 |
* |
| 5 |
* @since 1.3.0 |
| 6 |
* @package WordPress |
| 7 |
* @subpackage Plausible Analytics |
| 8 |
*/ |
| 9 |
|
| 10 |
namespace Plausible\Analytics\WP\Admin; |
| 11 |
|
| 12 |
use Exception; |
| 13 |
use Plausible\Analytics\WP\Helpers; |
| 14 |
use Plausible\Analytics\WP\Proxy; |
| 15 |
|
| 16 |
class Module { |
| 17 |
/** |
| 18 |
* Build properties. |
| 19 |
* |
| 20 |
* @return void |
| 21 |
*/ |
| 22 |
public function __construct() { |
| 23 |
$this->init(); |
| 24 |
} |
| 25 |
|
| 26 |
/** |
| 27 |
* Filters & Actions. |
| 28 |
* |
| 29 |
* @return void |
| 30 |
*/ |
| 31 |
private function init() { |
| 32 |
add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_install_module' ], 9, 2 ); |
| 33 |
add_filter( 'pre_update_option_plausible_analytics_settings', [ $this, 'maybe_enable_proxy' ], 10, 2 ); |
| 34 |
} |
| 35 |
|
| 36 |
/** |
| 37 |
* Decide whether we should install the module, or not. |
| 38 |
* |
| 39 |
* @param array $settings Current settings, already written to the DB. |
| 40 |
* |
| 41 |
* @return void |
| 42 |
* @since 1.3.0 |
| 43 |
* |
| 44 |
*/ |
| 45 |
public function maybe_install_module( $old_settings, $settings ) { |
| 46 |
$settings_proxy = ( is_array( $settings ) && isset( $settings['proxy_enabled'] ) ) ? $settings['proxy_enabled'] : ''; |
| 47 |
$old_proxy = ( is_array( $old_settings ) && isset( $old_settings['proxy_enabled'] ) ) ? $old_settings['proxy_enabled'] : ''; |
| 48 |
|
| 49 |
if ( $settings_proxy === 'on' && $old_proxy !== 'on' ) { |
| 50 |
$this->install(); |
| 51 |
} elseif ( $settings_proxy === '' && $old_proxy === 'on' ) { |
| 52 |
$this->uninstall(); |
| 53 |
} |
| 54 |
} |
| 55 |
|
| 56 |
/** |
| 57 |
* Takes care of installing the M(ust)U(se) plugin when the Proxy is enabled. |
| 58 |
* |
| 59 |
* @return void. |
| 60 |
* @since 1.3.0 |
| 61 |
*/ |
| 62 |
public function install() { |
| 63 |
if ( ! current_user_can( 'install_plugins' ) ) { |
| 64 |
return; // @codeCoverageIgnore |
| 65 |
} |
| 66 |
|
| 67 |
if ( ! function_exists( 'WP_Filesystem' ) ) { |
| 68 |
require_once( ABSPATH . 'wp-admin/includes/file.php' ); // @codeCoverageIgnore |
| 69 |
} |
| 70 |
|
| 71 |
WP_Filesystem(); |
| 72 |
|
| 73 |
if ( ! is_dir( WPMU_PLUGIN_DIR ) ) { |
| 74 |
wp_mkdir_p( WPMU_PLUGIN_DIR ); |
| 75 |
add_option( 'plausible_analytics_created_mu_plugins_dir', true ); |
| 76 |
} |
| 77 |
|
| 78 |
if ( ! is_dir( WPMU_PLUGIN_DIR ) ) { |
| 79 |
$this->show_module_not_installed_error(); // @codeCoverageIgnore |
| 80 |
} |
| 81 |
|
| 82 |
$results = copy_dir( PLAUSIBLE_ANALYTICS_PLUGIN_DIR . 'mu-plugin', WPMU_PLUGIN_DIR ); |
| 83 |
|
| 84 |
if ( is_wp_error( $results ) ) { |
| 85 |
$this->show_module_not_installed_error(); // @codeCoverageIgnore |
| 86 |
} |
| 87 |
|
| 88 |
add_option( 'plausible_analytics_proxy_speed_module_installed', true ); |
| 89 |
} |
| 90 |
|
| 91 |
/** |
| 92 |
* @return void |
| 93 |
* |
| 94 |
* @codeCoverageIgnore |
| 95 |
* @since 1.3.0 |
| 96 |
*/ |
| 97 |
private function show_module_not_installed_error() { |
| 98 |
$message = sprintf( |
| 99 |
wp_kses( |
| 100 |
// translators: %s: URL to manual proxy speed module installation instructions. |
| 101 |
__( |
| 102 |
'The proxy is enabled, but the proxy\'s speed module failed to install. Try <a href="%s" target="_blank">installing it manually</a>.', |
| 103 |
'plausible-analytics' |
| 104 |
), |
| 105 |
'post' |
| 106 |
), |
| 107 |
'https://plausible.io/docs/troubleshoot-integration#proxy-script-is-slow' |
| 108 |
); |
| 109 |
|
| 110 |
Messages::set_error( $message ); |
| 111 |
} |
| 112 |
|
| 113 |
/** |
| 114 |
* Uninstall the Speed Module, generates JS files and all related settings when the proxy is disabled. |
| 115 |
* |
| 116 |
* @return void. |
| 117 |
* @throws Exception |
| 118 |
* @since 1.3.0 |
| 119 |
*/ |
| 120 |
public function uninstall() { |
| 121 |
if ( ! current_user_can( 'install_plugins' ) ) { |
| 122 |
return; // @codeCoverageIgnore |
| 123 |
} |
| 124 |
|
| 125 |
/** |
| 126 |
* Clean up MU plugin. |
| 127 |
*/ |
| 128 |
$file_path = WP_CONTENT_DIR . '/mu-plugins/plausible-proxy-speed-module.php'; |
| 129 |
|
| 130 |
if ( file_exists( $file_path ) ) { |
| 131 |
wp_delete_file( $file_path ); |
| 132 |
} |
| 133 |
|
| 134 |
if ( get_option( 'plausible_analytics_created_mu_plugins_dir' ) && $this->dir_is_empty( WPMU_PLUGIN_DIR ) ) { |
| 135 |
rmdir( WPMU_PLUGIN_DIR ); |
| 136 |
} |
| 137 |
|
| 138 |
/** |
| 139 |
* Clean up generated JS files in /uploads dir. |
| 140 |
*/ |
| 141 |
$cache_dir = Helpers::get_proxy_resource( 'cache_dir' ); |
| 142 |
$js_file = $this->get_filename(); |
| 143 |
|
| 144 |
if ( file_exists( $cache_dir . $js_file . '.js' ) ) { |
| 145 |
wp_delete_file( $cache_dir . $js_file . '.js' ); // @codeCoverageIgnore |
| 146 |
} |
| 147 |
|
| 148 |
if ( $this->dir_is_empty( $cache_dir ) ) { |
| 149 |
rmdir( $cache_dir ); |
| 150 |
} |
| 151 |
|
| 152 |
/** |
| 153 |
* Clean up related DB entries. |
| 154 |
*/ |
| 155 |
delete_option( 'plausible_analytics_created_mu_plugins_dir' ); |
| 156 |
delete_option( 'plausible_analytics_proxy_speed_module_installed' ); |
| 157 |
delete_option( 'plausible_analytics_proxy_resources' ); |
| 158 |
} |
| 159 |
|
| 160 |
/** |
| 161 |
* Check if a directory is empty. |
| 162 |
* This works because a new FilesystemIterator will initially point to the first file in the folder - |
| 163 |
* if there are no files in the folder, valid() will return false. |
| 164 |
* |
| 165 |
* @see https://www.php.net/manual/en/directoryiterator.valid.php |
| 166 |
* @since 1.3.0 |
| 167 |
* |
| 168 |
* @param mixed $dir |
| 169 |
* |
| 170 |
* @return bool |
| 171 |
* |
| 172 |
* @codeCoverageIgnore Because we don't want to test the filesystem. |
| 173 |
*/ |
| 174 |
protected function dir_is_empty( $dir ) { |
| 175 |
$iterator = new \FilesystemIterator( $dir ); |
| 176 |
|
| 177 |
return ! $iterator->valid(); |
| 178 |
} |
| 179 |
|
| 180 |
/** |
| 181 |
* @throws Exception |
| 182 |
* |
| 183 |
* @codeCoverageIgnore because Helpers are tested elsewhere. |
| 184 |
*/ |
| 185 |
protected function get_filename() { |
| 186 |
return Helpers::get_filename(); |
| 187 |
} |
| 188 |
|
| 189 |
/** |
| 190 |
* Test the proxy before enabling the option. |
| 191 |
* |
| 192 |
* @param mixed $settings |
| 193 |
* |
| 194 |
* @return mixed |
| 195 |
* @throws Exception |
| 196 |
* @since 1.3.0 |
| 197 |
* |
| 198 |
*/ |
| 199 |
public function maybe_enable_proxy( $settings, $old_settings ) { |
| 200 |
/** |
| 201 |
* No need to run this on each update run, or when the proxy is disabled. |
| 202 |
*/ |
| 203 |
$new_proxy_setting = ( is_array( $settings ) && isset( $settings['proxy_enabled'] ) ) ? $settings['proxy_enabled'] : ''; |
| 204 |
$old_proxy_setting = ( is_array( $old_settings ) && isset( $old_settings['proxy_enabled'] ) ) ? $old_settings['proxy_enabled'] : ''; |
| 205 |
|
| 206 |
if ( empty( $new_proxy_setting ) || ( $new_proxy_setting === 'on' && $old_proxy_setting === 'on' ) ) { |
| 207 |
return $settings; |
| 208 |
} |
| 209 |
|
| 210 |
$is_ssl = $this->is_ssl(); |
| 211 |
|
| 212 |
if ( ! $is_ssl ) { |
| 213 |
Messages::set_notice( |
| 214 |
sprintf( |
| 215 |
// translators: %s: URL to list of potential proxy solutions. |
| 216 |
__( |
| 217 |
'Please check that your proxy is functioning correctly. If you encounter any issues with tracking, <a href="%s" target="_blank">click here</a> for a list of potential solutions', |
| 218 |
'plausible-analytics' |
| 219 |
), |
| 220 |
'https://plausible.io/docs/troubleshoot-integration#proxy-not-recording-stats-after-setup' |
| 221 |
) |
| 222 |
); |
| 223 |
} |
| 224 |
|
| 225 |
$test_succeeded = $this->test_proxy( Helpers::proxy_enabled( $settings ) && wp_doing_ajax() ); |
| 226 |
|
| 227 |
if ( ! $test_succeeded ) { |
| 228 |
// @codeCoverageIgnoreStart |
| 229 |
Messages::set_error( |
| 230 |
sprintf( |
| 231 |
wp_kses( |
| 232 |
// translators: 1: Proxy endpoint URL, 2: URL to Plausible support. |
| 233 |
__( |
| 234 |
'Plausible\'s proxy couldn\'t be enabled, because the WordPress API is inaccessible. This might be due to a conflicting setting in a (security) plugin or server firewall. Make sure you whitelist requests to the Proxy\'s endpoint: <code>%1$s</code>. <a href="%2$s" target="_blank">Contact support</a> if you need help locating the issue.', |
| 235 |
'plausible-analytics' |
| 236 |
), |
| 237 |
'post' |
| 238 |
), |
| 239 |
Helpers::get_rest_endpoint( false ), |
| 240 |
'https://plausible.io/contact' |
| 241 |
) |
| 242 |
); |
| 243 |
|
| 244 |
// Disable the proxy. |
| 245 |
return $old_settings; |
| 246 |
// @codeCoverageIgnoreEnd |
| 247 |
} |
| 248 |
|
| 249 |
return $settings; |
| 250 |
} |
| 251 |
|
| 252 |
/** |
| 253 |
* is_ssl() only checks the current scheme that is used, which fails in a Nginx Reverse Proxy configuration (where the scheme is HTTP behind the |
| 254 |
* proxy), this function is a custom wrapper which also checks the WordPress configuration for the presence of "https" in the configured Home |
| 255 |
* URL. |
| 256 |
* |
| 257 |
* @return bool |
| 258 |
*/ |
| 259 |
private function is_ssl() { |
| 260 |
return strpos( get_home_url(), 'https' ) !== false || is_ssl(); |
| 261 |
} |
| 262 |
|
| 263 |
/** |
| 264 |
* Runs a quick internal call to the WordPress API to make sure it's accessible. |
| 265 |
* |
| 266 |
* @return bool |
| 267 |
* @throws Exception |
| 268 |
* @since 1.3.0 |
| 269 |
*/ |
| 270 |
private function test_proxy( $run = true ) { |
| 271 |
// Always succeed if this is a CI environment. |
| 272 |
if ( defined( 'PLAUSIBLE_CI' ) ) { |
| 273 |
return true; |
| 274 |
} |
| 275 |
|
| 276 |
// Should we run the test? |
| 277 |
if ( ! apply_filters( 'plausible_analytics_module_run_test_proxy', $run ) ) { |
| 278 |
return false; // @codeCoverageIgnore |
| 279 |
} |
| 280 |
|
| 281 |
$proxy = new Proxy( false ); |
| 282 |
$result = $proxy->do_request( 'pageview', 'plausible.test', 'https://plausible.test/test' ); |
| 283 |
|
| 284 |
return wp_remote_retrieve_response_code( $result ) === 202; |
| 285 |
} |
| 286 |
} |
| 287 |
|