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