PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 5.12.1
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v5.12.1
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 week ago Commands 2 years ago Db 3 weeks ago Ecommerce 1 week ago Report 1 week ago Site 1 week ago TrackingCode 1 week ago Updater 4 years ago User 1 week ago Workarounds 2 years ago WpStatistics 1 week ago views 1 week ago AIBotTracking.php 1 week ago API.php 3 weeks ago Access.php 1 week ago AjaxTracker.php 5 months ago Annotations.php 1 week ago Bootstrap.php 11 months ago Capabilities.php 1 week ago Compatibility.php 1 week ago Email.php 1 week ago ErrorNotice.php 1 week ago Feature.php 3 months ago Installer.php 1 week ago Logger.php 1 year ago MinimumRequirementsNotice.php 1 month ago OptOut.php 2 months ago Paths.php 1 week ago PluginActionLinks.php 3 months ago PluginAdminOverrides.php 1 week ago PluginInit.php 1 week ago PrivacyBadge.php 4 years ago RedirectOnActivation.php 3 months ago Referral.php 3 months ago Roles.php 3 months ago ScheduledTasks.php 1 week ago Settings.php 1 week ago Site.php 3 years ago TrackingCode.php 1 week ago Uninstaller.php 6 months ago Updater.php 1 week ago User.php 3 weeks ago
ScheduledTasks.php
540 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 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writeable
198 if ( is_writeable( $file ) ) {
199 $content = str_replace( $search, $replace, $content );
200 $paths = new Paths();
201 $wp_filesystem = $paths->get_file_system();
202 $wp_filesystem->put_contents( $file, $content );
203 } else {
204 $this->logger->log( 'Cannot update file as not writable ' . $file );
205 }
206 }
207 }
208 }
209 } catch ( Exception $e ) {
210 $this->on_task_fail( 'disable_addhandler', $e, 'An error occurred when trying to disable AddHandler in apache config.' );
211 }
212 }
213 }
214
215 private function check_try_update() {
216 try {
217 $installer = new Installer( $this->settings );
218 if ( ! $installer->looks_like_it_is_installed() ) {
219 $installer->install();
220 }
221
222 $updater = new Updater( $this->settings );
223 $updater->update_if_needed();
224 } catch ( Exception $e ) {
225 // we don't want to rethrow exception otherwise some other blogs might never sync
226 $this->logger->log_exception( 'check_try_update', $e );
227 }
228 }
229
230 public function perform_update() {
231 $this->remove_task_errors( [ 'cron_update' ] );
232
233 $this->logger->log( 'Scheduled tasks perform update' );
234
235 try {
236 $updater = new Updater( $this->settings );
237 $updater->update();
238 } catch ( Exception $e ) {
239 $this->on_task_fail( 'cron_update', $e, 'An error occurred when upgrading the Matomo database tables.' );
240 return false;
241 }
242
243 return true;
244 }
245
246 public function update_geo_ip2_db( $db_url_override = null, $asn_url_override = null ) {
247 if ( is_multisite() && ! is_main_site() && is_plugin_active_for_network( 'matomo/matomo.php' ) ) {
248 return; // only run this task once per entire WP install
249 }
250
251 if ( ! $this->is_internet_features_enabled() ) {
252 return;
253 }
254
255 $this->remove_task_errors( [ 'update_geoip2' ] );
256
257 $this->logger->log( 'Scheduled tasks update geoip database' );
258 try {
259 Bootstrap::do_bootstrap();
260
261 $maxmind_license = $this->settings->get_global_option( 'maxmind_license_key' );
262 if ( empty( $maxmind_license ) ) {
263 $db_url = GeoIp2::getDbIpLiteUrl();
264 $asn_url = GeoIp2::getDbIpLiteUrl( 'asn' );
265 } else {
266 $db_url = 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz&license_key=' . $maxmind_license;
267 $asn_url = 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&suffix=tar.gz&license_key=' . $maxmind_license;
268 }
269
270 if ( ! empty( $db_url_override ) ) {
271 $db_url = $db_url_override;
272 }
273 if ( ! empty( $asn_url_override ) ) {
274 $asn_url = $asn_url_override;
275 }
276
277 Option::set( GeoIP2AutoUpdater::LOC_URL_OPTION_NAME, $db_url );
278
279 if ( Manager::getInstance()->isPluginActivated( 'Provider' ) ) {
280 Option::set( GeoIP2AutoUpdater::ISP_URL_OPTION_NAME, $asn_url );
281 } else {
282 Option::delete( GeoIP2AutoUpdater::ISP_URL_OPTION_NAME );
283 }
284
285 $updater = StaticContainer::get( GeoIP2AutoUpdater::class );
286 $updater->update();
287 if ( LocationProvider::getCurrentProviderId() !== Php::ID && LocationProvider::getProviderById( Php::ID ) ) {
288 LocationProvider::setCurrentProvider( Php::ID );
289 }
290 } catch ( Exception $e ) {
291 $next = wp_next_scheduled( self::EVENT_GEOIP );
292 if ( false === $next || $next - time() > 2 * 24 * 60 * 60 ) {
293 wp_schedule_single_event( time() + 24 * 60 * 60, self::EVENT_GEOIP );
294 }
295
296 $this->on_task_fail( 'update_geoip2', $e, 'An error occurred while updating the geolocation database.' );
297 }
298 }
299
300 public function sync() {
301 $this->remove_task_errors( [ 'matomo_url_sync', 'cron_sync' ] );
302
303 $this->check_try_update();
304
305 $this->logger->log( 'Scheduled tasks sync all sites and users' );
306
307 try {
308 // we update the matomo url if needed/when possible. eg an update may be needed when site_url changes
309 $installer = new Installer( $this->settings );
310 if ( $installer->looks_like_it_is_installed() ) {
311 Bootstrap::do_bootstrap();
312 $installer->set_matomo_url();
313 }
314 } catch ( Exception $e ) {
315 $this->on_task_fail( 'matomo_url_sync', $e, 'An error occurred when syncing the WordPress site URL with Matomo.' );
316 }
317
318 try {
319 $site = new SiteSync( $this->settings );
320 $site->sync_all();
321 $user = new UserSync();
322 $user->sync_all();
323 } catch ( Exception $e ) {
324 $this->on_task_fail( 'cron_sync', $e, 'An error occurred when syncing WordPress sites and users with Matomo.' );
325 }
326 }
327
328 public function archive( $force = false, $throw_exception = true ) {
329 $this->check_try_update();
330
331 if ( defined( 'MATOMO_DISABLE_WP_ARCHIVING' ) && MATOMO_DISABLE_WP_ARCHIVING ) {
332 return;
333 }
334
335 $this->remove_task_errors( [ 'archive_bootstrap', 'archive_main' ] );
336
337 // exceptions should not be rethrown as they will prevent other cron tasks
338 // from running (wp-cron.php does not handle exceptions). we only want exceptions
339 // when running tests.
340 $should_rethrow_exception = ( defined( 'MATOMO_PHPUNIT_TEST' ) && MATOMO_PHPUNIT_TEST );
341
342 $this->logger->log( 'Scheduled tasks archive data' );
343
344 try {
345 Bootstrap::do_bootstrap();
346 } catch ( Exception $e ) {
347 if ( $should_rethrow_exception || $force ) {
348 $this->logger->log_exception( 'archive_bootstrap', $e );
349
350 // we want to trigger an exception if it was forced from the UI
351 throw $e;
352 }
353
354 $this->on_task_fail( 'archive_bootstrap', $e, 'An error occurred during Matomo archiving.' );
355 return;
356 }
357
358 $archiver = new CronArchive();
359 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
360 $archiver->concurrentRequestsPerWebsite = 1;
361 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
362 $archiver->maxConcurrentArchivers = 1;
363
364 // see https://github.com/matomo-org/matomo/pull/21216
365 if ( ! CronArchive\SharedSiteIds::isSupported() ) {
366 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
367 $archiver->shouldArchiveAllSites = true;
368 }
369
370 if ( $force ) {
371 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
372 $archiver->shouldArchiveAllSites = true;
373 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
374 $archiver->disableScheduledTasks = true;
375 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
376 if ( ! defined( 'PIWIK_ARCHIVE_NO_TRUNCATE' ) ) {
377 define( 'PIWIK_ARCHIVE_NO_TRUNCATE', true );
378 }
379 }
380
381 if ( is_multisite() ) {
382 if ( is_network_admin() ) {
383 return; // nothing to archive
384 } else {
385 $blog_id = get_current_blog_id();
386 $idsite = Site::get_matomo_site_id( $blog_id );
387 if ( ! empty( $idsite ) ) {
388 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
389 $archiver->shouldArchiveSpecifiedSites = [ $idsite ];
390 } else {
391 // there is no site mapped to it so there's no point in archiving it
392 return;
393 }
394 }
395 }
396
397 try {
398 $archiver->main();
399
400 $archive_errors = $archiver->getErrors();
401 } catch ( Exception $e ) {
402 $this->on_task_fail( 'archive_main', $e, 'An error occurred during Matomo archiving.' );
403
404 $archive_errors = $archiver->getErrors();
405
406 if ( ! empty( $archive_errors ) ) {
407 $message = '';
408 foreach ( $archiver->getErrors() as $error ) {
409 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
410 $message .= var_export( $error, 1 ) . ' ';
411 }
412 $message = new Exception( trim( $message ) );
413 $this->logger->log_exception( 'archive_errors', $message );
414 }
415
416 if ( $throw_exception ) {
417 if ( $should_rethrow_exception ) {
418 throw $e;
419 }
420 // we otherwise only log the error but don't throw an exception
421 } else {
422 $archive_errors[] = $e->getMessage();
423 }
424 }
425
426 return $archive_errors;
427 }
428
429 public function uninstall() {
430 $this->logger->log( 'Scheduled tasks uninstall all events' );
431
432 foreach ( $this->get_all_events() as $event_name => $config ) {
433 $timestamp = wp_next_scheduled( $event_name );
434 if ( $timestamp ) {
435 wp_unschedule_event( $timestamp, $event_name );
436 }
437 }
438 foreach ( $this->get_all_events() as $event_name => $config ) {
439 wp_clear_scheduled_hook( $event_name );
440 }
441 }
442
443 private function on_task_fail( $log_key, Exception $exception, $user_error_message ) {
444 $this->logger->log_exception( $log_key, $exception, 'Matomo error - ' . $user_error_message . ' Details: ' );
445
446 $failures = $this->get_recorded_task_failures();
447
448 $failures[ $log_key ] = $user_error_message;
449
450 update_option( self::FAILURES_LIST_OPTION, $failures );
451 }
452
453 private function remove_task_errors( $log_keys ) {
454 // remove any failures from the list when the task starts again
455 $failures = $this->get_recorded_task_failures();
456 if ( empty( $failures ) ) {
457 return;
458 }
459
460 foreach ( $log_keys as $log_key ) {
461 unset( $failures[ $log_key ] );
462 }
463
464 update_option( self::FAILURES_LIST_OPTION, $failures );
465 }
466
467 public function get_recorded_task_failures() {
468 $failures = get_option( self::FAILURES_LIST_OPTION );
469 if ( ! is_array( $failures ) ) {
470 $failures = [];
471 }
472 return $failures;
473 }
474
475 public function show_errors_if_admin() {
476 if ( ! is_admin()
477 || ! Admin::is_matomo_admin()
478 ) {
479 return;
480 }
481
482 $matomo_task_failures = $this->get_recorded_task_failures();
483 if ( empty( $matomo_task_failures ) ) {
484 return;
485 }
486
487 add_action(
488 'admin_enqueue_scripts',
489 function () {
490 wp_localize_script(
491 'matomo-admin-js',
492 'mtmScheduledTaskErrorAjax',
493 [
494 'ajax_url' => admin_url( 'admin-ajax.php' ),
495 'nonce' => wp_create_nonce( 'matomo-scheduled-task-errors' ),
496 ]
497 );
498 }
499 );
500
501 add_action(
502 'admin_notices',
503 function () use ( $matomo_task_failures ) {
504 $user = wp_get_current_user();
505 if ( ! in_array( 'administrator', $user->roles, true ) ) {
506 return;
507 }
508
509 $matomo_diagnostics_url = home_url( '/wp-admin/admin.php?page=matomo-systemreport#logs' );
510
511 include __DIR__ . '/Admin/views/scheduled_tasks_failures.php';
512 }
513 );
514 }
515
516 public function register_ajax() {
517 add_action( 'wp_ajax_mtm_remove_cron_error', [ $this, 'remove_cron_error_ajax' ] );
518 }
519
520 public function remove_cron_error_ajax() {
521 check_ajax_referer( 'matomo-scheduled-task-errors' );
522
523 // phpcs:disable WordPress.Security.NonceVerification.Missing
524 if ( empty( $_POST['matomo_job_id'] ) ) {
525 wp_send_json( false );
526 return;
527 }
528
529 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
530 $job_id = wp_unslash( $_POST['matomo_job_id'] );
531 $this->remove_task_errors( [ $job_id ] );
532
533 wp_send_json( true );
534 }
535
536 private function is_internet_features_enabled() {
537 return strval( $this->site_config->get_config_value( 'General', 'enable_internet_features' ) ) === '1';
538 }
539 }
540