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