PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.0.7
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.0.7
5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / classes / WpMatomo / ScheduledTasks.php
matomo / classes / WpMatomo Last commit date
Admin 2 years ago Commands 2 years ago Db 2 years ago Ecommerce 2 years ago Report 2 years ago Site 2 years ago TrackingCode 2 years ago Updater 4 years ago User 2 years ago Workarounds 2 years ago WpStatistics 2 years ago views 4 years ago API.php 2 years ago Access.php 4 years ago AjaxTracker.php 2 years ago Annotations.php 4 years ago Bootstrap.php 2 years ago Capabilities.php 4 years ago Compatibility.php 2 years ago Email.php 2 years ago ErrorNotice.php 2 years ago Installer.php 2 years ago Logger.php 2 years ago OptOut.php 4 years ago Paths.php 2 years ago PluginAdminOverrides.php 2 years ago PrivacyBadge.php 4 years ago RedirectOnActivation.php 4 years ago Referral.php 4 years ago Roles.php 4 years ago ScheduledTasks.php 2 years ago Settings.php 2 years ago Site.php 3 years ago TrackingCode.php 4 years ago Uninstaller.php 2 years ago Updater.php 2 years ago User.php 4 years ago
ScheduledTasks.php
505 lines
1 <?php
2 /**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 * @package matomo
8 */
9
10 namespace WpMatomo;
11
12 use Exception;
13 use Piwik\Container\StaticContainer;
14 use Piwik\CronArchive;
15 use Piwik\Filesystem;
16 use Piwik\Option;
17 use Piwik\Plugin\Manager;
18 use Piwik\Plugins\GeoIp2\GeoIP2AutoUpdater;
19 use Piwik\Plugins\GeoIp2\LocationProvider\GeoIp2;
20 use Piwik\Plugins\GeoIp2\LocationProvider\GeoIp2\Php;
21 use Piwik\Plugins\UserCountry\LocationProvider;
22 use WpMatomo\Admin\Admin;
23 use WpMatomo\Site\Sync as SiteSync;
24 use WpMatomo\User\Sync as UserSync;
25 use WpMatomo\Paths;
26
27 if ( ! defined( 'ABSPATH' ) ) {
28 exit; // if accessed directly
29 }
30
31 class ScheduledTasks {
32 const EVENT_SYNC = 'matomo_scheduled_sync';
33 const EVENT_DISABLE_ADDHANDLER = 'matomo_scheduled_disable_addhandler';
34 const EVENT_ARCHIVE = 'matomo_scheduled_archive';
35 const EVENT_GEOIP = 'matomo_scheduled_geoipdb';
36 const EVENT_UPDATE = 'matomo_update_core';
37
38 const KEY_BEFORE_CRON = 'before-cron-';
39 const KEY_AFTER_CRON = 'after-cron-';
40
41 const FAILURES_LIST_OPTION = Settings::OPTION_PREFIX . 'scheduled-task-failures';
42
43 /**
44 * @var Settings
45 */
46 private $settings;
47
48 /**
49 * @var Logger
50 */
51 private $logger;
52
53 public function __construct( Settings $settings ) {
54 $this->settings = $settings;
55 $this->logger = new Logger();
56 }
57
58 public function add_monthly_schedule( $schedules ) {
59 $schedules['matomo_monthly'] = [
60 'interval' => 60 * 60 * 24 * 30,
61 'display' => __( 'Monthly', 'matomo' ),
62 ];
63
64 return $schedules;
65 }
66
67 public function schedule() {
68 add_action( self::EVENT_UPDATE, [ $this, 'perform_update' ] );
69 add_filter( 'cron_schedules', [ $this, 'add_monthly_schedule' ] );
70
71 $self = $this;
72 $event_priority = 10;
73
74 $installer = new Installer( $this->settings );
75 $looks_installed = $installer->looks_like_it_is_installed(); // we only schedule events when Matomo looks installed but we still listen to the actions in case the app triggers a one time update.
76
77 foreach ( $this->get_all_events() as $event_name => $event_config ) {
78 if ( $looks_installed && wp_next_scheduled( $event_name ) === false ) {
79 wp_schedule_event( time(), $event_config['interval'], $event_name );
80 }
81
82 // logging last execution start time
83 add_action(
84 $event_name,
85 function () use ( $self, $event_name ) {
86 $self->set_last_time_before_cron( $event_name, time() );
87 },
88 $event_priority / 2,
89 $accepted_args = 0
90 );
91
92 // actual event
93 add_action( $event_name, [ $this, $event_config['method'] ], $event_priority, $accepted_args = 0 );
94
95 // logging last execution end time
96 add_action(
97 $event_name,
98 function () use ( $self, $event_name ) {
99 $self->set_last_time_after_cron( $event_name, time() );
100 },
101 $event_priority * 2,
102 $accepted_args = 0
103 );
104 }
105
106 register_deactivation_hook( MATOMO_ANALYTICS_FILE, [ $this, 'uninstall' ] );
107 }
108
109 public function get_last_time_before_cron( $event_name ) {
110 return get_option( Settings::OPTION_PREFIX . self::KEY_BEFORE_CRON . $event_name );
111 }
112
113 public function set_last_time_before_cron( $event_name, $time ) {
114 // we use settings prefix so data automatically gets removed when uninstalling
115 update_option( Settings::OPTION_PREFIX . self::KEY_BEFORE_CRON . $event_name, $time );
116 }
117
118 public function get_last_time_after_cron( $event_name ) {
119 return get_option( Settings::OPTION_PREFIX . self::KEY_AFTER_CRON . $event_name );
120 }
121
122 public function set_last_time_after_cron( $event_name, $time ) {
123 // we use settings prefix so data automatically gets removed when uninstalling
124 update_option( Settings::OPTION_PREFIX . self::KEY_AFTER_CRON . $event_name, $time );
125 }
126
127 public function get_all_events() {
128 $events = [
129 self::EVENT_SYNC => [
130 'name' => 'Sync users & sites',
131 'interval' => 'daily',
132 'method' => 'sync',
133 ],
134 self::EVENT_ARCHIVE => [
135 'name' => 'Archive',
136 'interval' => 'hourly',
137 'method' => 'archive',
138 ],
139 self::EVENT_GEOIP => [
140 'name' => 'Update GeoIP DB',
141 'interval' => 'matomo_monthly',
142 'method' => 'update_geo_ip2_db',
143 ],
144 ];
145 if ( $this->settings->should_disable_addhandler() ) {
146 $events[ self::EVENT_DISABLE_ADDHANDLER ] = [
147 'name' => 'Disable AddHandler',
148 'interval' => 'hourly',
149 'method' => 'disable_add_handler',
150 ];
151 }
152
153 return $events;
154 }
155
156 public function disable_add_handler( $force_undo = false ) {
157 $this->remove_task_errors( [ 'disable_addhandler' ] );
158
159 $disable_addhandler = $this->settings->should_disable_addhandler();
160 if ( $disable_addhandler ) {
161 $this->logger->log( 'Scheduled tasks disabling addhandler' );
162 try {
163 Bootstrap::do_bootstrap();
164
165 $files = Filesystem::globr( dirname( MATOMO_ANALYTICS_FILE ), '.htaccess' );
166 foreach ( $files as $file ) {
167 if ( is_readable( $file ) ) {
168 // we don't need to access remote files
169 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
170 $content = file_get_contents( $file );
171 $search = 'AddHandler';
172 $replace = '# AddHandler';
173 if ( $force_undo ) {
174 $search = '# AddHandler';
175 $replace = 'AddHandler';
176 }
177 if ( strpos( $content, $search ) !== false && ( $force_undo || strpos( $content, $replace ) === false ) ) {
178 if ( is_writeable( $file ) ) {
179 $content = str_replace( $search, $replace, $content );
180 $paths = new Paths();
181 $wp_filesystem = $paths->get_file_system();
182 $wp_filesystem->put_contents( $file, $content );
183 } else {
184 $this->logger->log( 'Cannot update file as not writable ' . $file );
185 }
186 }
187 }
188 }
189 } catch ( Exception $e ) {
190 $this->on_task_fail( 'disable_addhandler', $e, 'An error occurred when trying to disable AddHandler in apache config.' );
191 }
192 }
193 }
194
195 private function check_try_update() {
196 try {
197 $installer = new Installer( $this->settings );
198 if ( $installer->looks_like_it_is_installed() ) {
199 $updater = new Updater( $this->settings );
200 $updater->update_if_needed();
201 }
202 } catch ( Exception $e ) {
203 // we don't want to rethrow exception otherwise some other blogs might never sync
204 $this->logger->log_exception( 'check_try_update', $e );
205 }
206 }
207
208 public function perform_update() {
209 $this->remove_task_errors( [ 'cron_update' ] );
210
211 $this->logger->log( 'Scheduled tasks perform update' );
212
213 try {
214 $updater = new Updater( $this->settings );
215 $updater->update();
216 } catch ( Exception $e ) {
217 $this->on_task_fail( 'cron_update', $e, 'An error occurred when upgrading the Matomo database tables.' );
218 return false;
219 }
220
221 return true;
222 }
223
224 public function update_geo_ip2_db() {
225 $this->remove_task_errors( [ 'update_geoip2' ] );
226
227 $this->logger->log( 'Scheduled tasks update geoip database' );
228 try {
229 Bootstrap::do_bootstrap();
230
231 $maxmind_license = $this->settings->get_global_option( 'maxmind_license_key' );
232 if ( empty( $maxmind_license ) ) {
233 $db_url = GeoIp2::getDbIpLiteUrl();
234 $asn_url = GeoIp2::getDbIpLiteUrl( 'asn' );
235 } else {
236 $db_url = 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz&license_key=' . $maxmind_license;
237 $asn_url = 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&suffix=tar.gz&license_key=' . $maxmind_license;
238 }
239
240 Option::set( GeoIP2AutoUpdater::LOC_URL_OPTION_NAME, $db_url );
241
242 if ( Manager::getInstance()->isPluginActivated( 'Provider' ) ) {
243 Option::set( GeoIP2AutoUpdater::ISP_URL_OPTION_NAME, $asn_url );
244 } else {
245 Option::delete( GeoIP2AutoUpdater::ISP_URL_OPTION_NAME );
246 }
247
248 $updater = StaticContainer::get( GeoIP2AutoUpdater::class );
249 $updater->update();
250 if ( LocationProvider::getCurrentProviderId() !== Php::ID && LocationProvider::getProviderById( Php::ID ) ) {
251 LocationProvider::setCurrentProvider( Php::ID );
252 }
253 } catch ( Exception $e ) {
254 $next = wp_next_scheduled( self::EVENT_GEOIP );
255 if ( false === $next || $next - time() > 2 * 24 * 60 * 60 ) {
256 wp_schedule_single_event( time() + 24 * 60 * 60, self::EVENT_GEOIP );
257 }
258
259 $this->on_task_fail( 'update_geoip2', $e, 'An error occurred while updating the geolocation database.' );
260 }
261 }
262
263 public function sync() {
264 $this->remove_task_errors( [ 'matomo_url_sync', 'cron_sync' ] );
265
266 $this->check_try_update();
267
268 $this->logger->log( 'Scheduled tasks sync all sites and users' );
269
270 try {
271 // we update the matomo url if needed/when possible. eg an update may be needed when site_url changes
272 $installer = new Installer( $this->settings );
273 if ( $installer->looks_like_it_is_installed() ) {
274 Bootstrap::do_bootstrap();
275 $installer->set_matomo_url();
276 }
277 } catch ( Exception $e ) {
278 $this->on_task_fail( 'matomo_url_sync', $e, 'An error occurred when syncing the WordPress site URL with Matomo.' );
279 }
280
281 try {
282 $site = new SiteSync( $this->settings );
283 $site->sync_all();
284 $user = new UserSync();
285 $user->sync_all();
286 } catch ( Exception $e ) {
287 $this->on_task_fail( 'cron_sync', $e, 'An error occurred when syncing WordPress sites and users with Matomo.' );
288 }
289 }
290
291 public function archive( $force = false, $throw_exception = true ) {
292 $this->check_try_update();
293
294 if ( defined( 'MATOMO_DISABLE_WP_ARCHIVING' ) && MATOMO_DISABLE_WP_ARCHIVING ) {
295 return;
296 }
297
298 $this->remove_task_errors( [ 'archive_bootstrap', 'archive_main' ] );
299
300 // exceptions should not be rethrown as they will prevent other cron tasks
301 // from running (wp-cron.php does not handle exceptions). we only want exceptions
302 // when running tests.
303 $should_rethrow_exception = ( defined( 'MATOMO_PHPUNIT_TEST' ) && MATOMO_PHPUNIT_TEST );
304
305 $this->logger->log( 'Scheduled tasks archive data' );
306
307 try {
308 Bootstrap::do_bootstrap();
309 } catch ( Exception $e ) {
310 if ( $should_rethrow_exception || $force ) {
311 $this->logger->log_exception( 'archive_bootstrap', $e );
312
313 // we want to trigger an exception if it was forced from the UI
314 throw $e;
315 }
316
317 $this->on_task_fail( 'archive_bootstrap', $e, 'An error occurred during Matomo archiving.' );
318 return;
319 }
320
321 $archiver = new CronArchive();
322 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
323 $archiver->concurrentRequestsPerWebsite = 1;
324 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
325 $archiver->maxConcurrentArchivers = 1;
326
327 // see https://github.com/matomo-org/matomo/pull/21216
328 if ( ! CronArchive\SharedSiteIds::isSupported() ) {
329 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
330 $archiver->shouldArchiveAllSites = true;
331 }
332
333 if ( $force ) {
334 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
335 $archiver->shouldArchiveAllSites = true;
336 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
337 $archiver->disableScheduledTasks = true;
338 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
339 if ( ! defined( 'PIWIK_ARCHIVE_NO_TRUNCATE' ) ) {
340 define( 'PIWIK_ARCHIVE_NO_TRUNCATE', true );
341 }
342 }
343
344 if ( is_multisite() ) {
345 if ( is_network_admin() ) {
346 return; // nothing to archive
347 } else {
348 $blog_id = get_current_blog_id();
349 $idsite = Site::get_matomo_site_id( $blog_id );
350 if ( ! empty( $idsite ) ) {
351 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
352 $archiver->shouldArchiveSpecifiedSites = [ $idsite ];
353 } else {
354 // there is no site mapped to it so there's no point in archiving it
355 return;
356 }
357 }
358 }
359
360 try {
361 $archiver->main();
362
363 $archive_errors = $archiver->getErrors();
364 } catch ( Exception $e ) {
365 $this->on_task_fail( 'archive_main', $e, 'An error occurred during Matomo archiving.' );
366
367 $archive_errors = $archiver->getErrors();
368
369 if ( ! empty( $archive_errors ) ) {
370 $message = '';
371 foreach ( $archiver->getErrors() as $error ) {
372 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
373 $message .= var_export( $error, 1 ) . ' ';
374 }
375 $message = new Exception( trim( $message ) );
376 $this->logger->log_exception( 'archive_errors', $message );
377 }
378
379 if ( $throw_exception ) {
380 if ( $should_rethrow_exception ) {
381 throw $e;
382 }
383 // we otherwise only log the error but don't throw an exception
384 } else {
385 $archive_errors[] = $e->getMessage();
386 }
387 }
388
389 return $archive_errors;
390 }
391
392 public function uninstall() {
393 $this->logger->log( 'Scheduled tasks uninstall all events' );
394
395 foreach ( $this->get_all_events() as $event_name => $config ) {
396 $timestamp = wp_next_scheduled( $event_name );
397 if ( $timestamp ) {
398 wp_unschedule_event( $timestamp, $event_name );
399 }
400 }
401 foreach ( $this->get_all_events() as $event_name => $config ) {
402 wp_clear_scheduled_hook( $event_name );
403 }
404 }
405
406 private function on_task_fail( $log_key, Exception $exception, $user_error_message ) {
407 $this->logger->log_exception( $log_key, $exception, 'Matomo error - ' . $user_error_message . ' Details: ' );
408
409 $failures = $this->get_recorded_task_failures();
410
411 $failures[ $log_key ] = $user_error_message;
412
413 update_option( self::FAILURES_LIST_OPTION, $failures );
414 }
415
416 private function remove_task_errors( $log_keys ) {
417 // remove any failures from the list when the task starts again
418 $failures = $this->get_recorded_task_failures();
419 if ( empty( $failures ) ) {
420 return;
421 }
422
423 foreach ( $log_keys as $log_key ) {
424 unset( $failures[ $log_key ] );
425 }
426
427 update_option( self::FAILURES_LIST_OPTION, $failures );
428 }
429
430 public function get_recorded_task_failures() {
431 $failures = get_option( self::FAILURES_LIST_OPTION );
432 if ( ! is_array( $failures ) ) {
433 $failures = [];
434 }
435 return $failures;
436 }
437
438 public function show_errors_if_admin() {
439 if ( ! is_admin()
440 || ! Admin::is_matomo_admin()
441 ) {
442 return;
443 }
444
445 $matomo_task_failures = $this->get_recorded_task_failures();
446 if ( empty( $matomo_task_failures ) ) {
447 return;
448 }
449
450 add_action(
451 'admin_enqueue_scripts',
452 function () {
453 wp_enqueue_script(
454 'matomo-scheduled-task-errors',
455 plugins_url( '/assets/js/scheduled_task_errors.js', MATOMO_ANALYTICS_FILE ),
456 [ 'jquery' ],
457 '1.0.0',
458 true
459 );
460
461 wp_localize_script(
462 'matomo-scheduled-task-errors',
463 'mtmScheduledTaskErrorAjax',
464 [
465 'ajax_url' => admin_url( 'admin-ajax.php' ),
466 'nonce' => wp_create_nonce( 'matomo-scheduled-task-errors' ),
467 ]
468 );
469 }
470 );
471
472 add_action(
473 'admin_notices',
474 function () use ( $matomo_task_failures ) {
475 $user = wp_get_current_user();
476 if ( ! in_array( 'administrator', $user->roles, true ) ) {
477 return;
478 }
479
480 $matomo_diagnostics_url = home_url( '/wp-admin/admin.php?page=matomo-systemreport#logs' );
481
482 include __DIR__ . '/Admin/views/scheduled_tasks_failures.php';
483 }
484 );
485 }
486
487 public function register_ajax() {
488 add_action( 'wp_ajax_mtm_remove_cron_error', [ $this, 'remove_cron_error_ajax' ] );
489 }
490
491 public function remove_cron_error_ajax() {
492 // phpcs:disable WordPress.Security.NonceVerification.Missing
493 if ( empty( $_POST['matomo_job_id'] ) ) {
494 wp_send_json( false );
495 return;
496 }
497
498 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
499 $job_id = wp_unslash( $_POST['matomo_job_id'] );
500 $this->remove_task_errors( [ $job_id ] );
501
502 wp_send_json( true );
503 }
504 }
505