Matomo Analytics – Ethical Stats. Powerful Insights. - Version 1.0.4

Version Description

  • Update Matomo core to 3.13.4
  • Fix the website's timezone may be set to UTC instead of the WP timezone
  • Improve compatibility with PHP 7.4 by fixing more notices
  • Add a review link to the About page
  • Add a newsletter signup possibility to the About page.
  • Support MaxMind geolocation database
  • Better support for hiding login URLs eg with WPS plugin
  • Show header icon images
  • Update GeoIP DB monthly instead of weekly
  • Ask for a review every 90 days unless dismissed
  • Possibility to configure proxy client header

See changelog for all versions.

Download this release

Release Info

Developer matomoteam
Plugin Icon 128x128 Matomo Analytics – Ethical Stats. Powerful Insights.
Version 1.0.4
Comparing to
See all releases

Code changes from version 1.0.3 to 1.0.4

Files changed (102) hide show
  1. app/bootstrap.php +26 -7
  2. app/core/Access.php +10 -0
  3. app/core/Archive.php +4 -72
  4. app/core/Archive/ArchiveInvalidator.php +16 -7
  5. app/core/ArchiveProcessor/Loader.php +84 -13
  6. app/core/ArchiveProcessor/Parameters.php +5 -0
  7. app/core/Config/IniFileChain.php +6 -4
  8. app/core/Cookie.php +12 -10
  9. app/core/CronArchive.php +63 -25
  10. app/core/DataAccess/ArchiveSelector.php +92 -60
  11. app/core/DataAccess/LogAggregator.php +28 -20
  12. app/core/DataAccess/Model.php +13 -4
  13. app/core/Db/Schema/Mysql.php +1 -1
  14. app/core/DbHelper.php +42 -0
  15. app/core/FrontController.php +7 -0
  16. app/core/Http.php +6 -3
  17. app/core/Period.php +15 -0
  18. app/core/Period/Factory.php +16 -0
  19. app/core/Plugin/ControllerAdmin.php +3 -21
  20. app/core/RankingQuery.php +4 -1
  21. app/core/Session.php +1 -1
  22. app/core/Tracker/GoalManager.php +6 -1
  23. app/core/Tracker/Model.php +11 -4
  24. app/core/Tracker/Request.php +7 -1
  25. app/core/Tracker/Visit.php +4 -4
  26. app/core/Twig.php +1 -1
  27. app/core/Updates/3.13.4-b1.php +26 -0
  28. app/core/Version.php +1 -1
  29. app/core/View.php +3 -0
  30. app/lang/en.json +4 -1
  31. app/plugins/CoreAdminHome/Tasks.php +18 -0
  32. app/plugins/CoreAdminHome/templates/optOut.twig +8 -1
  33. app/plugins/CoreHome/CoreHome.php +6 -0
  34. app/plugins/Dashboard/Dashboard.php +1 -5
  35. app/plugins/Goals/Controller.php +0 -9
  36. app/plugins/Installation/ServerFilesGenerator.php +7 -1
  37. app/plugins/Live/Model.php +35 -54
  38. app/plugins/Marketplace/Api/Client.php +5 -1
  39. app/plugins/Marketplace/templates/getPremiumFeatures.twig +3 -0
  40. app/plugins/Monolog/Processor/ExceptionToTextProcessor.php +4 -0
  41. app/plugins/Proxy/Controller.php +6 -1
  42. app/plugins/TagManager/API/PreviewCookie.php +2 -2
  43. app/plugins/TagManager/Context/BaseContext.php +5 -4
  44. app/plugins/TagManager/Dao/VariablesDao.php +1 -1
  45. app/plugins/Transitions/API.php +39 -19
  46. app/plugins/UserCountry/templates/_updaterManage.twig +3 -2
  47. app/plugins/UserCountry/templates/adminIndex.twig +11 -2
  48. app/vendor/autoload.php +1 -1
  49. app/vendor/composer/autoload_classmap.php +1 -0
  50. app/vendor/composer/autoload_real.php +10 -7
  51. app/vendor/composer/autoload_static.php +7 -6
  52. assets/css/admin-style.css +5 -0
  53. assets/img/activity_log.jpg +0 -0
  54. assets/img/cohorts.png +0 -0
  55. assets/img/custom_reports.png +0 -0
  56. assets/img/form_analytics.jpg +0 -0
  57. assets/img/funnels.png +0 -0
  58. assets/img/heatmap.jpg +0 -0
  59. assets/img/logo-full.png +0 -0
  60. assets/img/logo.png +0 -0
  61. assets/img/media_analytics.jpg +0 -0
  62. assets/img/multi_attribution.png +0 -0
  63. assets/img/premiumbundle.png +0 -0
  64. assets/img/search_engine_keywords.png +0 -0
  65. assets/img/users_flow.png +0 -0
  66. assets/js/admin.js +7 -0
  67. assets/js/asset_manager_core_js.js +1 -1
  68. classes/WpMatomo.php +6 -17
  69. classes/WpMatomo/Admin/Admin.php +3 -2
  70. classes/WpMatomo/Admin/AdminSettings.php +10 -4
  71. classes/WpMatomo/Admin/AdvancedSettings.php +98 -0
  72. classes/WpMatomo/Admin/GeolocationSettings.php +71 -0
  73. classes/WpMatomo/Admin/Info.php +43 -2
  74. classes/WpMatomo/Admin/Menu.php +2 -2
  75. classes/WpMatomo/Admin/SystemReport.php +17 -2
  76. classes/WpMatomo/Admin/views/access.php +3 -2
  77. classes/WpMatomo/Admin/views/advanced_settings.php +59 -0
  78. classes/WpMatomo/Admin/views/exclusion_settings.php +5 -10
  79. classes/WpMatomo/Admin/views/geolocation_settings.php +74 -0
  80. classes/WpMatomo/Admin/views/get_started.php +1 -1
  81. classes/WpMatomo/Admin/views/info.php +6 -4
  82. classes/WpMatomo/Admin/views/info_help.php +1 -1
  83. classes/WpMatomo/Admin/views/info_multisite.php +1 -1
  84. classes/WpMatomo/Admin/views/info_newsletter.php +46 -0
  85. classes/WpMatomo/Admin/views/info_shared.php +1 -1
  86. classes/WpMatomo/Admin/views/marketplace.php +23 -16
  87. classes/WpMatomo/Admin/views/settings.php +1 -0
  88. classes/WpMatomo/Admin/views/summary.php +2 -2
  89. classes/WpMatomo/Admin/views/systemreport.php +2 -0
  90. classes/WpMatomo/Installer.php +4 -0
  91. classes/WpMatomo/Referral.php +125 -0
  92. classes/WpMatomo/ScheduledTasks.php +13 -5
  93. classes/WpMatomo/Settings.php +1 -0
  94. classes/WpMatomo/Site/Sync.php +5 -2
  95. classes/WpMatomo/TrackingCode.php +4 -2
  96. classes/WpMatomo/Updater.php +1 -1
  97. classes/WpMatomo/views/referral.php +22 -0
  98. matomo.php +50 -4
  99. plugins/WordPress/Controller.php +2 -1
  100. plugins/WordPress/WordPress.php +18 -1
  101. plugins/WordPress/WpAssetManager.php +2 -1
  102. readme.txt +20 -1
app/bootstrap.php CHANGED
@@ -22,8 +22,7 @@ if ( ! defined( 'PIWIK_ENABLE_ERROR_HANDLER' ) ) {
22
  }
23
  }
24
 
25
- $matomo_was_wp_loaded_directly = ! defined( 'ABSPATH' );
26
-
27
 
28
  function matomo_log_message_no_display($message)
29
  {
@@ -52,7 +51,8 @@ function matomo_log_message_no_display($message)
52
  }
53
  }
54
 
55
- if ( $matomo_was_wp_loaded_directly ) {
 
56
  // prevent from loading twice
57
  $matomo_wpload_base = '../../../../wp-load.php';
58
  $matomo_wpload_full = dirname( __FILE__ ) . '/' . $matomo_wpload_base;
@@ -76,8 +76,8 @@ if ( $matomo_was_wp_loaded_directly ) {
76
  });
77
  }
78
 
79
- if (!empty($_ENV['MATOMO_WP_ROOT_PATH']) && file_exists( rtrim($_ENV['MATOMO_WP_ROOT_PATH'], '/') . '/wp-load.php')) {
80
- require_once rtrim($_ENV['MATOMO_WP_ROOT_PATH'], '/') . '/wp-load.php';
81
  } elseif ( file_exists($matomo_wpload_full ) ) {
82
  require_once $matomo_wpload_full;
83
  } elseif (realpath( $matomo_wpload_full ) && file_exists(realpath( $matomo_wpload_full ))) {
@@ -94,6 +94,25 @@ if ( $matomo_was_wp_loaded_directly ) {
94
  }
95
  }
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  if ($matomo_is_archive_request) {
98
  restore_error_handler();
99
  if (ob_get_level()) {
@@ -106,7 +125,7 @@ if ( $matomo_was_wp_loaded_directly ) {
106
  }
107
 
108
  if ( ! defined( 'ABSPATH' ) ) {
109
- echo 'Could not find wp-load. If your server uses symlinks or a custom content directory, Matomo may not work for you as we cannot detect the paths correctly. For more information see https://matomo.org/faq/wordpress/what-are-the-requirements-for-matomo-for-wordpress/';
110
  exit; // if accessed directly
111
  }
112
 
@@ -115,7 +134,7 @@ if ( !is_plugin_active('matomo/matomo.php')
115
  exit;
116
  }
117
 
118
- if ($matomo_was_wp_loaded_directly) {
119
  // see https://github.com/matomo-org/wp-matomo/issues/190
120
  // wp-external-links plugin would register an ob_start(function () {...}) and manipulate any of our API output
121
  // and in some cases the output would get completely lost causing blank pages.
22
  }
23
  }
24
 
25
+ $GLOBALS['MATOMO_LOADED_DIRECTLY'] = ! defined( 'ABSPATH' );
 
26
 
27
  function matomo_log_message_no_display($message)
28
  {
51
  }
52
  }
53
 
54
+ if ( $GLOBALS['MATOMO_LOADED_DIRECTLY'] ) {
55
+
56
  // prevent from loading twice
57
  $matomo_wpload_base = '../../../../wp-load.php';
58
  $matomo_wpload_full = dirname( __FILE__ ) . '/' . $matomo_wpload_base;
76
  });
77
  }
78
 
79
+ if (!empty($_SERVER['MATOMO_WP_ROOT_PATH']) && file_exists( rtrim($_SERVER['MATOMO_WP_ROOT_PATH'], '/') . '/wp-load.php')) {
80
+ require_once rtrim($_SERVER['MATOMO_WP_ROOT_PATH'], '/') . '/wp-load.php';
81
  } elseif ( file_exists($matomo_wpload_full ) ) {
82
  require_once $matomo_wpload_full;
83
  } elseif (realpath( $matomo_wpload_full ) && file_exists(realpath( $matomo_wpload_full ))) {
94
  }
95
  }
96
 
97
+ if (!defined( 'ABSPATH')) {
98
+ // still not loaded... look in plugins directory if there is a config file for us.
99
+ $matomo_wpload_config = dirname(__FILE__) . '/../../matomo.wpload_dir.php';
100
+ if (file_exists( $matomo_wpload_config) && is_readable($matomo_wpload_config)) {
101
+ $matomo_wpload_content = @file_get_contents($matomo_wpload_config); // we do not include that file for security reasons
102
+ if (!empty($matomo_wpload_content)) {
103
+ $matomo_wpload_content = str_replace(array('<?php', 'exit;', 'wp-load.php'), '', $matomo_wpload_content);
104
+ $matomo_wpload_content = preg_replace('/\s/', '', $matomo_wpload_content);
105
+ $matomo_wpload_content = trim(ltrim(trim($matomo_wpload_content), '#')); // the path may be commented out # /abs/path
106
+ if (strpos($matomo_wpload_content, DIRECTORY_SEPARATOR) === 0) {
107
+ $matomo_wpload_file = rtrim($matomo_wpload_content, DIRECTORY_SEPARATOR) . '/wp-load.php';
108
+ if (file_exists($matomo_wpload_file) && is_readable($matomo_wpload_file)) {
109
+ require_once $matomo_wpload_file;
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
  if ($matomo_is_archive_request) {
117
  restore_error_handler();
118
  if (ob_get_level()) {
125
  }
126
 
127
  if ( ! defined( 'ABSPATH' ) ) {
128
+ echo 'Could not find wp-load. If your server uses symlinks or a custom content directory, Matomo may not work for you as we cannot detect the paths correctly. For more information see https://matomo.org/faq/wordpress/how-do-i-make-matomo-for-wordpress-work-when-i-have-a-custom-content-directory/';
129
  exit; // if accessed directly
130
  }
131
 
134
  exit;
135
  }
136
 
137
+ if ( $GLOBALS['MATOMO_LOADED_DIRECTLY'] ) {
138
  // see https://github.com/matomo-org/wp-matomo/issues/190
139
  // wp-external-links plugin would register an ob_start(function () {...}) and manipulate any of our API output
140
  // and in some cases the output would get completely lost causing blank pages.
app/core/Access.php CHANGED
@@ -727,6 +727,16 @@ class Access
727
 
728
  throw new NoAccessException($message);
729
  }
 
 
 
 
 
 
 
 
 
 
730
  }
731
 
732
  /**
727
 
728
  throw new NoAccessException($message);
729
  }
730
+
731
+ /**
732
+ * Returns true if the current user is logged in or not.
733
+ *
734
+ * @return bool
735
+ */
736
+ public function isUserLoggedIn()
737
+ {
738
+ return !empty($this->login);
739
+ }
740
  }
741
 
742
  /**
app/core/Archive.php CHANGED
@@ -12,7 +12,6 @@ use Piwik\Archive\ArchiveQuery;
12
  use Piwik\Archive\ArchiveQueryFactory;
13
  use Piwik\Archive\Parameters;
14
  use Piwik\ArchiveProcessor\Rules;
15
- use Piwik\Archive\ArchiveInvalidator;
16
  use Piwik\Container\StaticContainer;
17
  use Piwik\DataAccess\ArchiveSelector;
18
 
@@ -167,11 +166,6 @@ class Archive implements ArchiveQuery
167
  */
168
  private static $cache;
169
 
170
- /**
171
- * @var ArchiveInvalidator
172
- */
173
- private $invalidator;
174
-
175
  /**
176
  * @param Parameters $params
177
  * @param bool $forceIndexedBySite Whether to force index the result of a query by site ID.
@@ -183,8 +177,6 @@ class Archive implements ArchiveQuery
183
  $this->params = $params;
184
  $this->forceIndexedBySite = $forceIndexedBySite;
185
  $this->forceIndexedByDate = $forceIndexedByDate;
186
-
187
- $this->invalidator = StaticContainer::get('Piwik\Archive\ArchiveInvalidator');
188
  }
189
 
190
  /**
@@ -453,67 +445,6 @@ class Archive implements ArchiveQuery
453
  return $dataTable;
454
  }
455
 
456
- private function getSiteIdsThatAreRequestedInThisArchiveButWereNotInvalidatedYet()
457
- {
458
- if (is_null(self::$cache)) {
459
- self::$cache = Cache::getTransientCache();
460
- }
461
-
462
- $id = 'Archive.SiteIdsOfRememberedReportsInvalidated';
463
-
464
- if (!self::$cache->contains($id)) {
465
- self::$cache->save($id, array());
466
- }
467
-
468
- $siteIdsAlreadyHandled = self::$cache->fetch($id);
469
- $siteIdsRequested = $this->params->getIdSites();
470
-
471
- foreach ($siteIdsRequested as $index => $siteIdRequested) {
472
- $siteIdRequested = (int) $siteIdRequested;
473
-
474
- if (in_array($siteIdRequested, $siteIdsAlreadyHandled)) {
475
- unset($siteIdsRequested[$index]); // was already handled previously, do not do it again
476
- } else {
477
- $siteIdsAlreadyHandled[] = $siteIdRequested; // we will handle this id this time
478
- }
479
- }
480
-
481
- self::$cache->save($id, $siteIdsAlreadyHandled);
482
-
483
- return $siteIdsRequested;
484
- }
485
-
486
- private function invalidatedReportsIfNeeded()
487
- {
488
- $siteIdsRequested = $this->getSiteIdsThatAreRequestedInThisArchiveButWereNotInvalidatedYet();
489
-
490
- if (empty($siteIdsRequested)) {
491
- return; // all requested site ids were already handled
492
- }
493
-
494
- $sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
495
-
496
- foreach ($sitesPerDays as $date => $siteIds) {
497
- if (empty($siteIds)) {
498
- continue;
499
- }
500
-
501
- $siteIdsToActuallyInvalidate = array_intersect($siteIds, $siteIdsRequested);
502
-
503
- if (empty($siteIdsToActuallyInvalidate)) {
504
- continue; // all site ids that should be handled are already handled
505
- }
506
-
507
- try {
508
- $this->invalidator->markArchivesAsInvalidated($siteIdsToActuallyInvalidate, array(Date::factory($date)), false);
509
- } catch (\Exception $e) {
510
- Site::clearCache();
511
- throw $e;
512
- }
513
- }
514
-
515
- Site::clearCache();
516
- }
517
 
518
  /**
519
  * Queries archive tables for data and returns the result.
@@ -638,8 +569,6 @@ class Archive implements ArchiveQuery
638
  */
639
  private function cacheArchiveIdsAfterLaunching($archiveGroups, $plugins)
640
  {
641
- $this->invalidatedReportsIfNeeded();
642
-
643
  $today = Date::today();
644
 
645
  foreach ($this->params->getPeriods() as $period) {
@@ -856,8 +785,11 @@ class Archive implements ArchiveQuery
856
  */
857
  private function prepareArchive(array $archiveGroups, Site $site, Period $period)
858
  {
 
 
 
859
  $parameters = new ArchiveProcessor\Parameters($site, $period, $this->params->getSegment());
860
- $archiveLoader = new ArchiveProcessor\Loader($parameters);
861
 
862
  $periodString = $period->getRangeString();
863
 
12
  use Piwik\Archive\ArchiveQueryFactory;
13
  use Piwik\Archive\Parameters;
14
  use Piwik\ArchiveProcessor\Rules;
 
15
  use Piwik\Container\StaticContainer;
16
  use Piwik\DataAccess\ArchiveSelector;
17
 
166
  */
167
  private static $cache;
168
 
 
 
 
 
 
169
  /**
170
  * @param Parameters $params
171
  * @param bool $forceIndexedBySite Whether to force index the result of a query by site ID.
177
  $this->params = $params;
178
  $this->forceIndexedBySite = $forceIndexedBySite;
179
  $this->forceIndexedByDate = $forceIndexedByDate;
 
 
180
  }
181
 
182
  /**
445
  return $dataTable;
446
  }
447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
  /**
450
  * Queries archive tables for data and returns the result.
569
  */
570
  private function cacheArchiveIdsAfterLaunching($archiveGroups, $plugins)
571
  {
 
 
572
  $today = Date::today();
573
 
574
  foreach ($this->params->getPeriods() as $period) {
785
  */
786
  private function prepareArchive(array $archiveGroups, Site $site, Period $period)
787
  {
788
+ // if cron archiving is running, we will invalidate in CronArchive, not here
789
+ $invalidateBeforeArchiving = !SettingsServer::isArchivePhpTriggered();
790
+
791
  $parameters = new ArchiveProcessor\Parameters($site, $period, $this->params->getSegment());
792
+ $archiveLoader = new ArchiveProcessor\Loader($parameters, $invalidateBeforeArchiving);
793
 
794
  $periodString = $period->getRangeString();
795
 
app/core/Archive/ArchiveInvalidator.php CHANGED
@@ -78,11 +78,12 @@ class ArchiveInvalidator
78
  // we do not really have to get the value first. we could simply always try to call set() and it would update or
79
  // insert the record if needed but we do not want to lock the table (especially since there are still some
80
  // MyISAM installations)
81
- $values = Option::getLike($this->rememberArchivedReportIdStart . '%');
82
 
83
  $all = [];
84
  foreach ($values as $name => $value) {
85
- $suffix = substr($name, strlen($this->rememberArchivedReportIdStart));
 
86
  list($idSite, $dateStr) = explode('_', $suffix);
87
 
88
  $all[$idSite][$dateStr] = $value;
@@ -102,7 +103,7 @@ class ArchiveInvalidator
102
  // we do not really have to get the value first. we could simply always try to call set() and it would update or
103
  // insert the record if needed but we do not want to lock the table (especially since there are still some
104
  // MyISAM installations)
105
- $value = Option::getLike($key . '%');
106
  }
107
 
108
  // getLike() returns an empty array rather than 'false'
@@ -117,6 +118,7 @@ class ArchiveInvalidator
117
  $mykey = $this->buildRememberArchivedReportIdProcessSafe($idSite, $date->toString());
118
  Option::set($mykey, '1');
119
  Cache::clearCacheGeneral();
 
120
  }
121
  }
122
 
@@ -134,16 +136,21 @@ class ArchiveInvalidator
134
 
135
  public function getRememberedArchivedReportsThatShouldBeInvalidated()
136
  {
137
- $reports = Option::getLike($this->rememberArchivedReportIdStart . '%_%');
138
 
139
  $sitesPerDay = array();
140
 
141
  foreach ($reports as $report => $value) {
 
142
  $report = str_replace($this->rememberArchivedReportIdStart, '', $report);
143
  $report = explode('_', $report);
144
  $siteId = (int) $report[0];
145
  $date = $report[1];
146
 
 
 
 
 
147
  if (empty($sitesPerDay[$date])) {
148
  $sitesPerDay[$date] = array();
149
  }
@@ -170,14 +177,16 @@ class ArchiveInvalidator
170
  // This version is multi process safe on the insert of a new date to invalidate.
171
  private function buildRememberArchivedReportIdProcessSafe($idSite, $date)
172
  {
173
- $id = $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date);
 
174
  $id .= '_' . Common::getProcessId();
 
175
  return $id;
176
  }
177
 
178
  public function forgetRememberedArchivedReportsToInvalidateForSite($idSite)
179
  {
180
- $id = $this->buildRememberArchivedReportIdForSite($idSite) . '_%';
181
  $this->deleteOptionLike($id);
182
  Cache::clearCacheGeneral();
183
  }
@@ -201,7 +210,7 @@ class ArchiveInvalidator
201
  {
202
  // we're not using deleteLike since it maybe could cause deadlocks see https://github.com/matomo-org/matomo/issues/15545
203
  // we want to reduce number of rows scanned and only delete specific primary key
204
- $keys = Option::getLike($id . '%');
205
 
206
  if (empty($keys)) {
207
  return;
78
  // we do not really have to get the value first. we could simply always try to call set() and it would update or
79
  // insert the record if needed but we do not want to lock the table (especially since there are still some
80
  // MyISAM installations)
81
+ $values = Option::getLike('%' . $this->rememberArchivedReportIdStart . '%');
82
 
83
  $all = [];
84
  foreach ($values as $name => $value) {
85
+ $suffix = substr($name, strpos($name, $this->rememberArchivedReportIdStart));
86
+ $suffix = str_replace($this->rememberArchivedReportIdStart, '', $suffix);
87
  list($idSite, $dateStr) = explode('_', $suffix);
88
 
89
  $all[$idSite][$dateStr] = $value;
103
  // we do not really have to get the value first. we could simply always try to call set() and it would update or
104
  // insert the record if needed but we do not want to lock the table (especially since there are still some
105
  // MyISAM installations)
106
+ $value = Option::getLike('%' . $key . '%');
107
  }
108
 
109
  // getLike() returns an empty array rather than 'false'
118
  $mykey = $this->buildRememberArchivedReportIdProcessSafe($idSite, $date->toString());
119
  Option::set($mykey, '1');
120
  Cache::clearCacheGeneral();
121
+ return $mykey;
122
  }
123
  }
124
 
136
 
137
  public function getRememberedArchivedReportsThatShouldBeInvalidated()
138
  {
139
+ $reports = Option::getLike('%' . $this->rememberArchivedReportIdStart . '%_%');
140
 
141
  $sitesPerDay = array();
142
 
143
  foreach ($reports as $report => $value) {
144
+ $report = substr($report, strpos($report, $this->rememberArchivedReportIdStart));
145
  $report = str_replace($this->rememberArchivedReportIdStart, '', $report);
146
  $report = explode('_', $report);
147
  $siteId = (int) $report[0];
148
  $date = $report[1];
149
 
150
+ if (empty($siteId)) {
151
+ continue;
152
+ }
153
+
154
  if (empty($sitesPerDay[$date])) {
155
  $sitesPerDay[$date] = array();
156
  }
177
  // This version is multi process safe on the insert of a new date to invalidate.
178
  private function buildRememberArchivedReportIdProcessSafe($idSite, $date)
179
  {
180
+ $id = Common::getRandomString(4, 'abcdefghijklmnoprstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ') . '_';
181
+ $id .= $this->buildRememberArchivedReportIdForSiteAndDate($idSite, $date);
182
  $id .= '_' . Common::getProcessId();
183
+
184
  return $id;
185
  }
186
 
187
  public function forgetRememberedArchivedReportsToInvalidateForSite($idSite)
188
  {
189
+ $id = $this->buildRememberArchivedReportIdForSite($idSite);
190
  $this->deleteOptionLike($id);
191
  Cache::clearCacheGeneral();
192
  }
210
  {
211
  // we're not using deleteLike since it maybe could cause deadlocks see https://github.com/matomo-org/matomo/issues/15545
212
  // we want to reduce number of rows scanned and only delete specific primary key
213
+ $keys = Option::getLike('%' . $id . '%');
214
 
215
  if (empty($keys)) {
216
  return;
app/core/ArchiveProcessor/Loader.php CHANGED
@@ -8,13 +8,18 @@
8
  */
9
  namespace Piwik\ArchiveProcessor;
10
 
 
11
  use Piwik\Cache;
12
  use Piwik\Config;
13
  use Piwik\Container\StaticContainer;
14
  use Piwik\Context;
15
  use Piwik\DataAccess\ArchiveSelector;
 
16
  use Piwik\Date;
 
17
  use Piwik\Piwik;
 
 
18
 
19
  /**
20
  * This class uses PluginsArchiver class to trigger data aggregation and create archives.
@@ -33,9 +38,28 @@ class Loader
33
  */
34
  protected $params;
35
 
36
- public function __construct(Parameters $params)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  {
38
  $this->params = $params;
 
 
 
 
39
  }
40
 
41
  /**
@@ -65,11 +89,19 @@ class Loader
65
  {
66
  $this->params->setRequestedPlugin($pluginName);
67
 
68
- list($idArchive, $visits, $visitsConverted) = $this->loadExistingArchiveIdFromDb();
69
- if (!empty($idArchive)) {
70
  return $idArchive;
71
  }
72
 
 
 
 
 
 
 
 
 
73
  /** @var ArchivingStatus $archivingStatus */
74
  $archivingStatus = StaticContainer::get(ArchivingStatus::class);
75
  $archivingStatus->archiveStarted($this->params);
@@ -170,20 +202,17 @@ class Loader
170
  */
171
  public function loadExistingArchiveIdFromDb()
172
  {
173
- $noArchiveFound = array(false, false, false);
174
-
175
  if ($this->isArchivingForcedToTrigger()) {
176
- return $noArchiveFound;
177
- }
178
 
179
- $minDatetimeArchiveProcessedUTC = $this->getMinTimeArchiveProcessed();
180
- $idAndVisits = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC);
181
-
182
- if (!$idAndVisits) {
183
- return $noArchiveFound;
184
  }
185
 
186
- return $idAndVisits;
 
 
187
  }
188
 
189
  /**
@@ -242,4 +271,46 @@ class Loader
242
 
243
  return $cache->fetch($cacheKey);
244
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  }
8
  */
9
  namespace Piwik\ArchiveProcessor;
10
 
11
+ use Piwik\Archive\ArchiveInvalidator;
12
  use Piwik\Cache;
13
  use Piwik\Config;
14
  use Piwik\Container\StaticContainer;
15
  use Piwik\Context;
16
  use Piwik\DataAccess\ArchiveSelector;
17
+ use Piwik\DataAccess\ArchiveTableCreator;
18
  use Piwik\Date;
19
+ use Piwik\Db;
20
  use Piwik\Piwik;
21
+ use Piwik\Site;
22
+ use Psr\Log\LoggerInterface;
23
 
24
  /**
25
  * This class uses PluginsArchiver class to trigger data aggregation and create archives.
38
  */
39
  protected $params;
40
 
41
+ /**
42
+ * @var ArchiveInvalidator
43
+ */
44
+ private $invalidator;
45
+
46
+ /**
47
+ * @var \Piwik\Cache\Cache
48
+ */
49
+ private $cache;
50
+
51
+ /**
52
+ * @var LoggerInterface
53
+ */
54
+ private $logger;
55
+
56
+ public function __construct(Parameters $params, $invalidateBeforeArchiving = false)
57
  {
58
  $this->params = $params;
59
+ $this->invalidateBeforeArchiving = $invalidateBeforeArchiving;
60
+ $this->invalidator = StaticContainer::get(ArchiveInvalidator::class);
61
+ $this->cache = Cache::getTransientCache();
62
+ $this->logger = StaticContainer::get(LoggerInterface::class);
63
  }
64
 
65
  /**
89
  {
90
  $this->params->setRequestedPlugin($pluginName);
91
 
92
+ list($idArchive, $visits, $visitsConverted, $isAnyArchiveExists) = $this->loadExistingArchiveIdFromDb();
93
+ if (!empty($idArchive)) { // we have a usable idarchive (it's not invalidated and it's new enough)
94
  return $idArchive;
95
  }
96
 
97
+ // if there is an archive, but we can't use it for some reason, invalidate existing archives before
98
+ // we start archiving. if the archive is made invalid, we will correctly re-archive below.
99
+ if ($this->invalidateBeforeArchiving
100
+ && $isAnyArchiveExists
101
+ ) {
102
+ $this->invalidatedReportsIfNeeded();
103
+ }
104
+
105
  /** @var ArchivingStatus $archivingStatus */
106
  $archivingStatus = StaticContainer::get(ArchivingStatus::class);
107
  $archivingStatus->archiveStarted($this->params);
202
  */
203
  public function loadExistingArchiveIdFromDb()
204
  {
 
 
205
  if ($this->isArchivingForcedToTrigger()) {
206
+ $this->logger->debug("Archiving forced to trigger for {$this->params}.");
 
207
 
208
+ // return no usable archive found, and no existing archive. this will skip invalidation, which should
209
+ // be fine since we just force archiving.
210
+ return [false, false, false, false];
 
 
211
  }
212
 
213
+ $minDatetimeArchiveProcessedUTC = $this->getMinTimeArchiveProcessed();
214
+ $result = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC);
215
+ return $result;
216
  }
217
 
218
  /**
271
 
272
  return $cache->fetch($cacheKey);
273
  }
274
+
275
+ // public for tests
276
+ public function getReportsToInvalidate()
277
+ {
278
+ $sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
279
+
280
+ foreach ($sitesPerDays as $dateStr => $siteIds) {
281
+ if (empty($siteIds)
282
+ || !in_array($this->params->getSite()->getId(), $siteIds)
283
+ ) {
284
+ unset($sitesPerDays[$dateStr]);
285
+ }
286
+
287
+ $date = Date::factory($dateStr);
288
+ if ($date->isEarlier($this->params->getPeriod()->getDateStart())
289
+ || $date->isLater($this->params->getPeriod()->getDateEnd())
290
+ ) { // date in list is not the current date, so ignore it
291
+ unset($sitesPerDays[$dateStr]);
292
+ }
293
+ }
294
+
295
+ return $sitesPerDays;
296
+ }
297
+
298
+ private function invalidatedReportsIfNeeded()
299
+ {
300
+ $sitesPerDays = $this->getReportsToInvalidate();
301
+ if (empty($sitesPerDays)) {
302
+ return;
303
+ }
304
+
305
+ foreach ($sitesPerDays as $date => $siteIds) {
306
+ try {
307
+ $this->invalidator->markArchivesAsInvalidated([$this->params->getSite()->getId()], array(Date::factory($date)), false, $this->params->getSegment());
308
+ } catch (\Exception $e) {
309
+ Site::clearCache();
310
+ throw $e;
311
+ }
312
+ }
313
+
314
+ Site::clearCache();
315
+ }
316
  }
app/core/ArchiveProcessor/Parameters.php CHANGED
@@ -258,4 +258,9 @@ class Parameters
258
  {
259
  $this->isRootArchiveRequest = $isRootArchiveRequest;
260
  }
 
 
 
 
 
261
  }
258
  {
259
  $this->isRootArchiveRequest = $isRootArchiveRequest;
260
  }
261
+
262
+ public function __toString()
263
+ {
264
+ return "[idSite = {$this->getSite()->getId()}, period = {$this->getPeriod()->getLabel()} {$this->getPeriod()->getRangeString()}, segment = {$this->getSegment()->getString()}]";
265
+ }
266
  }
app/core/Config/IniFileChain.php CHANGED
@@ -210,13 +210,16 @@ class IniFileChain
210
  $this->resetSettingsChain($defaultSettingsFiles, $userSettingsFile);
211
  }
212
 
213
- if (!empty($userSettingsFile) && !empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE'])) {
 
 
 
214
  $cache = new Cache();
215
  $values = $cache->doFetch(self::CONFIG_CACHE_KEY);
216
 
217
  if (!empty($values)
218
  && isset($values['mergedSettings'])
219
- && isset($values['settingsChain'])) {
220
  $this->mergedSettings = $values['mergedSettings'];
221
  $this->settingsChain = $values['settingsChain'];
222
  return;
@@ -246,8 +249,7 @@ class IniFileChain
246
  $this->mergedSettings = call_user_func($GLOBALS['MATOMO_MODIFY_CONFIG_SETTINGS'], $this->mergedSettings);
247
  }
248
 
249
- if (!empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE'])
250
- && !empty($userSettingsFile)
251
  && !empty($this->mergedSettings)
252
  && !empty($this->settingsChain)) {
253
 
210
  $this->resetSettingsChain($defaultSettingsFiles, $userSettingsFile);
211
  }
212
 
213
+ $hasAbsoluteConfigFile = !empty($userSettingsFile) && strpos($userSettingsFile, DIRECTORY_SEPARATOR) === 0;
214
+ $useConfigCache = !empty($GLOBALS['ENABLE_CONFIG_PHP_CACHE']) && $hasAbsoluteConfigFile;
215
+
216
+ if ($useConfigCache) {
217
  $cache = new Cache();
218
  $values = $cache->doFetch(self::CONFIG_CACHE_KEY);
219
 
220
  if (!empty($values)
221
  && isset($values['mergedSettings'])
222
+ && isset($values['settingsChain'][$userSettingsFile])) {
223
  $this->mergedSettings = $values['mergedSettings'];
224
  $this->settingsChain = $values['settingsChain'];
225
  return;
249
  $this->mergedSettings = call_user_func($GLOBALS['MATOMO_MODIFY_CONFIG_SETTINGS'], $this->mergedSettings);
250
  }
251
 
252
+ if ($useConfigCache
 
253
  && !empty($this->mergedSettings)
254
  && !empty($this->settingsChain)) {
255
 
app/core/Cookie.php CHANGED
@@ -441,16 +441,18 @@ class Cookie
441
  $sameSite = ucfirst(strtolower($default));
442
 
443
  if ($sameSite == 'None') {
444
- $userAgent = Http::getUserAgent();
445
- $ddFactory = StaticContainer::get(\Piwik\DeviceDetector\DeviceDetectorFactory::class);
446
- $deviceDetector = $ddFactory->makeInstance($userAgent);
447
- $deviceDetector->parse();
448
-
449
- $browserFamily = \DeviceDetector\Parser\Client\Browser::getBrowserFamily($deviceDetector->getClient('short_name'));
450
- if ((!ProxyHttp::isHttps()) && $browserFamily === 'Chrome') {
451
- $sameSite = 'Lax';
452
- } else if ($browserFamily === 'Safari') {
453
- $sameSite = '';
 
 
454
  }
455
  }
456
 
441
  $sameSite = ucfirst(strtolower($default));
442
 
443
  if ($sameSite == 'None') {
444
+ if ((!ProxyHttp::isHttps())) {
445
+ $sameSite = 'Lax'; // None can be only used when secure flag will be set
446
+ } else {
447
+ $userAgent = Http::getUserAgent();
448
+ $ddFactory = StaticContainer::get(\Piwik\DeviceDetector\DeviceDetectorFactory::class);
449
+ $deviceDetector = $ddFactory->makeInstance($userAgent);
450
+ $deviceDetector->parse();
451
+
452
+ $browserFamily = \DeviceDetector\Parser\Client\Browser::getBrowserFamily($deviceDetector->getClient('short_name'));
453
+ if ($browserFamily === 'Safari') {
454
+ $sameSite = '';
455
+ }
456
  }
457
  }
458
 
app/core/CronArchive.php CHANGED
@@ -898,6 +898,8 @@ class CronArchive
898
 
899
  $visitsLastDays = 0;
900
 
 
 
901
  list($isThereArchive, $newDate) = $this->isThereAValidArchiveForPeriod($idSite, 'day', $date, $segment = '');
902
  if ($isThereArchive) {
903
  $visitsToday = Archive::build($idSite, 'day', $date)->getNumeric('nb_visits');
@@ -972,7 +974,8 @@ class CronArchive
972
  return $dayArchiveWasSuccessful;
973
  }
974
 
975
- private function isThereAValidArchiveForPeriod($idSite, $period, $date, $segment = '')
 
976
  {
977
  if (Range::isMultiplePeriod($date, $period)) {
978
  $rangePeriod = Factory::build($period, $date, Site::getTimezoneFor($idSite));
@@ -981,9 +984,17 @@ class CronArchive
981
  $periodsToCheck = [Factory::build($period, $date, Site::getTimezoneFor($idSite))];
982
  }
983
 
984
- $periodsToCheckRanges = array_map(function (Period $p) { return $p->getRangeString(); }, $periodsToCheck);
 
985
 
986
- $this->invalidateArchivedReportsForSitesThatNeedToBeArchivedAgain();
 
 
 
 
 
 
 
987
 
988
  $archiveIds = ArchiveSelector::getArchiveIds(
989
  [$idSite], $periodsToCheck, new Segment($segment, [$idSite]), $plugins = [], // empty plugins param since we only check for an 'all' archive
@@ -1003,36 +1014,59 @@ class CronArchive
1003
  // if there is an invalidated archive within the range, find out the oldest one and how far it is from today,
1004
  // and change the lastN $date to be value so it is correctly re-processed.
1005
  $newDate = $date;
1006
- if (!$isThereArchiveForAllPeriods
1007
- && preg_match('/^last([0-9]+)/', $date, $matches)
1008
- ) {
1009
- $lastNValue = (int) $matches[1];
1010
-
1011
- usort($diff, function ($lhs, $rhs) {
1012
- $lhsDate = explode(',', $lhs)[0];
1013
- $rhsDate = explode(',', $rhs)[0];
1014
-
1015
- if ($lhsDate == $rhsDate) {
1016
- return 1;
1017
- } else if (Date::factory($lhsDate)->isEarlier(Date::factory($rhsDate))) {
1018
- return -1;
1019
- } else {
1020
- return 1;
1021
- }
1022
- });
1023
 
1024
- $oldestDateWithoutArchive = explode(',', reset($diff))[0];
1025
- $todayInTimezone = Date::factoryInTimezone('today', Site::getTimezoneFor($idSite));
1026
 
1027
- /** @var Range $newRangePeriod */
1028
- $newRangePeriod = PeriodFactory::build($period, $oldestDateWithoutArchive . ',' . $todayInTimezone);
1029
 
1030
- $newDate = 'last' . min($lastNValue, $newRangePeriod->getNumberOfSubperiods());
 
 
 
 
1031
  }
1032
 
1033
  return [$isThereArchiveForAllPeriods, $newDate];
1034
  }
1035
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1036
  /**
1037
  * @param $idSite
1038
  * @return array
@@ -1102,6 +1136,8 @@ class CronArchive
1102
  return Request::ABORT;
1103
  }
1104
 
 
 
1105
  list($isThereArchive, $newDate) = $this->isThereAValidArchiveForPeriod($idSite, $period, $date, $segment);
1106
  if ($isThereArchive) {
1107
  $this->logArchiveWebsiteSkippedValidArchiveExists($idSite, $period, $date);
@@ -1979,6 +2015,8 @@ class CronArchive
1979
  return Request::ABORT;
1980
  }
1981
 
 
 
1982
  list($isThereArchive, $newDate) = $this->isThereAValidArchiveForPeriod($idSite, $period, $date, $segment);
1983
  if ($isThereArchive) {
1984
  $this->logArchiveWebsiteSkippedValidArchiveExists($idSite, $period, $date, $segment);
898
 
899
  $visitsLastDays = 0;
900
 
901
+ $this->invalidateArchivedReportsForSitesThatNeedToBeArchivedAgain();
902
+
903
  list($isThereArchive, $newDate) = $this->isThereAValidArchiveForPeriod($idSite, 'day', $date, $segment = '');
904
  if ($isThereArchive) {
905
  $visitsToday = Archive::build($idSite, 'day', $date)->getNumeric('nb_visits');
974
  return $dayArchiveWasSuccessful;
975
  }
976
 
977
+ // public for tests
978
+ public function isThereAValidArchiveForPeriod($idSite, $period, $date, $segment = '')
979
  {
980
  if (Range::isMultiplePeriod($date, $period)) {
981
  $rangePeriod = Factory::build($period, $date, Site::getTimezoneFor($idSite));
984
  $periodsToCheck = [Factory::build($period, $date, Site::getTimezoneFor($idSite))];
985
  }
986
 
987
+ $isTodayIncluded = $this->isTodayIncludedInPeriod($idSite, $periodsToCheck);
988
+ $isLast = preg_match('/^last([0-9]+)/', $date, $matches);
989
 
990
+ // don't do this check for a single period that includes today
991
+ if ($isTodayIncluded
992
+ && !$isLast
993
+ ) {
994
+ return [false, $date];
995
+ }
996
+
997
+ $periodsToCheckRanges = array_map(function (Period $p) { return $p->getRangeString(); }, $periodsToCheck);
998
 
999
  $archiveIds = ArchiveSelector::getArchiveIds(
1000
  [$idSite], $periodsToCheck, new Segment($segment, [$idSite]), $plugins = [], // empty plugins param since we only check for an 'all' archive
1014
  // if there is an invalidated archive within the range, find out the oldest one and how far it is from today,
1015
  // and change the lastN $date to be value so it is correctly re-processed.
1016
  $newDate = $date;
1017
+ if ($isLast) {
1018
+ if (!$isThereArchiveForAllPeriods) {
1019
+ $lastNValue = (int)$matches[1];
1020
+
1021
+ usort($diff, function ($lhs, $rhs) {
1022
+ $lhsDate = explode(',', $lhs)[0];
1023
+ $rhsDate = explode(',', $rhs)[0];
1024
+
1025
+ if ($lhsDate == $rhsDate) {
1026
+ return 1;
1027
+ } else if (Date::factory($lhsDate)->isEarlier(Date::factory($rhsDate))) {
1028
+ return -1;
1029
+ } else {
1030
+ return 1;
1031
+ }
1032
+ });
 
1033
 
1034
+ $oldestDateWithoutArchive = explode(',', reset($diff))[0];
1035
+ $todayInTimezone = Date::factoryInTimezone('today', Site::getTimezoneFor($idSite));
1036
 
1037
+ /** @var Range $newRangePeriod */
1038
+ $newRangePeriod = PeriodFactory::build($period, $oldestDateWithoutArchive . ',' . $todayInTimezone);
1039
 
1040
+ $newDate = 'last' . max(min($lastNValue, $newRangePeriod->getNumberOfSubperiods()), 2);
1041
+ } else if ($isTodayIncluded) {
1042
+ $isThereArchiveForAllPeriods = false;
1043
+ $newDate = 'last2';
1044
+ }
1045
  }
1046
 
1047
  return [$isThereArchiveForAllPeriods, $newDate];
1048
  }
1049
 
1050
+ /**
1051
+ * @param int $idSite
1052
+ * @param Period[] $periods
1053
+ * @return bool
1054
+ * @throws Exception
1055
+ */
1056
+ private function isTodayIncludedInPeriod($idSite, $periods)
1057
+ {
1058
+ $timezone = Site::getTimezoneFor($idSite);
1059
+ $today = Date::factoryInTimezone('today', $timezone);
1060
+
1061
+ foreach ($periods as $period) {
1062
+ if ($period->isDateInPeriod($today)) {
1063
+ return true;
1064
+ }
1065
+ }
1066
+
1067
+ return false;
1068
+ }
1069
+
1070
  /**
1071
  * @param $idSite
1072
  * @return array
1136
  return Request::ABORT;
1137
  }
1138
 
1139
+ $this->invalidateArchivedReportsForSitesThatNeedToBeArchivedAgain();
1140
+
1141
  list($isThereArchive, $newDate) = $this->isThereAValidArchiveForPeriod($idSite, $period, $date, $segment);
1142
  if ($isThereArchive) {
1143
  $this->logArchiveWebsiteSkippedValidArchiveExists($idSite, $period, $date);
2015
  return Request::ABORT;
2016
  }
2017
 
2018
+ $this->invalidateArchivedReportsForSitesThatNeedToBeArchivedAgain();
2019
+
2020
  list($isThereArchive, $newDate) = $this->isThereAValidArchiveForPeriod($idSite, $period, $date, $segment);
2021
  if ($isThereArchive) {
2022
  $this->logArchiveWebsiteSkippedValidArchiveExists($idSite, $period, $date, $segment);
app/core/DataAccess/ArchiveSelector.php CHANGED
@@ -48,7 +48,12 @@ class ArchiveSelector
48
  /**
49
  * @param ArchiveProcessor\Parameters $params
50
  * @param bool $minDatetimeArchiveProcessedUTC deprecated. will be removed in Matomo 4.
51
- * @return array|bool
 
 
 
 
 
52
  * @throws Exception
53
  */
54
  public static function getArchiveIdAndVisits(ArchiveProcessor\Parameters $params, $minDatetimeArchiveProcessedUTC = false, $includeInvalidated = true)
@@ -61,80 +66,41 @@ class ArchiveSelector
61
 
62
  $numericTable = ArchiveTableCreator::getNumericTable($dateStart);
63
 
64
- $minDatetimeIsoArchiveProcessedUTC = null;
65
- if ($minDatetimeArchiveProcessedUTC) {
66
- $minDatetimeIsoArchiveProcessedUTC = Date::factory($minDatetimeArchiveProcessedUTC)->getDatetime();
67
- }
68
-
69
  $requestedPlugin = $params->getRequestedPlugin();
70
  $segment = $params->getSegment();
71
  $plugins = array("VisitsSummary", $requestedPlugin);
72
 
73
  $doneFlags = Rules::getDoneFlags($plugins, $segment);
 
74
  $doneFlagValues = Rules::getSelectableDoneFlagValues($includeInvalidated, $params);
75
 
76
- $results = self::getModel()->getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC, $doneFlags, $doneFlagValues);
77
-
78
- if (empty($results)) {
79
- return false;
80
- }
81
-
82
- $idArchive = self::getMostRecentIdArchiveFromResults($segment, $requestedPlugin, $results);
83
-
84
- $idArchiveVisitsSummary = self::getMostRecentIdArchiveFromResults($segment, "VisitsSummary", $results);
85
-
86
- list($visits, $visitsConverted) = self::getVisitsMetricsFromResults($idArchive, $idArchiveVisitsSummary, $results);
87
-
88
- if (false === $visits && false === $idArchive) {
89
- return false;
90
  }
91
 
92
- return array($idArchive, $visits, $visitsConverted);
93
- }
94
 
95
- protected static function getVisitsMetricsFromResults($idArchive, $idArchiveVisitsSummary, $results)
96
- {
97
- $visits = $visitsConverted = false;
98
- $archiveWithVisitsMetricsWasFound = ($idArchiveVisitsSummary !== false);
99
 
100
- if ($archiveWithVisitsMetricsWasFound) {
101
- $visits = $visitsConverted = 0;
 
 
102
  }
103
 
104
- foreach ($results as $result) {
105
- if (in_array($result['idarchive'], array($idArchive, $idArchiveVisitsSummary))) {
106
- $value = (int)$result['value'];
107
- if (empty($visits)
108
- && $result['name'] == self::NB_VISITS_RECORD_LOOKED_UP
109
- ) {
110
- $visits = $value;
111
- }
112
- if (empty($visitsConverted)
113
- && $result['name'] == self::NB_VISITS_CONVERTED_RECORD_LOOKED_UP
114
- ) {
115
- $visitsConverted = $value;
116
- }
117
- }
118
  }
119
 
120
- return array($visits, $visitsConverted);
121
- }
122
-
123
- protected static function getMostRecentIdArchiveFromResults(Segment $segment, $requestedPlugin, $results)
124
- {
125
- $idArchive = false;
126
- $namesRequestedPlugin = Rules::getDoneFlags(array($requestedPlugin), $segment);
127
-
128
- foreach ($results as $result) {
129
- if ($idArchive === false
130
- && in_array($result['name'], $namesRequestedPlugin)
131
- ) {
132
- $idArchive = $result['idarchive'];
133
- break;
134
- }
135
- }
136
 
137
- return $idArchive;
138
  }
139
 
140
  /**
@@ -213,7 +179,6 @@ class ArchiveSelector
213
 
214
  $sql = sprintf($getArchiveIdsSql, $table, $dateCondition);
215
 
216
-
217
  $archiveIds = $db->fetchAll($sql, $bind);
218
 
219
  // get the archive IDs
@@ -381,4 +346,71 @@ class ArchiveSelector
381
  // create the SQL to find archives that are DONE
382
  return "((name IN ($allDoneFlags)) AND (value IN (" . implode(',', $possibleValues) . ")))";
383
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  }
48
  /**
49
  * @param ArchiveProcessor\Parameters $params
50
  * @param bool $minDatetimeArchiveProcessedUTC deprecated. will be removed in Matomo 4.
51
+ * @return array An array with four values: \
52
+ * - the latest archive ID or false if none
53
+ * - the latest visits value for the latest archive, regardless of whether the archive is invalidated or not
54
+ * - the latest visits converted value for the latest archive, regardless of whether the archive is invalidated or not
55
+ * - whether there is an archive that exists or not. if this is true and the latest archive is false, it means
56
+ * the archive found was not usable (for example, it was invalidated and we are not looking for invalidated archives)
57
  * @throws Exception
58
  */
59
  public static function getArchiveIdAndVisits(ArchiveProcessor\Parameters $params, $minDatetimeArchiveProcessedUTC = false, $includeInvalidated = true)
66
 
67
  $numericTable = ArchiveTableCreator::getNumericTable($dateStart);
68
 
 
 
 
 
 
69
  $requestedPlugin = $params->getRequestedPlugin();
70
  $segment = $params->getSegment();
71
  $plugins = array("VisitsSummary", $requestedPlugin);
72
 
73
  $doneFlags = Rules::getDoneFlags($plugins, $segment);
74
+ $requestedPluginDoneFlags = Rules::getDoneFlags([$requestedPlugin], $segment);
75
  $doneFlagValues = Rules::getSelectableDoneFlagValues($includeInvalidated, $params);
76
 
77
+ $results = self::getModel()->getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, null, $doneFlags);
78
+ if (empty($results)) { // no archive found
79
+ return [false, false, false, false];
 
 
 
 
 
 
 
 
 
 
 
80
  }
81
 
82
+ $result = self::findArchiveDataWithLatestTsArchived($results, $requestedPluginDoneFlags);
 
83
 
84
+ $visits = isset($result['nb_visits']) ? $result['nb_visits'] : false;
85
+ $visitsConverted = isset($result['nb_visits_converted']) ? $result['nb_visits_converted'] : false;
 
 
86
 
87
+ if (isset($result['value'])
88
+ && !in_array($result['value'], $doneFlagValues)
89
+ ) { // the archive cannot be considered valid for this request (has wrong done flag value)
90
+ return [false, $visits, $visitsConverted, true];
91
  }
92
 
93
+ // the archive is too old
94
+ if ($minDatetimeArchiveProcessedUTC
95
+ && isset($result['idarchive'])
96
+ && Date::factory($result['ts_archived'])->isEarlier(Date::factory($minDatetimeArchiveProcessedUTC))
97
+ ) {
98
+ return [false, $visits, $visitsConverted, true];
 
 
 
 
 
 
 
 
99
  }
100
 
101
+ $idArchive = isset($result['idarchive']) ? $result['idarchive'] : false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ return array($idArchive, $visits, $visitsConverted, true);
104
  }
105
 
106
  /**
179
 
180
  $sql = sprintf($getArchiveIdsSql, $table, $dateCondition);
181
 
 
182
  $archiveIds = $db->fetchAll($sql, $bind);
183
 
184
  // get the archive IDs
346
  // create the SQL to find archives that are DONE
347
  return "((name IN ($allDoneFlags)) AND (value IN (" . implode(',', $possibleValues) . ")))";
348
  }
349
+
350
+ /**
351
+ * This method takes the output of Model::getArchiveIdAndVisits() and selects data from the
352
+ * latest archives.
353
+ *
354
+ * This includes:
355
+ * - the idarchive with the latest ts_archived ($results will be ordered by ts_archived desc)
356
+ * - the visits/converted visits of the latest archive, which includes archives for VisitsSummary alone
357
+ * ($requestedPluginDoneFlags will have the done flag for the overall archive plus a done flag for
358
+ * VisitsSummary by itself)
359
+ * - the ts_archived for the latest idarchive
360
+ * - the doneFlag value for the latest archive
361
+ *
362
+ * @param $results
363
+ * @param $requestedPluginDoneFlags
364
+ * @return array
365
+ */
366
+ private static function findArchiveDataWithLatestTsArchived($results, $requestedPluginDoneFlags)
367
+ {
368
+ // find latest idarchive for each done flag
369
+ $idArchives = [];
370
+ foreach ($results as $row) {
371
+ $doneFlag = $row['name'];
372
+ if (preg_match('/^done/', $doneFlag)
373
+ && !isset($idArchives[$doneFlag])
374
+ ) {
375
+ $idArchives[$doneFlag] = $row['idarchive'];
376
+ }
377
+ }
378
+
379
+ $archiveData = [];
380
+
381
+ // gather the latest visits/visits_converted metrics
382
+ foreach ($results as $row) {
383
+ $name = $row['name'];
384
+ if (!isset($archiveData[$name])
385
+ && in_array($name, [self::NB_VISITS_RECORD_LOOKED_UP, self::NB_VISITS_CONVERTED_RECORD_LOOKED_UP])
386
+ && in_array($row['idarchive'], $idArchives)
387
+ ) {
388
+ $archiveData[$name] = $row['value'];
389
+ }
390
+ }
391
+
392
+ // if an archive is found, but the metric data isn't found, we set the value to 0,
393
+ // so it won't get returned as false. this is here because the code used to do this before this change
394
+ // and we didn't want to introduce any side effects. it may be removable in the future.
395
+ foreach ([self::NB_VISITS_RECORD_LOOKED_UP, self::NB_VISITS_CONVERTED_RECORD_LOOKED_UP] as $metric) {
396
+ if (!empty($idArchives)
397
+ && !isset($archiveData[$metric])
398
+ ) {
399
+ $archiveData[$metric] = 0;
400
+ }
401
+ }
402
+
403
+ // set the idarchive & ts_archived for the archive we're looking for
404
+ foreach ($results as $row) {
405
+ $name = $row['name'];
406
+ if (in_array($name, $requestedPluginDoneFlags)) {
407
+ $archiveData['idarchive'] = $row['idarchive'];
408
+ $archiveData['ts_archived'] = $row['ts_archived'];
409
+ $archiveData['value'] = $row['value'];
410
+ break;
411
+ }
412
+ }
413
+
414
+ return $archiveData;
415
+ }
416
  }
app/core/DataAccess/LogAggregator.php CHANGED
@@ -467,18 +467,18 @@ class LogAggregator
467
  *
468
  * The following columns are in each row of the result set:
469
  *
470
- * - **{@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}**: The total number of unique visitors in this group
471
  * of aggregated visits.
472
- * - **{@link Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits aggregated.
473
- * - **{@link Piwik\Metrics::INDEX_NB_ACTIONS}**: The total number of actions performed in this group of
474
  * aggregated visits.
475
- * - **{@link Piwik\Metrics::INDEX_MAX_ACTIONS}**: The maximum actions perfomred in one visit for this group of
476
  * visits.
477
- * - **{@link Piwik\Metrics::INDEX_SUM_VISIT_LENGTH}**: The total amount of time spent on the site for this
478
  * group of visits.
479
- * - **{@link Piwik\Metrics::INDEX_BOUNCE_COUNT}**: The total number of bounced visits in this group of
480
  * visits.
481
- * - **{@link Piwik\Metrics::INDEX_NB_VISITS_CONVERTED}**: The total number of visits for which at least one
482
  * conversion occurred, for this group of visits.
483
  *
484
  * Additional data can be selected by setting the `$additionalSelects` parameter.
@@ -498,24 +498,26 @@ class LogAggregator
498
  * @param bool|array $metrics The set of metrics to calculate and return. If false, the query will select
499
  * all of them. The following values can be used:
500
  *
501
- * - {@link Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}
502
- * - {@link Piwik\Metrics::INDEX_NB_VISITS}
503
- * - {@link Piwik\Metrics::INDEX_NB_ACTIONS}
504
- * - {@link Piwik\Metrics::INDEX_MAX_ACTIONS}
505
- * - {@link Piwik\Metrics::INDEX_SUM_VISIT_LENGTH}
506
- * - {@link Piwik\Metrics::INDEX_BOUNCE_COUNT}
507
- * - {@link Piwik\Metrics::INDEX_NB_VISITS_CONVERTED}
508
  * @param bool|\Piwik\RankingQuery $rankingQuery
509
  * A pre-configured ranking query instance that will be used to limit the result.
510
- * If set, the return value is the array returned by {@link Piwik\RankingQuery::execute()}.
 
 
511
  *
512
  * @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of
513
- * {@link Piwik\RankingQuery::execute()}. Read {@link queryVisitsByDimension() this}
514
  * to see what aggregate data is calculated by the query.
515
  * @api
516
  */
517
  public function queryVisitsByDimension(array $dimensions = array(), $where = false, array $additionalSelects = array(),
518
- $metrics = false, $rankingQuery = false, $orderBy = false)
519
  {
520
  $tableName = self::LOG_VISIT_TABLE;
521
  $availableMetrics = $this->getVisitsMetricFields();
@@ -551,9 +553,11 @@ class LogAggregator
551
  $rankingQuery->addColumn(Metrics::INDEX_MAX_ACTIONS, 'max');
552
  }
553
 
554
- return $rankingQuery->execute($query['sql'], $query['bind']);
555
  }
556
 
 
 
557
  return $this->getDb()->query($query['sql'], $query['bind']);
558
  }
559
 
@@ -878,6 +882,7 @@ class LogAggregator
878
  * If a string is used for this parameter, the table alias is not
879
  * suffixed (since there is only one column).
880
  * @param string $secondaryOrderBy A secondary order by clause for the ranking query
 
881
  * @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of
882
  * {@link Piwik\RankingQuery::execute()}. Read [this](#queryEcommerceItems-result-set)
883
  * to see what aggregate data is calculated by the query.
@@ -890,7 +895,8 @@ class LogAggregator
890
  $metrics = false,
891
  $rankingQuery = null,
892
  $joinLogActionOnColumn = false,
893
- $secondaryOrderBy = null
 
894
  ) {
895
  $tableName = self::LOG_ACTIONS_TABLE;
896
  $availableMetrics = $this->getActionsMetricFields();
@@ -942,9 +948,11 @@ class LogAggregator
942
 
943
  $rankingQuery->addColumn($sumColumns, 'sum');
944
 
945
- return $rankingQuery->execute($query['sql'], $query['bind']);
946
  }
947
 
 
 
948
  return $this->getDb()->query($query['sql'], $query['bind']);
949
  }
950
 
467
  *
468
  * The following columns are in each row of the result set:
469
  *
470
+ * - **{@link \Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}**: The total number of unique visitors in this group
471
  * of aggregated visits.
472
+ * - **{@link \Piwik\Metrics::INDEX_NB_VISITS}**: The total number of visits aggregated.
473
+ * - **{@link \Piwik\Metrics::INDEX_NB_ACTIONS}**: The total number of actions performed in this group of
474
  * aggregated visits.
475
+ * - **{@link \Piwik\Metrics::INDEX_MAX_ACTIONS}**: The maximum actions perfomred in one visit for this group of
476
  * visits.
477
+ * - **{@link \Piwik\Metrics::INDEX_SUM_VISIT_LENGTH}**: The total amount of time spent on the site for this
478
  * group of visits.
479
+ * - **{@link \Piwik\Metrics::INDEX_BOUNCE_COUNT}**: The total number of bounced visits in this group of
480
  * visits.
481
+ * - **{@link \Piwik\Metrics::INDEX_NB_VISITS_CONVERTED}**: The total number of visits for which at least one
482
  * conversion occurred, for this group of visits.
483
  *
484
  * Additional data can be selected by setting the `$additionalSelects` parameter.
498
  * @param bool|array $metrics The set of metrics to calculate and return. If false, the query will select
499
  * all of them. The following values can be used:
500
  *
501
+ * - {@link \Piwik\Metrics::INDEX_NB_UNIQ_VISITORS}
502
+ * - {@link \Piwik\Metrics::INDEX_NB_VISITS}
503
+ * - {@link \Piwik\Metrics::INDEX_NB_ACTIONS}
504
+ * - {@link \Piwik\Metrics::INDEX_MAX_ACTIONS}
505
+ * - {@link \Piwik\Metrics::INDEX_SUM_VISIT_LENGTH}
506
+ * - {@link \Piwik\Metrics::INDEX_BOUNCE_COUNT}
507
+ * - {@link \Piwik\Metrics::INDEX_NB_VISITS_CONVERTED}
508
  * @param bool|\Piwik\RankingQuery $rankingQuery
509
  * A pre-configured ranking query instance that will be used to limit the result.
510
+ * If set, the return value is the array returned by {@link \Piwik\RankingQuery::execute()}.
511
+ * @param bool|string $orderBy Order By clause to add (e.g. user_id ASC)
512
+ * @param int $timeLimitInMs Adds a MAX_EXECUTION_TIME query hint to the query if $timeLimitInMs > 0
513
  *
514
  * @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of
515
+ * {@link \Piwik\RankingQuery::execute()}. Read {@link queryVisitsByDimension() this}
516
  * to see what aggregate data is calculated by the query.
517
  * @api
518
  */
519
  public function queryVisitsByDimension(array $dimensions = array(), $where = false, array $additionalSelects = array(),
520
+ $metrics = false, $rankingQuery = false, $orderBy = false, $timeLimitInMs = -1)
521
  {
522
  $tableName = self::LOG_VISIT_TABLE;
523
  $availableMetrics = $this->getVisitsMetricFields();
553
  $rankingQuery->addColumn(Metrics::INDEX_MAX_ACTIONS, 'max');
554
  }
555
 
556
+ return $rankingQuery->execute($query['sql'], $query['bind'], $timeLimitInMs);
557
  }
558
 
559
+ $query['sql'] = DbHelper::addMaxExecutionTimeHintToQuery($query['sql'], $timeLimitInMs);
560
+
561
  return $this->getDb()->query($query['sql'], $query['bind']);
562
  }
563
 
882
  * If a string is used for this parameter, the table alias is not
883
  * suffixed (since there is only one column).
884
  * @param string $secondaryOrderBy A secondary order by clause for the ranking query
885
+ * @param int $timeLimitInMs Adds a MAX_EXECUTION_TIME hint to the query if $timeLimitInMs > 0
886
  * @return mixed A Zend_Db_Statement if `$rankingQuery` isn't supplied, otherwise the result of
887
  * {@link Piwik\RankingQuery::execute()}. Read [this](#queryEcommerceItems-result-set)
888
  * to see what aggregate data is calculated by the query.
895
  $metrics = false,
896
  $rankingQuery = null,
897
  $joinLogActionOnColumn = false,
898
+ $secondaryOrderBy = null,
899
+ $timeLimitInMs = -1
900
  ) {
901
  $tableName = self::LOG_ACTIONS_TABLE;
902
  $availableMetrics = $this->getActionsMetricFields();
948
 
949
  $rankingQuery->addColumn($sumColumns, 'sum');
950
 
951
+ return $rankingQuery->execute($query['sql'], $query['bind'], $timeLimitInMs);
952
  }
953
 
954
+ $query['sql'] = DbHelper::addMaxExecutionTimeHintToQuery($query['sql'], $timeLimitInMs);
955
+
956
  return $this->getDb()->query($query['sql'], $query['bind']);
957
  }
958
 
app/core/DataAccess/Model.php CHANGED
@@ -215,7 +215,8 @@ class Model
215
  return $deletedRows;
216
  }
217
 
218
- public function getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC, $doneFlags, $doneFlagValues)
 
219
  {
220
  $bindSQL = array($idSite,
221
  $dateStartIso,
@@ -231,7 +232,8 @@ class Model
231
  $bindSQL[] = $minDatetimeIsoArchiveProcessedUTC;
232
  }
233
 
234
- $sqlQuery = "SELECT idarchive, value, name, date1 as startDate FROM $numericTable
 
235
  WHERE idsite = ?
236
  AND date1 = ?
237
  AND date2 = ?
@@ -240,7 +242,7 @@ class Model
240
  OR name = '" . ArchiveSelector::NB_VISITS_RECORD_LOOKED_UP . "'
241
  OR name = '" . ArchiveSelector::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "')
242
  $timeStampWhere
243
- ORDER BY idarchive DESC";
244
  $results = Db::fetchAll($sqlQuery, $bindSQL);
245
 
246
  return $results;
@@ -428,7 +430,14 @@ class Model
428
  $allDoneFlags = "'" . implode("','", $doneFlags) . "'";
429
 
430
  // create the SQL to find archives that are DONE
431
- return "((name IN ($allDoneFlags)) AND (value IN (" . implode(',', $possibleValues) . ")))";
 
 
 
 
 
 
 
432
  }
433
 
434
  }
215
  return $deletedRows;
216
  }
217
 
218
+ public function getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC,
219
+ $doneFlags, $doneFlagValues = null)
220
  {
221
  $bindSQL = array($idSite,
222
  $dateStartIso,
232
  $bindSQL[] = $minDatetimeIsoArchiveProcessedUTC;
233
  }
234
 
235
+ // NOTE: we can't predict how many segments there will be so there could be lots of nb_visits/nb_visits_converted rows... have to select everything.
236
+ $sqlQuery = "SELECT idarchive, value, name, ts_archived, date1 as startDate FROM $numericTable
237
  WHERE idsite = ?
238
  AND date1 = ?
239
  AND date2 = ?
242
  OR name = '" . ArchiveSelector::NB_VISITS_RECORD_LOOKED_UP . "'
243
  OR name = '" . ArchiveSelector::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "')
244
  $timeStampWhere
245
+ ORDER BY ts_archived DESC, idarchive DESC";
246
  $results = Db::fetchAll($sqlQuery, $bindSQL);
247
 
248
  return $results;
430
  $allDoneFlags = "'" . implode("','", $doneFlags) . "'";
431
 
432
  // create the SQL to find archives that are DONE
433
+ $result = "((name IN ($allDoneFlags))";
434
+
435
+ if (!empty($possibleValues)) {
436
+ $result .= " AND (value IN (" . implode(',', $possibleValues) . ")))";
437
+ }
438
+ $result .= ')';
439
+
440
+ return $result;
441
  }
442
 
443
  }
app/core/Db/Schema/Mysql.php CHANGED
@@ -225,7 +225,7 @@ class Mysql implements SchemaInterface
225
  idvisit BIGINT(10) UNSIGNED NOT NULL,
226
  idaction_url_ref INTEGER(10) UNSIGNED NULL DEFAULT 0,
227
  idaction_name_ref INTEGER(10) UNSIGNED NULL,
228
- custom_float FLOAT NULL DEFAULT NULL,
229
  PRIMARY KEY(idlink_va),
230
  INDEX index_idvisit(idvisit)
231
  ) ENGINE=$engine DEFAULT CHARSET=utf8
225
  idvisit BIGINT(10) UNSIGNED NOT NULL,
226
  idaction_url_ref INTEGER(10) UNSIGNED NULL DEFAULT 0,
227
  idaction_name_ref INTEGER(10) UNSIGNED NULL,
228
+ custom_float DOUBLE NULL DEFAULT NULL,
229
  PRIMARY KEY(idlink_va),
230
  INDEX index_idvisit(idvisit)
231
  ) ENGINE=$engine DEFAULT CHARSET=utf8
app/core/DbHelper.php CHANGED
@@ -177,6 +177,21 @@ class DbHelper
177
  Schema::getInstance()->createDatabase($dbName);
178
  }
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  /**
181
  * Get the SQL to create Piwik tables
182
  *
@@ -212,6 +227,33 @@ class DbHelper
212
  ArchiveTableCreator::refreshTableList($forceReload = true);
213
  }
214
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  /**
216
  * Returns true if the string is a valid database name for MySQL. MySQL allows + in the database names.
217
  * Database names that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), plus(+), and dot(.) will be accepted.
177
  Schema::getInstance()->createDatabase($dbName);
178
  }
179
 
180
+ /**
181
+ * Returns if the given table has an index with the given name
182
+ *
183
+ * @param string $table
184
+ * @param string $indexName
185
+ *
186
+ * @return bool
187
+ * @throws Exception
188
+ */
189
+ public static function tableHasIndex($table, $indexName)
190
+ {
191
+ $result = Db::get()->fetchOne('SHOW INDEX FROM '.$table.' WHERE Key_name = ?', [$indexName]);
192
+ return !empty($result);
193
+ }
194
+
195
  /**
196
  * Get the SQL to create Piwik tables
197
  *
227
  ArchiveTableCreator::refreshTableList($forceReload = true);
228
  }
229
 
230
+ /**
231
+ * Adds a MAX_EXECUTION_TIME hint into a SELECT query if $limit is bigger than 1
232
+ *
233
+ * @param string $sql query to add hint to
234
+ * @param int $limit time limit in seconds
235
+ * @return string
236
+ */
237
+ public static function addMaxExecutionTimeHintToQuery($sql, $limit)
238
+ {
239
+ if ($limit <= 0) {
240
+ return $sql;
241
+ }
242
+
243
+ $sql = trim($sql);
244
+ $pos = stripos($sql, 'SELECT');
245
+ if ($pos !== false) {
246
+
247
+ $timeInMs = $limit * 1000;
248
+ $timeInMs = (int) $timeInMs;
249
+ $maxExecutionTimeHint = ' /*+ MAX_EXECUTION_TIME('.$timeInMs.') */ ';
250
+
251
+ $sql = substr_replace($sql, 'SELECT ' . $maxExecutionTimeHint, $pos, strlen('SELECT'));
252
+ }
253
+
254
+ return $sql;
255
+ }
256
+
257
  /**
258
  * Returns true if the string is a valid database name for MySQL. MySQL allows + in the database names.
259
  * Database names that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), plus(+), and dot(.) will be accepted.
app/core/FrontController.php CHANGED
@@ -392,6 +392,7 @@ class FrontController extends Singleton
392
 
393
  $loggedIn = false;
394
 
 
395
  // try authenticating w/ session first...
396
  $sessionAuth = $this->makeSessionAuthenticator();
397
  if ($sessionAuth) {
@@ -647,6 +648,12 @@ class FrontController extends Singleton
647
 
648
  private function makeSessionAuthenticator()
649
  {
 
 
 
 
 
 
650
  $module = Common::getRequestVar('module', self::DEFAULT_MODULE, 'string');
651
  $action = Common::getRequestVar('action', false);
652
 
392
 
393
  $loggedIn = false;
394
 
395
+ // don't use sessionauth in cli mode
396
  // try authenticating w/ session first...
397
  $sessionAuth = $this->makeSessionAuthenticator();
398
  if ($sessionAuth) {
648
 
649
  private function makeSessionAuthenticator()
650
  {
651
+ if (Common::isPhpClimode()
652
+ && !defined('PIWIK_TEST_MODE')
653
+ ) { // don't use the session auth during CLI requests
654
+ return null;
655
+ }
656
+
657
  $module = Common::getRequestVar('module', self::DEFAULT_MODULE, 'string');
658
  $action = Common::getRequestVar('action', false);
659
 
app/core/Http.php CHANGED
@@ -934,9 +934,12 @@ class Http
934
  * With HTTP/2 Cloudflare is passing headers in lowercase (e.g. 'content-type' instead of 'Content-Type')
935
  * which breaks any code which uses the header data.
936
  */
937
- $camelName = ucwords($name, '-');
938
- if ($camelName !== $name) {
939
- $headers[$camelName] = trim($value);
 
 
 
940
  }
941
  }
942
 
934
  * With HTTP/2 Cloudflare is passing headers in lowercase (e.g. 'content-type' instead of 'Content-Type')
935
  * which breaks any code which uses the header data.
936
  */
937
+ if (version_compare(PHP_VERSION, '5.5.16', '>=')) {
938
+ // Passing a second arg to ucwords is not supported by older versions of PHP
939
+ $camelName = ucwords($name, '-');
940
+ if ($camelName !== $name) {
941
+ $headers[$camelName] = trim($value);
942
+ }
943
  }
944
  }
945
 
app/core/Period.php CHANGED
@@ -247,6 +247,21 @@ abstract class Period
247
  return $this->subperiods;
248
  }
249
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  /**
251
  * Add a date to the period.
252
  *
247
  return $this->subperiods;
248
  }
249
 
250
+ /**
251
+ * Returns whether the date `$date` is within the current period or not.
252
+ *
253
+ * Note: the time component of the period's dates and `$date` is ignored.
254
+ *
255
+ * @param Date $today
256
+ * @return bool
257
+ */
258
+ public function isDateInPeriod(Date $date)
259
+ {
260
+ $ts = $date->getStartOfDay()->getTimestamp();
261
+ return $ts >= $this->getDateStart()->getStartOfDay()->getTimestamp()
262
+ && $ts < $this->getDateEnd()->addDay(1)->getStartOfDay()->getTimestamp();
263
+ }
264
+
265
  /**
266
  * Add a date to the period.
267
  *
app/core/Period/Factory.php CHANGED
@@ -74,6 +74,7 @@ abstract class Factory
74
  self::checkPeriodIsEnabled($period);
75
 
76
  if (is_string($date)) {
 
77
  if (Period::isMultiplePeriod($date, $period)
78
  || $period == 'range'
79
  ) {
@@ -130,6 +131,19 @@ abstract class Factory
130
  throw new Exception($message);
131
  }
132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
  /**
134
  * Creates a Period instance using a period, date and timezone.
135
  *
@@ -146,6 +160,8 @@ abstract class Factory
146
  $timezone = 'UTC';
147
  }
148
 
 
 
149
  if ($period == 'range') {
150
  self::checkPeriodIsEnabled('range');
151
  $oPeriod = new Range('range', $date, $timezone, Date::factory('today', $timezone));
74
  self::checkPeriodIsEnabled($period);
75
 
76
  if (is_string($date)) {
77
+ list($period, $date) = self::convertRangeToDateIfNeeded($period, $date);
78
  if (Period::isMultiplePeriod($date, $period)
79
  || $period == 'range'
80
  ) {
131
  throw new Exception($message);
132
  }
133
 
134
+ private static function convertRangeToDateIfNeeded($period, $date)
135
+ {
136
+ if (is_string($period) && is_string($date) && $period === 'range') {
137
+ $dates = explode(',', $date);
138
+ if (count($dates) === 2 && $dates[0] === $dates[1]) {
139
+ $period = 'day';
140
+ $date = $dates[0];
141
+ }
142
+ }
143
+
144
+ return array($period, $date);
145
+ }
146
+
147
  /**
148
  * Creates a Period instance using a period, date and timezone.
149
  *
160
  $timezone = 'UTC';
161
  }
162
 
163
+ list($period, $date) = self::convertRangeToDateIfNeeded($period, $date);
164
+
165
  if ($period == 'range') {
166
  self::checkPeriodIsEnabled('range');
167
  $oPeriod = new Range('range', $date, $timezone, Date::factory('today', $timezone));
app/core/Plugin/ControllerAdmin.php CHANGED
@@ -207,12 +207,12 @@ abstract class ControllerAdmin extends Controller
207
  }
208
 
209
  /**
210
- * PHP Version required by the next major Piwik version
211
  * @return string
212
  */
213
  private static function getNextRequiredMinimumPHP()
214
  {
215
- return '7.1';
216
  }
217
 
218
  private static function isUsingPhpVersionCompatibleWithNextPiwik()
@@ -222,25 +222,7 @@ abstract class ControllerAdmin extends Controller
222
 
223
  private static function notifyWhenPhpVersionIsNotCompatibleWithNextMajorPiwik()
224
  {
225
- return; // no major version coming
226
-
227
- if (self::isUsingPhpVersionCompatibleWithNextPiwik()) {
228
- return;
229
- }
230
-
231
- $youMustUpgradePHP = Piwik::translate('General_YouMustUpgradePhpVersionToReceiveLatestPiwik');
232
- $message = Piwik::translate('General_PiwikCannotBeUpgradedBecausePhpIsTooOld')
233
- . ' '
234
- . sprintf(Piwik::translate('General_PleaseUpgradeYourPhpVersionSoYourPiwikDataStaysSecure'), self::getNextRequiredMinimumPHP())
235
- ;
236
-
237
- $notification = new Notification($message);
238
- $notification->title = $youMustUpgradePHP;
239
- $notification->priority = Notification::PRIORITY_LOW;
240
- $notification->context = Notification::CONTEXT_WARNING;
241
- $notification->type = Notification::TYPE_TRANSIENT;
242
- $notification->flags = Notification::FLAG_NO_CLEAR;
243
- NotificationManager::notify('PHPVersionTooOldForNewestPiwikCheck', $notification);
244
  }
245
 
246
  private static function notifyWhenPhpVersionIsEOL()
207
  }
208
 
209
  /**
210
+ * PHP Version required by the next major Matomo version
211
  * @return string
212
  */
213
  private static function getNextRequiredMinimumPHP()
214
  {
215
+ return '7.2';
216
  }
217
 
218
  private static function isUsingPhpVersionCompatibleWithNextPiwik()
222
 
223
  private static function notifyWhenPhpVersionIsNotCompatibleWithNextMajorPiwik()
224
  {
225
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  }
227
 
228
  private static function notifyWhenPhpVersionIsEOL()
app/core/RankingQuery.php CHANGED
@@ -220,12 +220,15 @@ class RankingQuery
220
  * has to be specified in this query. {@link RankingQuery} cannot apply ordering
221
  * itself.
222
  * @param $bind array Bindings for the inner query.
 
223
  * @return array The format depends on which methods have been used
224
  * to configure the ranking query.
225
  */
226
- public function execute($innerQuery, $bind = array())
227
  {
228
  $query = $this->generateRankingQuery($innerQuery);
 
 
229
  $data = Db::getReader()->fetchAll($query, $bind);
230
 
231
  if ($this->columnToMarkExcludedRows !== false) {
220
  * has to be specified in this query. {@link RankingQuery} cannot apply ordering
221
  * itself.
222
  * @param $bind array Bindings for the inner query.
223
+ * @param int $timeLimitInMs Adds a MAX_EXECUTION_TIME query hint to the query if $timeLimitInMs > 0
224
  * @return array The format depends on which methods have been used
225
  * to configure the ranking query.
226
  */
227
+ public function execute($innerQuery, $bind = array(), $timeLimitInMs = 0)
228
  {
229
  $query = $this->generateRankingQuery($innerQuery);
230
+ $query = DbHelper::addMaxExecutionTimeHintToQuery($query, $timeLimitInMs);
231
+
232
  $data = Db::getReader()->fetchAll($query, $bind);
233
 
234
  if ($this->columnToMarkExcludedRows !== false) {
app/core/Session.php CHANGED
@@ -222,7 +222,7 @@ class Session extends Zend_Session
222
  {
223
  $headerStr = 'Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value);
224
  if ($expires) {
225
- $headerStr .= '; expires=' . $expires;
226
  }
227
  if ($path) {
228
  $headerStr .= '; path=' . $path;
222
  {
223
  $headerStr = 'Set-Cookie: ' . rawurlencode($name) . '=' . rawurlencode($value);
224
  if ($expires) {
225
+ $headerStr .= '; expires=' . gmdate('D, d-M-Y H:i:s', $expires) . ' GMT';
226
  }
227
  if ($path) {
228
  $headerStr .= '; path=' . $path;
app/core/Tracker/GoalManager.php CHANGED
@@ -665,6 +665,10 @@ class GoalManager
665
 
666
  protected function getItemRowEnriched($goal, $item)
667
  {
 
 
 
 
668
  $newRow = array(
669
  'idaction_sku' => (int)$item[self::INTERNAL_ITEM_SKU],
670
  'idaction_name' => (int)$item[self::INTERNAL_ITEM_NAME],
@@ -879,8 +883,9 @@ class GoalManager
879
  {
880
  $lastVisitTime = $visitProperties->getProperty('visit_last_action_time');
881
  if (!$lastVisitTime) {
882
- $lastVisitTime = $request->getCurrentTimestamp();
883
  }
 
884
  $goal = array(
885
  'idvisit' => $visitProperties->getProperty('idvisit'),
886
  'idvisitor' => $visitProperties->getProperty('idvisitor'),
665
 
666
  protected function getItemRowEnriched($goal, $item)
667
  {
668
+ if (empty($goal['server_time'])) {
669
+ // hack to test #215
670
+ $goal['server_time'] = Date::getDatetimeFromTimestamp(time());
671
+ }
672
  $newRow = array(
673
  'idaction_sku' => (int)$item[self::INTERNAL_ITEM_SKU],
674
  'idaction_name' => (int)$item[self::INTERNAL_ITEM_NAME],
883
  {
884
  $lastVisitTime = $visitProperties->getProperty('visit_last_action_time');
885
  if (!$lastVisitTime) {
886
+ $lastVisitTime = $request->getCurrentTimestamp();
887
  }
888
+
889
  $goal = array(
890
  'idvisit' => $visitProperties->getProperty('idvisit'),
891
  'idvisitor' => $visitProperties->getProperty('idvisitor'),
app/core/Tracker/Model.php CHANGED
@@ -16,6 +16,7 @@ use Psr\Log\LoggerInterface;
16
 
17
  class Model
18
  {
 
19
 
20
  public function createAction($visitAction)
21
  {
@@ -70,10 +71,10 @@ class Model
70
  $sqlBind[] = $value;
71
  }
72
 
73
- $parts = implode($updateParts, ', ');
74
  $table = Common::prefixTable('log_conversion');
75
 
76
- $sql = "UPDATE $table SET $parts WHERE " . implode($updateWhereParts, ' AND ');
77
 
78
  try {
79
  $this->getDb()->query($sql, $sqlBind);
@@ -285,7 +286,7 @@ class Model
285
  $sqlBind[] = $value;
286
  }
287
 
288
- $parts = implode($updateParts, ', ');
289
  $table = Common::prefixTable('log_conversion_item');
290
 
291
  $sql = "UPDATE $table SET $parts WHERE idvisit = ? AND idorder = ? AND idaction_sku = ?";
@@ -418,7 +419,13 @@ class Model
418
 
419
  private function findVisitorByVisitorId($idVisitor, $select, $from, $where, $bindSql)
420
  {
421
- // will use INDEX index_idsite_idvisitor (idsite, idvisitor)
 
 
 
 
 
 
422
  $where .= ' AND idvisitor = ?';
423
  $bindSql[] = $idVisitor;
424
 
16
 
17
  class Model
18
  {
19
+ const CACHE_KEY_INDEX_IDSITE_IDVISITOR = 'log_visit_has_index_idsite_idvisitor';
20
 
21
  public function createAction($visitAction)
22
  {
71
  $sqlBind[] = $value;
72
  }
73
 
74
+ $parts = implode(', ', $updateParts);
75
  $table = Common::prefixTable('log_conversion');
76
 
77
+ $sql = "UPDATE $table SET $parts WHERE " . implode(' AND ', $updateWhereParts);
78
 
79
  try {
80
  $this->getDb()->query($sql, $sqlBind);
286
  $sqlBind[] = $value;
287
  }
288
 
289
+ $parts = implode(', ', $updateParts);
290
  $table = Common::prefixTable('log_conversion_item');
291
 
292
  $sql = "UPDATE $table SET $parts WHERE idvisit = ? AND idorder = ? AND idaction_sku = ?";
419
 
420
  private function findVisitorByVisitorId($idVisitor, $select, $from, $where, $bindSql)
421
  {
422
+ $cache = Cache::getCacheGeneral();
423
+
424
+ // use INDEX index_idsite_idvisitor (idsite, idvisitor) if available
425
+ if (array_key_exists(self::CACHE_KEY_INDEX_IDSITE_IDVISITOR, $cache) && true === $cache[self::CACHE_KEY_INDEX_IDSITE_IDVISITOR]) {
426
+ $from .= ' FORCE INDEX (index_idsite_idvisitor) ';
427
+ }
428
+
429
  $where .= ' AND idvisitor = ?';
430
  $bindSql[] = $idVisitor;
431
 
app/core/Tracker/Request.php CHANGED
@@ -20,6 +20,7 @@ use Piwik\Network\IPUtils;
20
  use Piwik\Piwik;
21
  use Piwik\Plugins\CustomVariables\CustomVariables;
22
  use Piwik\Plugins\UsersManager\UsersManager;
 
23
  use Piwik\Tracker;
24
  use Piwik\Cache as PiwikCache;
25
 
@@ -686,7 +687,12 @@ class Request
686
  $cookie = $this->makeThirdPartyCookieUID();
687
  $idVisitor = bin2hex($idVisitor);
688
  $cookie->set(0, $idVisitor);
689
- $cookie->save('None');
 
 
 
 
 
690
 
691
  Common::printDebug(sprintf("We set the visitor ID to %s in the 3rd party cookie...", $idVisitor));
692
  }
20
  use Piwik\Piwik;
21
  use Piwik\Plugins\CustomVariables\CustomVariables;
22
  use Piwik\Plugins\UsersManager\UsersManager;
23
+ use Piwik\ProxyHttp;
24
  use Piwik\Tracker;
25
  use Piwik\Cache as PiwikCache;
26
 
687
  $cookie = $this->makeThirdPartyCookieUID();
688
  $idVisitor = bin2hex($idVisitor);
689
  $cookie->set(0, $idVisitor);
690
+ if (ProxyHttp::isHttps()) {
691
+ $cookie->setSecure(true);
692
+ $cookie->save('None');
693
+ } else {
694
+ $cookie->save('Lax');
695
+ }
696
 
697
  Common::printDebug(sprintf("We set the visitor ID to %s in the 3rd party cookie...", $idVisitor));
698
  }
app/core/Tracker/Visit.php CHANGED
@@ -572,10 +572,10 @@ class Visit implements VisitInterface
572
  $date = Date::factory((int)$time, $timezone);
573
 
574
  // $date->isToday() is buggy when server and website timezones don't match - so we'll do our own checking
575
- $startOfTomorrow = Date::factoryInTimezone('today', $timezone)->addDay(1);
576
- $isLaterThanToday = $date->getTimestamp() >= $startOfTomorrow->getTimestamp();
577
- if ($isLaterThanToday) {
578
- return;
579
  }
580
 
581
  $this->invalidator->rememberToInvalidateArchivedReportsLater($idSite, $date);
572
  $date = Date::factory((int)$time, $timezone);
573
 
574
  // $date->isToday() is buggy when server and website timezones don't match - so we'll do our own checking
575
+ $startOfToday = Date::factoryInTimezone('yesterday', $timezone)->addDay(1);
576
+ $isLaterThanYesterday = $date->getTimestamp() >= $startOfToday->getTimestamp();
577
+ if ($isLaterThanYesterday) {
578
+ return; // don't try to invalidate archives for today or later
579
  }
580
 
581
  $this->invalidator->rememberToInvalidateArchivedReportsLater($idSite, $date);
app/core/Twig.php CHANGED
@@ -362,7 +362,7 @@ class Twig
362
  if (!empty($options['raw'])) {
363
  $template .= piwik_fix_lbrace($message);
364
  } else {
365
- $template .= twig_escape_filter($twigEnv, $message, 'html');
366
  }
367
 
368
  $template .= '</div>';
362
  if (!empty($options['raw'])) {
363
  $template .= piwik_fix_lbrace($message);
364
  } else {
365
+ $template .= piwik_escape_filter($twigEnv, $message, 'html');
366
  }
367
 
368
  $template .= '</div>';
app/core/Updates/3.13.4-b1.php ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Piwik - 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
+ *
8
+ */
9
+
10
+ namespace Piwik\Updates;
11
+
12
+ use Piwik\Plugins\Installation\ServerFilesGenerator;
13
+ use Piwik\SettingsServer;
14
+ use Piwik\Updater;
15
+ use Piwik\Updates as PiwikUpdates;
16
+
17
+ class Updates_3_13_4_b1 extends PiwikUpdates
18
+ {
19
+ public function doUpdate(Updater $updater)
20
+ {
21
+ // Fix issue with HeatmapSessionRecording on IIS (https://github.com/matomo-org/matomo/issues/15651)
22
+ if (SettingsServer::isIIS()) {
23
+ ServerFilesGenerator::createFilesForSecurity();
24
+ }
25
+ }
26
+ }
app/core/Version.php CHANGED
@@ -20,7 +20,7 @@ final class Version
20
  * The current Matomo version.
21
  * @var string
22
  */
23
- const VERSION = '3.13.3';
24
 
25
  public function isStableVersion($version)
26
  {
20
  * The current Matomo version.
21
  * @var string
22
  */
23
+ const VERSION = '3.13.4';
24
 
25
  public function isStableVersion($version)
26
  {
app/core/View.php CHANGED
@@ -287,6 +287,9 @@ class View implements ViewInterface
287
  // don't send Referer-Header for outgoing links
288
  if (!empty($this->useStrictReferrerPolicy)) {
289
  Common::sendHeader('Referrer-Policy: same-origin');
 
 
 
290
  }
291
 
292
  return $this->renderTwigTemplate();
287
  // don't send Referer-Header for outgoing links
288
  if (!empty($this->useStrictReferrerPolicy)) {
289
  Common::sendHeader('Referrer-Policy: same-origin');
290
+ } else {
291
+ // always send explicit default header
292
+ Common::sendHeader('Referrer-Policy: no-referrer-when-downgrade');
293
  }
294
 
295
  return $this->renderTwigTemplate();
app/lang/en.json CHANGED
@@ -2983,6 +2983,7 @@
2983
  "AllowedUploadFormats": "You may upload a plugin or theme in .zip format via this page.",
2984
  "Authors": "Authors",
2985
  "Browse": "Browse",
 
2986
  "LatestMarketplaceUpdates": "Latest Marketplace Updates",
2987
  "BackToMarketplace": "Back to Marketplace",
2988
  "BrowseMarketplace": "Browse Marketplace",
@@ -4377,7 +4378,9 @@
4377
  "UpdaterWasLastRun": "The updater was last run on %s.",
4378
  "UpdaterWillRunNext": "It is next scheduled to run on %s.",
4379
  "WidgetLocation": "Visitor Location",
4380
- "InstallGeoIp2": "Matomo recommends using the %1$sdbip%2$s databases, but this requires using the GeoIp2 plugin. If you install and activate it (which will require a possibly long running update), you will be able to get started using %1$sdbip%2$s databases. (Ignore this if you are using a third party plugin that provides it's own geolocation functionality.)"
 
 
4381
  },
4382
  "UserCountryMap": {
4383
  "PluginDescription": "This plugin provides the widgets Visitor Map and Real-time Map. Note: Requires the UserCountry plugin enabled.",
2983
  "AllowedUploadFormats": "You may upload a plugin or theme in .zip format via this page.",
2984
  "Authors": "Authors",
2985
  "Browse": "Browse",
2986
+ "SupportMatomoThankYou": "Any purchase will help fund the future of the Matomo open-source project. Thank you for your support!",
2987
  "LatestMarketplaceUpdates": "Latest Marketplace Updates",
2988
  "BackToMarketplace": "Back to Marketplace",
2989
  "BrowseMarketplace": "Browse Marketplace",
4378
  "UpdaterWasLastRun": "The updater was last run on %s.",
4379
  "UpdaterWillRunNext": "It is next scheduled to run on %s.",
4380
  "WidgetLocation": "Visitor Location",
4381
+ "InstallGeoIp2": "Matomo recommends using the %1$sdbip%2$s databases, but this requires using the GeoIp2 plugin. If you install and activate it (which will require a possibly long running update), you will be able to get started using %1$sdbip%2$s databases. (Ignore this if you are using a third party plugin that provides it's own geolocation functionality.)",
4382
+ "GeoIpDbIpAccuracyNote": "Note: the DBIP databases are free and can be downloaded automatically, but geolocation results (specifically city results) are not as accurate as MaxMind's. MaxMind, however, requires that you create an account even for the free database. If you want to use MaxMind's geolocation database, you can start the process %1$shere%2$s",
4383
+ "MaxMindLinkExplanation": "If you are using MaxMind's geolocation databases and you do not already know how to generate your download URL, %1$sclick here to learn how%2$s."
4384
  },
4385
  "UserCountryMap": {
4386
  "PluginDescription": "This plugin provides the widgets Visitor Map and Real-time Map. Note: Requires the UserCountry plugin enabled.",
app/plugins/CoreAdminHome/Tasks.php CHANGED
@@ -9,11 +9,14 @@
9
  namespace Piwik\Plugins\CoreAdminHome;
10
 
11
  use Piwik\API\Request;
 
 
12
  use Piwik\ArchiveProcessor\Rules;
13
  use Piwik\Archive\ArchivePurger;
14
  use Piwik\Common;
15
  use Piwik\Config;
16
  use Piwik\Container\StaticContainer;
 
17
  use Piwik\DataAccess\ArchiveTableCreator;
18
  use Piwik\Date;
19
  use Piwik\Db;
@@ -60,6 +63,10 @@ class Tasks extends \Piwik\Plugin\Tasks
60
 
61
  public function schedule()
62
  {
 
 
 
 
63
  // general data purge on older archive tables, executed daily
64
  $this->daily('purgeOutdatedArchives', null, self::HIGH_PRIORITY);
65
 
@@ -81,6 +88,17 @@ class Tasks extends \Piwik\Plugin\Tasks
81
  $this->scheduleTrackingCodeReminderChecks();
82
  }
83
 
 
 
 
 
 
 
 
 
 
 
 
84
  private function scheduleTrackingCodeReminderChecks()
85
  {
86
  $daysToTrackedVisitsCheck = (int) Config::getInstance()->General['num_days_before_tracking_code_reminder'];
9
  namespace Piwik\Plugins\CoreAdminHome;
10
 
11
  use Piwik\API\Request;
12
+ use Piwik\Archive;
13
+ use Piwik\Archive\ArchiveInvalidator;
14
  use Piwik\ArchiveProcessor\Rules;
15
  use Piwik\Archive\ArchivePurger;
16
  use Piwik\Common;
17
  use Piwik\Config;
18
  use Piwik\Container\StaticContainer;
19
+ use Piwik\CronArchive;
20
  use Piwik\DataAccess\ArchiveTableCreator;
21
  use Piwik\Date;
22
  use Piwik\Db;
63
 
64
  public function schedule()
65
  {
66
+ // for browser triggered archiving, make sure we invalidate archives once a day just to make
67
+ // sure all archives that need to be invalidated get invalidated
68
+ $this->daily('invalidateOutdatedArchives', null, self::HIGH_PRIORITY);
69
+
70
  // general data purge on older archive tables, executed daily
71
  $this->daily('purgeOutdatedArchives', null, self::HIGH_PRIORITY);
72
 
88
  $this->scheduleTrackingCodeReminderChecks();
89
  }
90
 
91
+ public function invalidateOutdatedArchives()
92
+ {
93
+ if (!Rules::isBrowserTriggerEnabled()) {
94
+ $this->logger->info("Browser triggered archiving disabled, archives will be invalidated during core:archive.");
95
+ return;
96
+ }
97
+
98
+ $cronArchive = new CronArchive();
99
+ $cronArchive->invalidateArchivedReportsForSitesThatNeedToBeArchivedAgain();
100
+ }
101
+
102
  private function scheduleTrackingCodeReminderChecks()
103
  {
104
  $daysToTrackedVisitsCheck = (int) Config::getInstance()->General['num_days_before_tracking_code_reminder'];
app/plugins/CoreAdminHome/templates/optOut.twig CHANGED
@@ -33,7 +33,14 @@
33
  #}
34
  {% if showConfirmOnly %}
35
  <p>{{ 'CoreAdminHome_OptingYouOut'|translate }}</p>
36
- <script>window.close();</script>
 
 
 
 
 
 
 
37
  <noscript>
38
  {% endif %}
39
 
33
  #}
34
  {% if showConfirmOnly %}
35
  <p>{{ 'CoreAdminHome_OptingYouOut'|translate }}</p>
36
+ <script>
37
+ {# try to update nonce in iframe, so sending it a second time works #}
38
+ try {
39
+ window.opener.document.querySelector('[name="nonce"]').value = '{{ nonce }}';
40
+ window.opener.document.querySelector('form').action = window.opener.document.querySelector('form').action.replace(/nonce=[0-9a-z]+/, 'nonce={{ nonce }}');
41
+ } catch (e) {}
42
+ window.close();
43
+ </script>
44
  <noscript>
45
  {% endif %}
46
 
app/plugins/CoreHome/CoreHome.php CHANGED
@@ -11,13 +11,16 @@ namespace Piwik\Plugins\CoreHome;
11
  use Piwik\Archive\ArchiveInvalidator;
12
  use Piwik\Columns\ComputedMetricFactory;
13
  use Piwik\Columns\MetricsList;
 
14
  use Piwik\Container\StaticContainer;
 
15
  use Piwik\IP;
16
  use Piwik\Piwik;
17
  use Piwik\Plugin\ArchivedMetric;
18
  use Piwik\Plugin\ComputedMetric;
19
  use Piwik\Plugin\ThemeStyles;
20
  use Piwik\SettingsServer;
 
21
 
22
  /**
23
  *
@@ -59,6 +62,9 @@ class CoreHome extends \Piwik\Plugin
59
  /** @var ArchiveInvalidator $archiveInvalidator */
60
  $archiveInvalidator = StaticContainer::get(ArchiveInvalidator::class);
61
  $cacheGeneral[ArchiveInvalidator::TRACKER_CACHE_KEY] = $archiveInvalidator->getAllRememberToInvalidateArchivedReportsLater();
 
 
 
62
  }
63
 
64
  public function addStylesheets(&$mergedContent)
11
  use Piwik\Archive\ArchiveInvalidator;
12
  use Piwik\Columns\ComputedMetricFactory;
13
  use Piwik\Columns\MetricsList;
14
+ use Piwik\Common;
15
  use Piwik\Container\StaticContainer;
16
+ use Piwik\DbHelper;
17
  use Piwik\IP;
18
  use Piwik\Piwik;
19
  use Piwik\Plugin\ArchivedMetric;
20
  use Piwik\Plugin\ComputedMetric;
21
  use Piwik\Plugin\ThemeStyles;
22
  use Piwik\SettingsServer;
23
+ use Piwik\Tracker\Model as TrackerModel;
24
 
25
  /**
26
  *
62
  /** @var ArchiveInvalidator $archiveInvalidator */
63
  $archiveInvalidator = StaticContainer::get(ArchiveInvalidator::class);
64
  $cacheGeneral[ArchiveInvalidator::TRACKER_CACHE_KEY] = $archiveInvalidator->getAllRememberToInvalidateArchivedReportsLater();
65
+
66
+ $hasIndex = DbHelper::tableHasIndex(Common::prefixTable('log_visit'), 'index_idsite_idvisitor');
67
+ $cacheGeneral[TrackerModel::CACHE_KEY_INDEX_IDSITE_IDVISITOR] = $hasIndex;
68
  }
69
 
70
  public function addStylesheets(&$mergedContent)
app/plugins/Dashboard/Dashboard.php CHANGED
@@ -155,11 +155,7 @@ class Dashboard extends \Piwik\Plugin
155
  if ($advertising->areAdsForProfessionalServicesEnabled() && Plugin\Manager::getInstance()->isPluginActivated('ProfessionalServices')) {
156
  $advertisingWidget = '{"uniqueId":"widgetProfessionalServicespromoServices","parameters":{"module":"ProfessionalServices","action":"promoServices"}},';
157
  }
158
- if (Piwik::hasUserSuperUserAccess()) {
159
- $piwikPromoWidget = '{"uniqueId":"widgetCoreHomegetDonateForm","parameters":{"module":"CoreHome","action":"getDonateForm"}}';
160
- } else {
161
- $piwikPromoWidget = '{"uniqueId":"widgetCoreHomegetPromoVideo","parameters":{"module":"CoreHome","action":"getPromoVideo"}}';
162
- }
163
  $defaultLayout = '[
164
  [
165
  {"uniqueId":"widgetLivewidget","parameters":{"module":"Live","action":"widget"}},
155
  if ($advertising->areAdsForProfessionalServicesEnabled() && Plugin\Manager::getInstance()->isPluginActivated('ProfessionalServices')) {
156
  $advertisingWidget = '{"uniqueId":"widgetProfessionalServicespromoServices","parameters":{"module":"ProfessionalServices","action":"promoServices"}},';
157
  }
158
+ $piwikPromoWidget = '{"uniqueId":"widgetCoreHomegetPromoVideo","parameters":{"module":"CoreHome","action":"getPromoVideo"}}';
 
 
 
 
159
  $defaultLayout = '[
160
  [
161
  {"uniqueId":"widgetLivewidget","parameters":{"module":"Live","action":"widget"}},
app/plugins/Goals/Controller.php CHANGED
@@ -24,17 +24,12 @@ use Piwik\View;
24
  */
25
  class Controller extends \Piwik\Plugin\Controller
26
  {
27
- const CONVERSION_RATE_PRECISION = 1;
28
-
29
  /**
30
  * Number of "Your top converting keywords/etc are" to display in the per Goal overview page
31
  * @var int
32
  */
33
  const COUNT_TOP_ROWS_TO_DISPLAY = 3;
34
 
35
- const ECOMMERCE_LOG_SHOW_ORDERS = 1;
36
- const ECOMMERCE_LOG_SHOW_ABANDONED_CARTS = 2;
37
-
38
  protected $goalColumnNameToLabel = array(
39
  'avg_order_revenue' => 'General_AverageOrderValue',
40
  'nb_conversions' => 'Goals_ColumnConversions',
@@ -59,10 +54,6 @@ class Controller extends \Piwik\Plugin\Controller
59
  }
60
  }
61
 
62
- if (!is_numeric($conversionRate)) {
63
- $conversionRate = sprintf('%.' . self::CONVERSION_RATE_PRECISION . 'f%%', $conversionRate);
64
- }
65
-
66
  return $conversionRate;
67
  }
68
 
24
  */
25
  class Controller extends \Piwik\Plugin\Controller
26
  {
 
 
27
  /**
28
  * Number of "Your top converting keywords/etc are" to display in the per Goal overview page
29
  * @var int
30
  */
31
  const COUNT_TOP_ROWS_TO_DISPLAY = 3;
32
 
 
 
 
33
  protected $goalColumnNameToLabel = array(
34
  'avg_order_revenue' => 'General_AverageOrderValue',
35
  'nb_conversions' => 'Goals_ColumnConversions',
54
  }
55
  }
56
 
 
 
 
 
57
  return $conversionRate;
58
  }
59
 
app/plugins/Installation/ServerFilesGenerator.php CHANGED
@@ -167,6 +167,12 @@ Header set Cache-Control \"Cache-Control: private, no-cache, no-store\"
167
  '/vendor',
168
  '/plugins',
169
  );
 
 
 
 
 
 
170
  foreach ($directoriesToProtect as $directoryToProtect) {
171
  @file_put_contents(PIWIK_INCLUDE_PATH . $directoryToProtect . '/web.config',
172
  '<?xml version="1.0" encoding="UTF-8"?>
@@ -176,7 +182,7 @@ Header set Cache-Control \"Cache-Control: private, no-cache, no-store\"
176
  <requestFiltering>
177
  <denyUrlSequences>
178
  <add sequence=".php" />
179
- </denyUrlSequences>
180
  </requestFiltering>
181
  </security>
182
  </system.webServer>
167
  '/vendor',
168
  '/plugins',
169
  );
170
+
171
+ $additionForPlugins = '
172
+ <alwaysAllowedUrls>
173
+ <add url="/plugins/HeatmapSessionRecording/configs.php" />
174
+ </alwaysAllowedUrls>';
175
+
176
  foreach ($directoriesToProtect as $directoryToProtect) {
177
  @file_put_contents(PIWIK_INCLUDE_PATH . $directoryToProtect . '/web.config',
178
  '<?xml version="1.0" encoding="UTF-8"?>
182
  <requestFiltering>
183
  <denyUrlSequences>
184
  <add sequence=".php" />
185
+ </denyUrlSequences>' . ($directoryToProtect === '/plugins' ? $additionForPlugins : '') . '
186
  </requestFiltering>
187
  </security>
188
  </system.webServer>
app/plugins/Live/Model.php CHANGED
@@ -16,6 +16,7 @@ use Piwik\Config;
16
  use Piwik\Container\StaticContainer;
17
  use Piwik\Date;
18
  use Piwik\Db;
 
19
  use Piwik\Period;
20
  use Piwik\Period\Range;
21
  use Piwik\Piwik;
@@ -121,7 +122,7 @@ class Model
121
  try {
122
  $visits = $readerDb->fetchAll($sql, $bind);
123
  } catch (Exception $e) {
124
- $this->handleMaxExecutionTimeError($readerDb, $e, $sql, $bind, $segment, $dateStart, $dateEnd, $minTimestamp, $limit);
125
  throw $e;
126
  }
127
  return $visits;
@@ -130,17 +131,16 @@ class Model
130
  /**
131
  * @param \Piwik\Tracker\Db|\Piwik\Db\AdapterInterface|\Piwik\Db $readerDb
132
  * @param Exception $e
133
- * @param $sql
134
- * @param array $bind
135
  * @param $segment
136
  * @param $dateStart
137
  * @param $dateEnd
138
  * @param $minTimestamp
139
  * @param $limit
 
140
  *
141
  * @throws MaxExecutionTimeExceededException
142
  */
143
- public function handleMaxExecutionTimeError($readerDb, $e, $sql, $bind, $segment, $dateStart, $dateEnd, $minTimestamp, $limit)
144
  {
145
  // we also need to check for the 'maximum statement execution time exceeded' text as the query might be
146
  // aborted at different stages and we can't really know all the possible codes at which it may be aborted etc
@@ -148,39 +148,41 @@ class Model
148
  || $readerDb->isErrNo($e, DbMigration::ERROR_CODE_MAX_EXECUTION_TIME_EXCEEDED_SORT_ABORTED)
149
  || strpos($e->getMessage(), 'maximum statement execution time exceeded') !== false;
150
 
151
- if ($isMaxExecutionTimeError) {
152
- $message = '';
 
 
 
153
 
154
- if ($this->isLookingAtMoreThanOneDay($dateStart, $dateEnd, $minTimestamp)) {
155
- $message .= ' ' . Piwik::translate('Live_QueryMaxExecutionTimeExceededReasonDateRange');
156
- }
157
 
158
- if (!empty($segment)) {
159
- $message .= ' ' . Piwik::translate('Live_QueryMaxExecutionTimeExceededReasonSegment');
160
- }
161
 
162
- $limitThatCannotBeSelectedInUiButOnlyApi = 550;
163
- if ($limit > $limitThatCannotBeSelectedInUiButOnlyApi) {
164
- $message .= ' ' . Piwik::translate('Live_QueryMaxExecutionTimeExceededLimit');
165
- }
166
 
167
- if (empty($message)) {
168
- $message .= ' ' . Piwik::translate('Live_QueryMaxExecutionTimeExceededReasonUnknown');
169
- }
170
 
171
- $message = Piwik::translate('Live_QueryMaxExecutionTimeExceeded') . ' ' . $message;
172
 
173
- $params = array(
174
- 'sql' => $sql, 'bind' => $bind, 'segment' => $segment, 'limit' => $limit
175
- );
176
 
177
- /**
178
- * @ignore
179
- * @internal
180
- */
181
- Piwik::postEvent('Live.queryMaxExecutionTimeExceeded', array($params));
182
- throw new MaxExecutionTimeExceededException($message);
183
- }
184
  }
185
 
186
  /**
@@ -190,7 +192,7 @@ class Model
190
  * @return bool
191
  * @throws Exception
192
  */
193
- public function isLookingAtMoreThanOneDay($dateStart, $dateEnd, $minTimestamp)
194
  {
195
  if (!$dateStart) {
196
  if (!$minTimestamp) {
@@ -493,35 +495,14 @@ class Model
493
 
494
  $bind = $innerQuery['bind'];
495
 
496
- $maxExecutionTimeHint = $this->getMaxExecutionTimeMySQLHint();
497
- if ($visitorId) {
498
  // for now let's not apply when looking for a specific visitor
499
- $maxExecutionTimeHint = '';
500
- }
501
- if ($maxExecutionTimeHint) {
502
- $innerQuery['sql'] = trim($innerQuery['sql']);
503
- $pos = stripos($innerQuery['sql'], 'SELECT');
504
- if ($pos !== false) {
505
- $innerQuery['sql'] = substr_replace($innerQuery['sql'], 'SELECT ' . $maxExecutionTimeHint, $pos, strlen('SELECT'));
506
- }
507
  }
508
 
509
  return array($innerQuery['sql'], $bind);
510
  }
511
 
512
- private function getMaxExecutionTimeMySQLHint()
513
- {
514
- $general = Config::getInstance()->General;
515
- $maxExecutionTime = $general['live_query_max_execution_time'];
516
- $maxExecutionTimeHint = '';
517
- if (is_numeric($maxExecutionTime) && $maxExecutionTime > 0) {
518
- $timeInMs = $maxExecutionTime * 1000;
519
- $timeInMs = (int) $timeInMs;
520
- $maxExecutionTimeHint = ' /*+ MAX_EXECUTION_TIME('.$timeInMs.') */ ';
521
- }
522
- return $maxExecutionTimeHint;
523
- }
524
-
525
  /**
526
  * @param $idSite
527
  * @return Site
16
  use Piwik\Container\StaticContainer;
17
  use Piwik\Date;
18
  use Piwik\Db;
19
+ use Piwik\DbHelper;
20
  use Piwik\Period;
21
  use Piwik\Period\Range;
22
  use Piwik\Piwik;
122
  try {
123
  $visits = $readerDb->fetchAll($sql, $bind);
124
  } catch (Exception $e) {
125
+ $this->handleMaxExecutionTimeError($readerDb, $e, $segment, $dateStart, $dateEnd, $minTimestamp, $limit, ['sql' => $sql, 'bind' => $bind,]);
126
  throw $e;
127
  }
128
  return $visits;
131
  /**
132
  * @param \Piwik\Tracker\Db|\Piwik\Db\AdapterInterface|\Piwik\Db $readerDb
133
  * @param Exception $e
 
 
134
  * @param $segment
135
  * @param $dateStart
136
  * @param $dateEnd
137
  * @param $minTimestamp
138
  * @param $limit
139
+ * @param $parameters
140
  *
141
  * @throws MaxExecutionTimeExceededException
142
  */
143
+ public static function handleMaxExecutionTimeError($readerDb, $e, $segment, $dateStart, $dateEnd, $minTimestamp, $limit, $parameters)
144
  {
145
  // we also need to check for the 'maximum statement execution time exceeded' text as the query might be
146
  // aborted at different stages and we can't really know all the possible codes at which it may be aborted etc
148
  || $readerDb->isErrNo($e, DbMigration::ERROR_CODE_MAX_EXECUTION_TIME_EXCEEDED_SORT_ABORTED)
149
  || strpos($e->getMessage(), 'maximum statement execution time exceeded') !== false;
150
 
151
+ if (false === $isMaxExecutionTimeError) {
152
+ return;
153
+ }
154
+
155
+ $message = '';
156
 
157
+ if (self::isLookingAtMoreThanOneDay($dateStart, $dateEnd, $minTimestamp)) {
158
+ $message .= ' ' . Piwik::translate('Live_QueryMaxExecutionTimeExceededReasonDateRange');
159
+ }
160
 
161
+ if (!empty($segment)) {
162
+ $message .= ' ' . Piwik::translate('Live_QueryMaxExecutionTimeExceededReasonSegment');
163
+ }
164
 
165
+ $limitThatCannotBeSelectedInUiButOnlyApi = 550;
166
+ if ($limit > $limitThatCannotBeSelectedInUiButOnlyApi) {
167
+ $message .= ' ' . Piwik::translate('Live_QueryMaxExecutionTimeExceededLimit');
168
+ }
169
 
170
+ if (empty($message)) {
171
+ $message .= ' ' . Piwik::translate('Live_QueryMaxExecutionTimeExceededReasonUnknown');
172
+ }
173
 
174
+ $message = Piwik::translate('Live_QueryMaxExecutionTimeExceeded') . ' ' . $message;
175
 
176
+ $params = array_merge($parameters, [
177
+ 'segment' => $segment, 'limit' => $limit
178
+ ]);
179
 
180
+ /**
181
+ * @ignore
182
+ * @internal
183
+ */
184
+ Piwik::postEvent('Live.queryMaxExecutionTimeExceeded', array($params));
185
+ throw new MaxExecutionTimeExceededException($message);
 
186
  }
187
 
188
  /**
192
  * @return bool
193
  * @throws Exception
194
  */
195
+ public static function isLookingAtMoreThanOneDay($dateStart, $dateEnd, $minTimestamp)
196
  {
197
  if (!$dateStart) {
198
  if (!$minTimestamp) {
495
 
496
  $bind = $innerQuery['bind'];
497
 
498
+ if (!$visitorId) {
 
499
  // for now let's not apply when looking for a specific visitor
500
+ $innerQuery['sql'] = DbHelper::addMaxExecutionTimeHintToQuery($innerQuery['sql'], Config::getInstance()->General['live_query_max_execution_time']);
 
 
 
 
 
 
 
501
  }
502
 
503
  return array($innerQuery['sql'], $bind);
504
  }
505
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  /**
507
  * @param $idSite
508
  * @return Site
app/plugins/Marketplace/Api/Client.php CHANGED
@@ -8,11 +8,14 @@
8
  */
9
  namespace Piwik\Plugins\Marketplace\Api;
10
 
 
11
  use Piwik\Cache;
12
  use Piwik\Common;
13
  use Piwik\Container\StaticContainer;
 
14
  use Piwik\Filesystem;
15
  use Piwik\Http;
 
16
  use Piwik\Plugin;
17
  use Piwik\Plugins\Marketplace\Environment;
18
  use Piwik\Plugins\Marketplace\Api\Service;
@@ -177,8 +180,9 @@ class Client
177
  }
178
 
179
  $params = array('plugins' => $params);
 
180
 
181
- $hasUpdates = $this->fetch('plugins/checkUpdates', array('plugins' => json_encode($params)));
182
 
183
  if (empty($hasUpdates)) {
184
  return array();
8
  */
9
  namespace Piwik\Plugins\Marketplace\Api;
10
 
11
+ use Piwik\API\Request;
12
  use Piwik\Cache;
13
  use Piwik\Common;
14
  use Piwik\Container\StaticContainer;
15
+ use Piwik\DataTable;
16
  use Piwik\Filesystem;
17
  use Piwik\Http;
18
+ use Piwik\Piwik;
19
  use Piwik\Plugin;
20
  use Piwik\Plugins\Marketplace\Environment;
21
  use Piwik\Plugins\Marketplace\Api\Service;
180
  }
181
 
182
  $params = array('plugins' => $params);
183
+ $params = array('plugins' => json_encode($params));
184
 
185
+ $hasUpdates = $this->fetch('plugins/checkUpdates', $params);
186
 
187
  if (empty($hasUpdates)) {
188
  return array();
app/plugins/Marketplace/templates/getPremiumFeatures.twig CHANGED
@@ -1,5 +1,8 @@
1
  <div class="getNewPlugins getPremiumFeatures widgetBody">
2
  <div class="row">
 
 
 
3
  {% for plugin in plugins %}
4
  <div class="col s12 m4">
5
 
1
  <div class="getNewPlugins getPremiumFeatures widgetBody">
2
  <div class="row">
3
+ <div class="col s12 m12">
4
+ <h3 style="margin-bottom: 28px;">{{ 'Marketplace_SupportMatomoThankYou'|translate }}</h3></div>
5
+
6
  {% for plugin in plugins %}
7
  <div class="col s12 m4">
8
 
app/plugins/Monolog/Processor/ExceptionToTextProcessor.php CHANGED
@@ -85,6 +85,10 @@ class ExceptionToTextProcessor
85
 
86
  public static function getWholeBacktrace(\Exception $exception, $shouldPrintBacktrace = true)
87
  {
 
 
 
 
88
  $message = "";
89
 
90
  $e = $exception;
85
 
86
  public static function getWholeBacktrace(\Exception $exception, $shouldPrintBacktrace = true)
87
  {
88
+ if (!$shouldPrintBacktrace) {
89
+ return $exception->getMessage();
90
+ }
91
+
92
  $message = "";
93
 
94
  $e = $exception;
app/plugins/Proxy/Controller.php CHANGED
@@ -11,6 +11,7 @@ namespace Piwik\Plugins\Proxy;
11
  use Piwik\AssetManager;
12
  use Piwik\AssetManager\UIAsset;
13
  use Piwik\Common;
 
14
  use Piwik\Piwik;
15
  use Piwik\ProxyHttp;
16
  use Piwik\Url;
@@ -33,7 +34,11 @@ class Controller extends \Piwik\Plugin\Controller
33
  */
34
  public function getCss()
35
  {
36
- $cssMergedFile = AssetManager::getInstance()->getMergedStylesheet();
 
 
 
 
37
  ProxyHttp::serverStaticFile($cssMergedFile->getAbsoluteLocation(), "text/css");
38
  }
39
 
11
  use Piwik\AssetManager;
12
  use Piwik\AssetManager\UIAsset;
13
  use Piwik\Common;
14
+ use Piwik\Exception\StylesheetLessCompileException;
15
  use Piwik\Piwik;
16
  use Piwik\ProxyHttp;
17
  use Piwik\Url;
34
  */
35
  public function getCss()
36
  {
37
+ try {
38
+ $cssMergedFile = AssetManager::getInstance()->getMergedStylesheet();
39
+ } catch (StylesheetLessCompileException $exception) {
40
+ $cssMergedFile = AssetManager::getInstance()->getMergedStylesheet();
41
+ }
42
  ProxyHttp::serverStaticFile($cssMergedFile->getAbsoluteLocation(), "text/css");
43
  }
44
 
app/plugins/TagManager/API/PreviewCookie.php CHANGED
@@ -28,13 +28,13 @@ class PreviewCookie extends Cookie
28
  public function enable($idSite, $idContainer)
29
  {
30
  $this->set($this->getCookieValueName($idSite, $idContainer), '1');
31
- $this->save();
32
  }
33
 
34
  public function disable($idSite, $idContainer)
35
  {
36
  $this->set($this->getCookieValueName($idSite, $idContainer), null);
37
- $this->save();
38
  }
39
 
40
  }
28
  public function enable($idSite, $idContainer)
29
  {
30
  $this->set($this->getCookieValueName($idSite, $idContainer), '1');
31
+ $this->save('Lax');
32
  }
33
 
34
  public function disable($idSite, $idContainer)
35
  {
36
  $this->set($this->getCookieValueName($idSite, $idContainer), null);
37
+ $this->save('Lax');
38
  }
39
 
40
  }
app/plugins/TagManager/Context/BaseContext.php CHANGED
@@ -228,7 +228,8 @@ abstract class BaseContext
228
  return strrpos($haystack, $needle, $offset);
229
  }
230
 
231
- protected function parameterToVariableJs($value, $container)
 
232
  {
233
  if (is_scalar($value) && preg_match_all('/{{.+?}}/', $value, $matches)) {
234
  $multiVars = [];
@@ -236,18 +237,18 @@ abstract class BaseContext
236
  $pos = 0;
237
 
238
  do {
239
- $start = $this->mb_strpos($value, '{{', $pos);
240
 
241
  $end = false;
242
  if ($start !== false) {
243
  // only if string contains a {{ we need to look to see if we find a matching end string
244
- $end = $this->mb_strpos($value, '}}', $start);
245
  }
246
 
247
  if ($end !== false) {
248
  // now this might seem random, but it is basically to detect if there are the brackets two times there
249
  // like "foo{{notExisting{{PageUrl}}" then we still detect "{{PageUrl}}"
250
- $start = $this->mb_strrpos(Common::mb_substr($value, 0, $end), '{{', $pos);
251
  }
252
 
253
  if ($start === false || $end === false) {
228
  return strrpos($haystack, $needle, $offset);
229
  }
230
 
231
+
232
+ protected function parameterToVariableJs($value, $container)
233
  {
234
  if (is_scalar($value) && preg_match_all('/{{.+?}}/', $value, $matches)) {
235
  $multiVars = [];
237
  $pos = 0;
238
 
239
  do {
240
+ $start = $this->mb_strpos($value, '{{', $pos);
241
 
242
  $end = false;
243
  if ($start !== false) {
244
  // only if string contains a {{ we need to look to see if we find a matching end string
245
+ $end = $this->mb_strpos($value, '}}', $start);
246
  }
247
 
248
  if ($end !== false) {
249
  // now this might seem random, but it is basically to detect if there are the brackets two times there
250
  // like "foo{{notExisting{{PageUrl}}" then we still detect "{{PageUrl}}"
251
+ $start = $this->mb_strpos(Common::mb_substr($value, 0, $end), '{{', $pos);
252
  }
253
 
254
  if ($start === false || $end === false) {
app/plugins/TagManager/Dao/VariablesDao.php CHANGED
@@ -33,7 +33,7 @@ class VariablesDao extends BaseDao implements TagManagerDao
33
  `created_date` DATETIME NOT NULL,
34
  `updated_date` DATETIME NOT NULL,
35
  `deleted_date` DATETIME NULL,
36
- PRIMARY KEY(`idvariable`), KEY(`idsite`), KEY (`idsite`, `idcontainerversion`)");
37
  // we cannot set a unique key on (`idsite`, `idcontainerversion`, `name`) because we soft delete tags and want to make sure names can be used again after deleting an entry
38
  }
39
 
33
  `created_date` DATETIME NOT NULL,
34
  `updated_date` DATETIME NOT NULL,
35
  `deleted_date` DATETIME NULL,
36
+ PRIMARY KEY(`idvariable`), KEY (`idsite`, `idcontainerversion`)");
37
  // we cannot set a unique key on (`idsite`, `idcontainerversion`, `name`) because we soft delete tags and want to make sure names can be used again after deleting an entry
38
  }
39
 
app/plugins/Transitions/API.php CHANGED
@@ -12,15 +12,18 @@ namespace Piwik\Plugins\Transitions;
12
  use Exception;
13
  use Piwik\ArchiveProcessor;
14
  use Piwik\Common;
 
15
  use Piwik\DataAccess\LogAggregator;
16
  use Piwik\DataArray;
17
  use Piwik\DataTable;
18
  use Piwik\DataTable\Row;
 
19
  use Piwik\Metrics;
20
  use Piwik\Period;
21
  use Piwik\Piwik;
22
  use Piwik\Plugins\Actions\Actions;
23
  use Piwik\Plugins\Actions\ArchivingHelper;
 
24
  use Piwik\RankingQuery;
25
  use Piwik\Segment;
26
  use Piwik\Segment\SegmentExpression;
@@ -81,25 +84,40 @@ class API extends \Piwik\Plugin\API
81
  'date' => Period\Factory::build($period->getLabel(), $date)->getLocalizedShortString()
82
  );
83
 
84
- $partsArray = explode(',', $parts);
85
- if ($parts == 'all' || in_array('internalReferrers', $partsArray)) {
86
- $this->addInternalReferrers($logAggregator, $report, $idaction, $actionType, $limitBeforeGrouping);
87
- }
88
- if ($parts == 'all' || in_array('followingActions', $partsArray)) {
89
- $includeLoops = $parts != 'all' && !in_array('internalReferrers', $partsArray);
90
- $this->addFollowingActions($logAggregator, $report, $idaction, $actionType, $limitBeforeGrouping, $includeLoops);
91
- }
92
- if ($parts == 'all' || in_array('externalReferrers', $partsArray)) {
93
- $this->addExternalReferrers($logAggregator, $report, $idaction, $actionType, $limitBeforeGrouping);
94
- }
 
95
 
96
- // derive the number of exits from the other metrics
97
- if ($parts == 'all') {
98
- $report['pageMetrics']['exits'] = $report['pageMetrics']['pageviews']
99
- - $this->getTotalTransitionsToFollowingActions()
100
- - $report['pageMetrics']['loops'];
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  }
102
 
 
103
  // replace column names in the data tables
104
  $reportNames = array(
105
  'previousPages' => true,
@@ -290,7 +308,8 @@ class API extends \Piwik\Plugin\API
290
  $metrics,
291
  $rankingQuery,
292
  $joinLogActionColumn,
293
- $secondaryOrderBy = "`name`"
 
294
  );
295
 
296
  $dataTables = $this->makeDataTablesFollowingActions($types, $data);
@@ -342,7 +361,7 @@ class API extends \Piwik\Plugin\API
342
  $where = 'visit_entry_idaction_' . $type . ' = ' . intval($idaction);
343
 
344
  $metrics = array(Metrics::INDEX_NB_VISITS);
345
- $data = $logAggregator->queryVisitsByDimension($dimensions, $where, $selects, $metrics, $rankingQuery);
346
 
347
  $referrerData = array();
348
  $referrerSubData = array();
@@ -433,7 +452,8 @@ class API extends \Piwik\Plugin\API
433
  $metrics,
434
  $rankingQuery,
435
  $joinLogActionOn,
436
- $secondaryOrderBy = "`name`"
 
437
  );
438
 
439
  $loops = 0;
12
  use Exception;
13
  use Piwik\ArchiveProcessor;
14
  use Piwik\Common;
15
+ use Piwik\Config;
16
  use Piwik\DataAccess\LogAggregator;
17
  use Piwik\DataArray;
18
  use Piwik\DataTable;
19
  use Piwik\DataTable\Row;
20
+ use Piwik\Db;
21
  use Piwik\Metrics;
22
  use Piwik\Period;
23
  use Piwik\Piwik;
24
  use Piwik\Plugins\Actions\Actions;
25
  use Piwik\Plugins\Actions\ArchivingHelper;
26
+ use Piwik\Plugins\Live\Model;
27
  use Piwik\RankingQuery;
28
  use Piwik\Segment;
29
  use Piwik\Segment\SegmentExpression;
84
  'date' => Period\Factory::build($period->getLabel(), $date)->getLocalizedShortString()
85
  );
86
 
87
+ try {
88
+ $partsArray = explode(',', $parts);
89
+ if ($parts == 'all' || in_array('internalReferrers', $partsArray)) {
90
+ $this->addInternalReferrers($logAggregator, $report, $idaction, $actionType, $limitBeforeGrouping);
91
+ }
92
+ if ($parts == 'all' || in_array('followingActions', $partsArray)) {
93
+ $includeLoops = $parts != 'all' && !in_array('internalReferrers', $partsArray);
94
+ $this->addFollowingActions($logAggregator, $report, $idaction, $actionType, $limitBeforeGrouping, $includeLoops);
95
+ }
96
+ if ($parts == 'all' || in_array('externalReferrers', $partsArray)) {
97
+ $this->addExternalReferrers($logAggregator, $report, $idaction, $actionType, $limitBeforeGrouping);
98
+ }
99
 
100
+ // derive the number of exits from the other metrics
101
+ if ($parts == 'all') {
102
+ $report['pageMetrics']['exits'] = $report['pageMetrics']['pageviews']
103
+ - $this->getTotalTransitionsToFollowingActions()
104
+ - $report['pageMetrics']['loops'];
105
+ }
106
+ } catch (\Exception $e) {
107
+ Model::handleMaxExecutionTimeError(
108
+ Db::getReader(),
109
+ $e,
110
+ $segment->getString(),
111
+ $period->getDateStart(),
112
+ $period->getDateEnd(),
113
+ 0,
114
+ Config::getInstance()->General['live_query_max_execution_time'],
115
+ ['method' => 'Transitions.getTransitionsForAction', 'actionName' => $actionName, 'actionType' => $actionType]
116
+ );
117
+ throw $e;
118
  }
119
 
120
+
121
  // replace column names in the data tables
122
  $reportNames = array(
123
  'previousPages' => true,
308
  $metrics,
309
  $rankingQuery,
310
  $joinLogActionColumn,
311
+ $secondaryOrderBy = "`name`",
312
+ Config::getInstance()->General['live_query_max_execution_time']
313
  );
314
 
315
  $dataTables = $this->makeDataTablesFollowingActions($types, $data);
361
  $where = 'visit_entry_idaction_' . $type . ' = ' . intval($idaction);
362
 
363
  $metrics = array(Metrics::INDEX_NB_VISITS);
364
+ $data = $logAggregator->queryVisitsByDimension($dimensions, $where, $selects, $metrics, $rankingQuery, false, Config::getInstance()->General['live_query_max_execution_time']);
365
 
366
  $referrerData = array();
367
  $referrerSubData = array();
452
  $metrics,
453
  $rankingQuery,
454
  $joinLogActionOn,
455
+ $secondaryOrderBy = "`name`",
456
+ Config::getInstance()->General['live_query_max_execution_time']
457
  );
458
 
459
  $loops = 0;
app/plugins/UserCountry/templates/_updaterManage.twig CHANGED
@@ -1,8 +1,9 @@
1
  <div ng-show="locationUpdater.geoipDatabaseInstalled" id="geoipdb-update-info">
2
  <p>
3
- {{ 'UserCountry_GeoIPUpdaterInstructions'|translate('<a href="http://www.maxmind.com/?rId=piwik">','</a>','<a rel="noreferrer noopener" href="https://db-ip.com/?refid=mtm">','</a>')|raw }}
4
  <br/><br/>
5
- {% if dbipLiteUrl|default is not empty %}{{ 'UserCountry_GeoLiteCityLink'|translate('<a rel="noreferrer noopener" href="'~dbipLiteUrl|e('html_attr')~'">',dbipLiteUrl|e('html'),'</a>')|raw }}{% endif %}
 
6
 
7
  <span ng-show="locationUpdater.geoipDatabaseInstalled">
8
  <br/><br/>{{ 'UserCountry_GeoIPUpdaterIntro'|translate }}:
1
  <div ng-show="locationUpdater.geoipDatabaseInstalled" id="geoipdb-update-info">
2
  <p>
3
+ {{ 'UserCountry_GeoIPUpdaterInstructions'|translate('<a href="http://www.maxmind.com/?rId=piwik" rel="noreferrer noopener">','</a>','<a rel="noreferrer noopener" href="https://db-ip.com/?refid=mtm">','</a>')|raw }}
4
  <br/><br/>
5
+ {% if dbipLiteUrl|default is not empty %}{{ 'UserCountry_GeoLiteCityLink'|translate('<a rel="noreferrer noopener" href="'~dbipLiteUrl|e('html_attr')~'">',dbipLiteUrl|e('html'),'</a>')|raw }}<br/><br/>{% endif %}
6
+ {{ 'UserCountry_MaxMindLinkExplanation'|translate('<a href="https://matomo.org/faq/how-to/how-do-i-get-the-geolocation-download-url-for-the-free-maxmind-db/" rel="noreferrer noopener" target="_blank">', '</a>')|raw }}
7
 
8
  <span ng-show="locationUpdater.geoipDatabaseInstalled">
9
  <br/><br/>{{ 'UserCountry_GeoIPUpdaterIntro'|translate }}:
app/plugins/UserCountry/templates/adminIndex.twig CHANGED
@@ -19,7 +19,7 @@
19
  {% if dbipLiteUrl|default is not empty %}
20
  <p>{{ 'UserCountry_HowToSetupGeoIPIntro'|translate }}</p>
21
  <ul style="list-style:disc !important;margin-left:2em;">
22
- <li style="list-style-type: disc !important;">{{ 'UserCountry_HowToSetupGeoIP_Step1'|translate('<a rel="noreferrer noopener" href="'~dbipLiteUrl~'">','</a>','<a rel="noreferrer noopener" target="_blank" href="http://db-ip.com/?refid=mtm">','</a>')|raw }}</li>
23
  <li style="list-style-type: disc !important;">{{ 'UserCountry_HowToSetupGeoIP_Step2'|translate("'"~dbipLiteFilename~"'",'<strong>','</strong>','<strong>'~dbipLiteDesiredFilename~'</strong>')|raw }}</li>
24
  <li style="list-style-type: disc !important;">{{ 'UserCountry_HowToSetupGeoIP_Step3'|translate('<strong>','</strong>','<span style="color:green"><strong>','</strong></span>')|raw }}</li>
25
  <li style="list-style-type: disc !important;">{{ 'UserCountry_HowToSetupGeoIP_Step4'|translate }}</li>
@@ -30,6 +30,12 @@
30
  <p>&nbsp;</p>
31
  {% endif %}
32
 
 
 
 
 
 
 
33
  <div class="row">
34
  <div class="col s12 push-m9 m3">{{ 'General_InfoFor'|translate(thisIP) }}</div>
35
  </div>
@@ -147,7 +153,7 @@
147
  <div id="manage-geoip-dbs">
148
  <div class="row" id="geoipdb-screen1">
149
  <div class="geoipdb-column-1 col s6">
150
- <p>{{ 'UserCountry_IWantToDownloadFreeGeoIP'|translate|raw }}</p>
151
  </div>
152
  <div class="geoipdb-column-2 col s6">
153
  <p>{{ 'UserCountry_IPurchasedGeoIPDBs'|translate('<a rel="noreferrer noopener" href="http://www.maxmind.com/en/geolocation_landing?rId=piwik">','</a>','<a rel="noreferrer noopener" href="https://db-ip.com/db/?refid=mtm">','</a>')|raw }}</p>
@@ -163,6 +169,9 @@
163
  value="{{ 'General_GetStarted'|translate }}..." id="start-automatic-update-geoip"/>
164
  </div>
165
  </div>
 
 
 
166
  </div>
167
  </div>
168
  {% if dbipLiteUrl|default is not empty %}
19
  {% if dbipLiteUrl|default is not empty %}
20
  <p>{{ 'UserCountry_HowToSetupGeoIPIntro'|translate }}</p>
21
  <ul style="list-style:disc !important;margin-left:2em;">
22
+ <li style="list-style-type: disc !important;">{{ 'UserCountry_HowToSetupGeoIP_Step1'|translate('<a rel="noreferrer noopener" href="'~dbipLiteUrl~'">','</a>','<a rel="noreferrer noopener" target="_blank" href="http://db-ip.com/?refid=mtm">','</a>')|raw }}<sup>*</sup></li>
23
  <li style="list-style-type: disc !important;">{{ 'UserCountry_HowToSetupGeoIP_Step2'|translate("'"~dbipLiteFilename~"'",'<strong>','</strong>','<strong>'~dbipLiteDesiredFilename~'</strong>')|raw }}</li>
24
  <li style="list-style-type: disc !important;">{{ 'UserCountry_HowToSetupGeoIP_Step3'|translate('<strong>','</strong>','<span style="color:green"><strong>','</strong></span>')|raw }}</li>
25
  <li style="list-style-type: disc !important;">{{ 'UserCountry_HowToSetupGeoIP_Step4'|translate }}</li>
30
  <p>&nbsp;</p>
31
  {% endif %}
32
 
33
+ <div class="row">
34
+ <div class="col s12">
35
+ <p><sup>{% if not isThereWorkingProvider %}*{% endif %} <small>{{ 'UserCountry_GeoIpDbIpAccuracyNote'|translate('<a href="https://dev.maxmind.com/geoip/geoip2/geolite2/?rId=piwik" rel="noreferrer noopener" target="_blank">', '</a>')|raw }}.</small></sup></p>
36
+ </div>
37
+ </div>
38
+
39
  <div class="row">
40
  <div class="col s12 push-m9 m3">{{ 'General_InfoFor'|translate(thisIP) }}</div>
41
  </div>
153
  <div id="manage-geoip-dbs">
154
  <div class="row" id="geoipdb-screen1">
155
  <div class="geoipdb-column-1 col s6">
156
+ <p>{{ 'UserCountry_IWantToDownloadFreeGeoIP'|translate|raw }}<sup><small>*</small></sup></p>
157
  </div>
158
  <div class="geoipdb-column-2 col s6">
159
  <p>{{ 'UserCountry_IPurchasedGeoIPDBs'|translate('<a rel="noreferrer noopener" href="http://www.maxmind.com/en/geolocation_landing?rId=piwik">','</a>','<a rel="noreferrer noopener" href="https://db-ip.com/db/?refid=mtm">','</a>')|raw }}</p>
169
  value="{{ 'General_GetStarted'|translate }}..." id="start-automatic-update-geoip"/>
170
  </div>
171
  </div>
172
+ <div class="row">
173
+ <p><sup>* <small>{{ 'UserCountry_GeoIpDbIpAccuracyNote'|translate('<a href="https://dev.maxmind.com/geoip/geoip2/geolite2/?rId=piwik" rel="noreferrer noopener" target="_blank">', '</a>')|raw }}.</small></sup></p>
174
+ </div>
175
  </div>
176
  </div>
177
  {% if dbipLiteUrl|default is not empty %}
app/vendor/autoload.php CHANGED
@@ -4,4 +4,4 @@
4
 
5
  require_once __DIR__ . '/composer/autoload_real.php';
6
 
7
- return ComposerAutoloaderInite67cb932b779452f67cda37373f10cc8::getLoader();
4
 
5
  require_once __DIR__ . '/composer/autoload_real.php';
6
 
7
+ return ComposerAutoloaderInitd94bcb60246118fb0b76072b15fbc1cf::getLoader();
app/vendor/composer/autoload_classmap.php CHANGED
@@ -2603,6 +2603,7 @@ return array(
2603
  'Piwik\\Updates\\Updates_3_12_0_b1' => $baseDir . '/core/Updates/3.12.0-b1.php',
2604
  'Piwik\\Updates\\Updates_3_12_0_b7' => $baseDir . '/core/Updates/3.12.0-b7.php',
2605
  'Piwik\\Updates\\Updates_3_13_1_b2' => $baseDir . '/core/Updates/3.13.1-b2.php',
 
2606
  'Piwik\\Updates\\Updates_3_5_0_b2' => $baseDir . '/core/Updates/3.5.0-b2.php',
2607
  'Piwik\\Updates\\Updates_3_5_0_b4' => $baseDir . '/core/Updates/3.5.0-b4.php',
2608
  'Piwik\\Updates\\Updates_3_5_0_rc2' => $baseDir . '/core/Updates/3.5.0-rc2.php',
2603
  'Piwik\\Updates\\Updates_3_12_0_b1' => $baseDir . '/core/Updates/3.12.0-b1.php',
2604
  'Piwik\\Updates\\Updates_3_12_0_b7' => $baseDir . '/core/Updates/3.12.0-b7.php',
2605
  'Piwik\\Updates\\Updates_3_13_1_b2' => $baseDir . '/core/Updates/3.13.1-b2.php',
2606
+ 'Piwik\\Updates\\Updates_3_13_4_b1' => $baseDir . '/core/Updates/3.13.4-b1.php',
2607
  'Piwik\\Updates\\Updates_3_5_0_b2' => $baseDir . '/core/Updates/3.5.0-b2.php',
2608
  'Piwik\\Updates\\Updates_3_5_0_b4' => $baseDir . '/core/Updates/3.5.0-b4.php',
2609
  'Piwik\\Updates\\Updates_3_5_0_rc2' => $baseDir . '/core/Updates/3.5.0-rc2.php',
app/vendor/composer/autoload_real.php CHANGED
@@ -2,7 +2,7 @@
2
 
3
  // autoload_real.php @generated by Composer
4
 
5
- class ComposerAutoloaderInite67cb932b779452f67cda37373f10cc8
6
  {
7
  private static $loader;
8
 
@@ -13,15 +13,18 @@ class ComposerAutoloaderInite67cb932b779452f67cda37373f10cc8
13
  }
14
  }
15
 
 
 
 
16
  public static function getLoader()
17
  {
18
  if (null !== self::$loader) {
19
  return self::$loader;
20
  }
21
 
22
- spl_autoload_register(array('ComposerAutoloaderInite67cb932b779452f67cda37373f10cc8', 'loadClassLoader'), true, false);
23
  self::$loader = $loader = new \Composer\Autoload\ClassLoader();
24
- spl_autoload_unregister(array('ComposerAutoloaderInite67cb932b779452f67cda37373f10cc8', 'loadClassLoader'));
25
 
26
  $includePaths = require __DIR__ . '/include_paths.php';
27
  $includePaths[] = get_include_path();
@@ -31,7 +34,7 @@ class ComposerAutoloaderInite67cb932b779452f67cda37373f10cc8
31
  if ($useStaticLoader) {
32
  require_once __DIR__ . '/autoload_static.php';
33
 
34
- call_user_func(\Composer\Autoload\ComposerStaticInite67cb932b779452f67cda37373f10cc8::getInitializer($loader));
35
  } else {
36
  $map = require __DIR__ . '/autoload_namespaces.php';
37
  foreach ($map as $namespace => $path) {
@@ -52,19 +55,19 @@ class ComposerAutoloaderInite67cb932b779452f67cda37373f10cc8
52
  $loader->register(false);
53
 
54
  if ($useStaticLoader) {
55
- $includeFiles = Composer\Autoload\ComposerStaticInite67cb932b779452f67cda37373f10cc8::$files;
56
  } else {
57
  $includeFiles = require __DIR__ . '/autoload_files.php';
58
  }
59
  foreach ($includeFiles as $fileIdentifier => $file) {
60
- composerRequiree67cb932b779452f67cda37373f10cc8($fileIdentifier, $file);
61
  }
62
 
63
  return $loader;
64
  }
65
  }
66
 
67
- function composerRequiree67cb932b779452f67cda37373f10cc8($fileIdentifier, $file)
68
  {
69
  if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
70
  require $file;
2
 
3
  // autoload_real.php @generated by Composer
4
 
5
+ class ComposerAutoloaderInitd94bcb60246118fb0b76072b15fbc1cf
6
  {
7
  private static $loader;
8
 
13
  }
14
  }
15
 
16
+ /**
17
+ * @return \Composer\Autoload\ClassLoader
18
+ */
19
  public static function getLoader()
20
  {
21
  if (null !== self::$loader) {
22
  return self::$loader;
23
  }
24
 
25
+ spl_autoload_register(array('ComposerAutoloaderInitd94bcb60246118fb0b76072b15fbc1cf', 'loadClassLoader'), true, false);
26
  self::$loader = $loader = new \Composer\Autoload\ClassLoader();
27
+ spl_autoload_unregister(array('ComposerAutoloaderInitd94bcb60246118fb0b76072b15fbc1cf', 'loadClassLoader'));
28
 
29
  $includePaths = require __DIR__ . '/include_paths.php';
30
  $includePaths[] = get_include_path();
34
  if ($useStaticLoader) {
35
  require_once __DIR__ . '/autoload_static.php';
36
 
37
+ call_user_func(\Composer\Autoload\ComposerStaticInitd94bcb60246118fb0b76072b15fbc1cf::getInitializer($loader));
38
  } else {
39
  $map = require __DIR__ . '/autoload_namespaces.php';
40
  foreach ($map as $namespace => $path) {
55
  $loader->register(false);
56
 
57
  if ($useStaticLoader) {
58
+ $includeFiles = Composer\Autoload\ComposerStaticInitd94bcb60246118fb0b76072b15fbc1cf::$files;
59
  } else {
60
  $includeFiles = require __DIR__ . '/autoload_files.php';
61
  }
62
  foreach ($includeFiles as $fileIdentifier => $file) {
63
+ composerRequired94bcb60246118fb0b76072b15fbc1cf($fileIdentifier, $file);
64
  }
65
 
66
  return $loader;
67
  }
68
  }
69
 
70
+ function composerRequired94bcb60246118fb0b76072b15fbc1cf($fileIdentifier, $file)
71
  {
72
  if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
73
  require $file;
app/vendor/composer/autoload_static.php CHANGED
@@ -4,7 +4,7 @@
4
 
5
  namespace Composer\Autoload;
6
 
7
- class ComposerStaticInite67cb932b779452f67cda37373f10cc8
8
  {
9
  public static $files = array (
10
  '04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php',
@@ -2845,6 +2845,7 @@ class ComposerStaticInite67cb932b779452f67cda37373f10cc8
2845
  'Piwik\\Updates\\Updates_3_12_0_b1' => __DIR__ . '/../..' . '/core/Updates/3.12.0-b1.php',
2846
  'Piwik\\Updates\\Updates_3_12_0_b7' => __DIR__ . '/../..' . '/core/Updates/3.12.0-b7.php',
2847
  'Piwik\\Updates\\Updates_3_13_1_b2' => __DIR__ . '/../..' . '/core/Updates/3.13.1-b2.php',
 
2848
  'Piwik\\Updates\\Updates_3_5_0_b2' => __DIR__ . '/../..' . '/core/Updates/3.5.0-b2.php',
2849
  'Piwik\\Updates\\Updates_3_5_0_b4' => __DIR__ . '/../..' . '/core/Updates/3.5.0-b4.php',
2850
  'Piwik\\Updates\\Updates_3_5_0_rc2' => __DIR__ . '/../..' . '/core/Updates/3.5.0-rc2.php',
@@ -3603,11 +3604,11 @@ class ComposerStaticInite67cb932b779452f67cda37373f10cc8
3603
  public static function getInitializer(ClassLoader $loader)
3604
  {
3605
  return \Closure::bind(function () use ($loader) {
3606
- $loader->prefixLengthsPsr4 = ComposerStaticInite67cb932b779452f67cda37373f10cc8::$prefixLengthsPsr4;
3607
- $loader->prefixDirsPsr4 = ComposerStaticInite67cb932b779452f67cda37373f10cc8::$prefixDirsPsr4;
3608
- $loader->prefixesPsr0 = ComposerStaticInite67cb932b779452f67cda37373f10cc8::$prefixesPsr0;
3609
- $loader->fallbackDirsPsr0 = ComposerStaticInite67cb932b779452f67cda37373f10cc8::$fallbackDirsPsr0;
3610
- $loader->classMap = ComposerStaticInite67cb932b779452f67cda37373f10cc8::$classMap;
3611
 
3612
  }, null, ClassLoader::class);
3613
  }
4
 
5
  namespace Composer\Autoload;
6
 
7
+ class ComposerStaticInitd94bcb60246118fb0b76072b15fbc1cf
8
  {
9
  public static $files = array (
10
  '04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php',
2845
  'Piwik\\Updates\\Updates_3_12_0_b1' => __DIR__ . '/../..' . '/core/Updates/3.12.0-b1.php',
2846
  'Piwik\\Updates\\Updates_3_12_0_b7' => __DIR__ . '/../..' . '/core/Updates/3.12.0-b7.php',
2847
  'Piwik\\Updates\\Updates_3_13_1_b2' => __DIR__ . '/../..' . '/core/Updates/3.13.1-b2.php',
2848
+ 'Piwik\\Updates\\Updates_3_13_4_b1' => __DIR__ . '/../..' . '/core/Updates/3.13.4-b1.php',
2849
  'Piwik\\Updates\\Updates_3_5_0_b2' => __DIR__ . '/../..' . '/core/Updates/3.5.0-b2.php',
2850
  'Piwik\\Updates\\Updates_3_5_0_b4' => __DIR__ . '/../..' . '/core/Updates/3.5.0-b4.php',
2851
  'Piwik\\Updates\\Updates_3_5_0_rc2' => __DIR__ . '/../..' . '/core/Updates/3.5.0-rc2.php',
3604
  public static function getInitializer(ClassLoader $loader)
3605
  {
3606
  return \Closure::bind(function () use ($loader) {
3607
+ $loader->prefixLengthsPsr4 = ComposerStaticInitd94bcb60246118fb0b76072b15fbc1cf::$prefixLengthsPsr4;
3608
+ $loader->prefixDirsPsr4 = ComposerStaticInitd94bcb60246118fb0b76072b15fbc1cf::$prefixDirsPsr4;
3609
+ $loader->prefixesPsr0 = ComposerStaticInitd94bcb60246118fb0b76072b15fbc1cf::$prefixesPsr0;
3610
+ $loader->fallbackDirsPsr0 = ComposerStaticInitd94bcb60246118fb0b76072b15fbc1cf::$fallbackDirsPsr0;
3611
+ $loader->classMap = ComposerStaticInitd94bcb60246118fb0b76072b15fbc1cf::$classMap;
3612
 
3613
  }, null, ClassLoader::class);
3614
  }
assets/css/admin-style.css CHANGED
@@ -67,6 +67,11 @@ ol.matomo-list {
67
  margin: 0 20px 10px;
68
  }
69
 
 
 
 
 
 
70
  .matomo-footer ul a {
71
  color: #37474f;
72
  }
67
  margin: 0 20px 10px;
68
  }
69
 
70
+ .matomo-header-icon {
71
+ height: 32px;
72
+ vertical-align: top;
73
+ }
74
+
75
  .matomo-footer ul a {
76
  color: #37474f;
77
  }
assets/img/activity_log.jpg DELETED
Binary file
assets/img/cohorts.png DELETED
Binary file
assets/img/custom_reports.png DELETED
Binary file
assets/img/form_analytics.jpg DELETED
Binary file
assets/img/funnels.png DELETED
Binary file
assets/img/heatmap.jpg DELETED
Binary file
assets/img/logo-full.png ADDED
Binary file
assets/img/logo.png ADDED
Binary file
assets/img/media_analytics.jpg DELETED
Binary file
assets/img/multi_attribution.png DELETED
Binary file
assets/img/premiumbundle.png DELETED
Binary file
assets/img/search_engine_keywords.png DELETED
Binary file
assets/img/users_flow.png DELETED
Binary file
assets/js/admin.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ jQuery(document).on( 'click', '#matomo-referral .notice-dismiss', function () {
2
+ var data = {'action': 'matomo_referral_dismiss_admin_notice'};
3
+ jQuery.post( ajaxurl, data );
4
+ });jQuery(document).on( 'click', '#matomo-referral .matomo-dismiss-forever', function () {
5
+ var data = {'action': 'matomo_referral_dismiss_admin_notice', forever: '1'};
6
+ jQuery.post( ajaxurl, data );
7
+ });
assets/js/asset_manager_core_js.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Matomo Javascript - cb=96b12efedc5a20952da305f7fa27e7d0*/
2
 
3
  /*! jQuery Browser - v0.1.0 - 3/23/2012
4
  * https://github.com/jquery/jquery-browser
1
+ /* Matomo Javascript - cb=e4651ae98a7539215063c712af63f642*/
2
 
3
  /*! jQuery Browser - v0.1.0 - 3/23/2012
4
  * https://github.com/jquery/jquery-browser
classes/WpMatomo.php CHANGED
@@ -86,6 +86,11 @@ class WpMatomo {
86
  $site_sync->register_hooks();
87
  $user_sync = new UserSync();
88
  $user_sync->register_hooks();
 
 
 
 
 
89
  }
90
 
91
  $tracking_code = new TrackingCode( self::$settings );
@@ -126,7 +131,7 @@ class WpMatomo {
126
  add_action(
127
  'admin_notices',
128
  function () use ( $upload_path ) {
129
- echo '<div class="error"><p>' . __( 'Matomo Analytics requires the uploads directory (' . esc_html( dirname( $upload_path ) ) . ') to be writable. Please make the directory writable for it to work.', 'matomo' ) . '</p></div>';
130
  }
131
  );
132
  }
@@ -136,22 +141,6 @@ class WpMatomo {
136
  return false;
137
  }
138
 
139
- if ( ! matomo_has_compatible_content_dir() ) {
140
- add_action(
141
- 'init',
142
- function () {
143
- if ( self::is_admin_user() ) {
144
- add_action(
145
- 'admin_notices',
146
- function () {
147
- echo '<div class="error"><p>' . __( 'It looks like you are maybe using a custom WordPress content directory. The Matomo reporting/admin pages might not work. You may be able to workaround this.', 'matomo' ) . ' <a target="_blank" rel="noreferrer noopener" href="https://matomo.org/faq/wordpress/what-are-the-requirements-for-matomo-for-wordpress/">' . esc_html__( 'Learn more', 'matomo' ) . '</a>.</p></div>';
148
- }
149
- );
150
- }
151
- }
152
- );
153
- }
154
-
155
  return true;
156
  }
157
 
86
  $site_sync->register_hooks();
87
  $user_sync = new UserSync();
88
  $user_sync->register_hooks();
89
+
90
+ $referral = new \WpMatomo\Referral();
91
+ if ($referral->should_show()) {
92
+ $referral->register_hooks();
93
+ }
94
  }
95
 
96
  $tracking_code = new TrackingCode( self::$settings );
131
  add_action(
132
  'admin_notices',
133
  function () use ( $upload_path ) {
134
+ echo '<div class="error"><p>' . sprintf(__( 'Matomo Analytics requires the uploads directory %s to be writable. Please make the directory writable for it to work.', 'matomo' ), '(' . esc_html( dirname( $upload_path ) ) . ')') . '</p></div>';
135
  }
136
  );
137
  }
141
  return false;
142
  }
143
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  return true;
145
  }
146
 
classes/WpMatomo/Admin/Admin.php CHANGED
@@ -22,11 +22,12 @@ class Admin {
22
  public function __construct( $settings ) {
23
  new Menu( $settings );
24
 
25
- add_action( 'admin_enqueue_scripts', array( $this, 'load_styles' ) );
26
  }
27
 
28
- public function load_styles() {
29
  wp_enqueue_style( 'matomo_admin_css', plugins_url( 'assets/css/admin-style.css', MATOMO_ANALYTICS_FILE ), false, '1.0.0' );
 
30
  }
31
 
32
  }
22
  public function __construct( $settings ) {
23
  new Menu( $settings );
24
 
25
+ add_action( 'admin_enqueue_scripts', array( $this, 'load_scripts' ) );
26
  }
27
 
28
+ public function load_scripts() {
29
  wp_enqueue_style( 'matomo_admin_css', plugins_url( 'assets/css/admin-style.css', MATOMO_ANALYTICS_FILE ), false, '1.0.0' );
30
+ wp_enqueue_script( 'matomo_admin_js', plugins_url( 'assets/js/admin.js', MATOMO_ANALYTICS_FILE ), array( 'jquery' ), '1.0', true );
31
  }
32
 
33
  }
classes/WpMatomo/Admin/AdminSettings.php CHANGED
@@ -17,10 +17,12 @@ if ( ! defined( 'ABSPATH' ) ) {
17
  }
18
 
19
  class AdminSettings {
20
- const TAB_TRACKING = 'tracking';
21
- const TAB_ACCESS = 'access';
22
- const TAB_EXCLUSIONS = 'exlusions';
23
- const TAB_PRIVACY = 'privacy';
 
 
24
 
25
  /**
26
  * @var Settings
@@ -40,12 +42,16 @@ class AdminSettings {
40
  $access_settings = new AccessSettings( $access, $this->settings );
41
  $tracking = new TrackingSettings( $this->settings );
42
  $exclusions = new ExclusionSettings( $this->settings );
 
43
  $privacy = new PrivacySettings();
 
44
  $setting_tabs = array(
45
  self::TAB_TRACKING => $tracking,
46
  self::TAB_ACCESS => $access_settings,
47
  self::TAB_PRIVACY => $privacy,
48
  self::TAB_EXCLUSIONS => $exclusions,
 
 
49
  );
50
 
51
  $setting_tabs = apply_filters( 'matomo_setting_tabs', $setting_tabs, $this->settings );
17
  }
18
 
19
  class AdminSettings {
20
+ const TAB_TRACKING = 'tracking';
21
+ const TAB_ACCESS = 'access';
22
+ const TAB_EXCLUSIONS = 'exlusions';
23
+ const TAB_PRIVACY = 'privacy';
24
+ const TAB_GEOLOCATION = 'geolocation';
25
+ const TAB_ADVANCED = 'advanced';
26
 
27
  /**
28
  * @var Settings
42
  $access_settings = new AccessSettings( $access, $this->settings );
43
  $tracking = new TrackingSettings( $this->settings );
44
  $exclusions = new ExclusionSettings( $this->settings );
45
+ $geolocation = new GeolocationSettings( $this->settings );
46
  $privacy = new PrivacySettings();
47
+ $advanced = new AdvancedSettings();
48
  $setting_tabs = array(
49
  self::TAB_TRACKING => $tracking,
50
  self::TAB_ACCESS => $access_settings,
51
  self::TAB_PRIVACY => $privacy,
52
  self::TAB_EXCLUSIONS => $exclusions,
53
+ self::TAB_GEOLOCATION => $geolocation,
54
+ self::TAB_ADVANCED => $advanced,
55
  );
56
 
57
  $setting_tabs = apply_filters( 'matomo_setting_tabs', $setting_tabs, $this->settings );
classes/WpMatomo/Admin/AdvancedSettings.php ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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\Admin;
11
+
12
+ use Piwik\Config;
13
+ use Piwik\IP;
14
+ use WpMatomo\Bootstrap;
15
+ use WpMatomo\Capabilities;
16
+ use WpMatomo\Settings;
17
+ use WpMatomo\Site;
18
+ use WpMatomo\TrackingCode\TrackingCodeGenerator;
19
+
20
+ if ( ! defined( 'ABSPATH' ) ) {
21
+ exit; // if accessed directly
22
+ }
23
+
24
+ class AdvancedSettings implements AdminSettingsInterface {
25
+ const FORM_NAME = 'matomo';
26
+ const NONCE_NAME = 'matomo_advanced';
27
+
28
+ public static $valid_host_headers = array(
29
+ 'HTTP_CLIENT_IP',
30
+ 'HTTP_X_REAL_IP',
31
+ 'HTTP_X_FORWARDED_FOR',
32
+ 'HTTP_X_FORWARDED',
33
+ 'HTTP_FORWARDED_FOR',
34
+ 'HTTP_FORWARDED',
35
+ 'HTTP_CF_CONNECTING_IP',
36
+ 'HTTP_TRUE_CLIENT_IP',
37
+ 'HTTP_X_CLUSTER_CLIENT_IP',
38
+ );
39
+
40
+ public function get_title() {
41
+ return esc_html__( 'Advanced', 'matomo' );
42
+ }
43
+
44
+ private function update_if_submitted() {
45
+ if ( isset( $_POST )
46
+ && ! empty( $_POST[ self::FORM_NAME ] )
47
+ && is_admin()
48
+ && check_admin_referer( self::NONCE_NAME )
49
+ && $this->can_user_manage() ) {
50
+ $this->apply_settings();
51
+
52
+ return true;
53
+ }
54
+
55
+ return false;
56
+ }
57
+
58
+ public function can_user_manage() {
59
+ return current_user_can( Capabilities::KEY_SUPERUSER );
60
+ }
61
+
62
+ private function apply_settings() {
63
+ Bootstrap::do_bootstrap();
64
+ $config = Config::getInstance();
65
+ $general = $config->General;
66
+ $general['proxy_client_headers'] = array();
67
+
68
+ if (!empty($_POST[ self::FORM_NAME ]['proxy_client_header'])) {
69
+ $client_header = $_POST[ self::FORM_NAME ]['proxy_client_header'];
70
+ if (in_array($client_header, self::$valid_host_headers, true)) {
71
+ $general['proxy_client_headers'][] = $client_header;
72
+ }
73
+ }
74
+ $config->General = $general;
75
+ $config->forceSave();
76
+
77
+ return true;
78
+ }
79
+
80
+ public function show_settings() {
81
+ $was_updated = $this->update_if_submitted();
82
+
83
+ $matomo_client_headers = array();
84
+ Bootstrap::do_bootstrap();
85
+ $config = Config::getInstance();
86
+ $general = $config->General;
87
+ if (!empty($general['proxy_client_headers']) && is_array($general['proxy_client_headers'])) {
88
+ $matomo_client_headers = $general['proxy_client_headers'];
89
+ }
90
+
91
+ $matomo_detected_ip = IP::getIpFromHeader();
92
+
93
+ include dirname( __FILE__ ) . '/views/advanced_settings.php';
94
+ }
95
+
96
+
97
+
98
+ }
classes/WpMatomo/Admin/GeolocationSettings.php ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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\Admin;
11
+
12
+ use WpMatomo\Capabilities;
13
+ use WpMatomo\ScheduledTasks;
14
+ use WpMatomo\Settings;
15
+
16
+ if ( ! defined( 'ABSPATH' ) ) {
17
+ exit; // if accessed directly
18
+ }
19
+
20
+ class GeolocationSettings implements AdminSettingsInterface {
21
+ const NONCE_NAME = 'matomo_geolocation';
22
+ const FORM_NAME = 'matomo_maxmind_license';
23
+
24
+ /**
25
+ * @var Settings
26
+ */
27
+ private $settings;
28
+
29
+ public function __construct( Settings $settings ) {
30
+ $this->settings = $settings;
31
+ }
32
+
33
+ public function get_title() {
34
+ return esc_html__( 'Geolocation', 'matomo' );
35
+ }
36
+
37
+ private function update_if_submitted() {
38
+ if ( isset( $_POST )
39
+ && isset( $_POST[ self::FORM_NAME ] )
40
+ && is_admin()
41
+ && check_admin_referer( self::NONCE_NAME )
42
+ && current_user_can( Capabilities::KEY_SUPERUSER ) ) {
43
+
44
+ $maxmind_license = stripslashes($_POST[ self::FORM_NAME ]);
45
+
46
+ if (empty($maxmind_license)) {
47
+ $maxmind_license = '';
48
+ } elseif (strlen($maxmind_license) > 20 || strlen($maxmind_license) < 7 || !ctype_graph($maxmind_license)) {
49
+ return false;
50
+ }
51
+
52
+ $this->settings->apply_changes(array(
53
+ 'maxmind_license_key' => $maxmind_license
54
+ ));
55
+
56
+ // update geoip in the backgronud
57
+ wp_schedule_single_event( time() + 10, ScheduledTasks::EVENT_GEOIP );
58
+
59
+ return true;
60
+ }
61
+ }
62
+
63
+ public function show_settings() {
64
+ $invalid_format = $this->update_if_submitted() === false;
65
+
66
+ $current_maxmind_license = $this->settings->get_global_option('maxmind_license_key');
67
+
68
+ include dirname( __FILE__ ) . '/views/geolocation_settings.php';
69
+ }
70
+
71
+ }
classes/WpMatomo/Admin/Info.php CHANGED
@@ -9,18 +9,59 @@
9
 
10
  namespace WpMatomo\Admin;
11
 
 
 
12
  if ( ! defined( 'ABSPATH' ) ) {
13
  exit; // if accessed directly
14
  }
15
 
16
  class Info {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  public function show() {
19
- include dirname( __FILE__ ) . '/views/info.php';
20
  }
21
 
22
  public function show_multisite() {
23
- include dirname( __FILE__ ) . '/views/info_multisite.php';
 
 
 
 
 
 
 
24
  }
25
 
26
 
9
 
10
  namespace WpMatomo\Admin;
11
 
12
+ use WpMatomo\Capabilities;
13
+
14
  if ( ! defined( 'ABSPATH' ) ) {
15
  exit; // if accessed directly
16
  }
17
 
18
  class Info {
19
+ const NONCE_NAME = 'matomo_newsletter';
20
+ const FORM_NAME = 'matomo_newsletter_signup';
21
+
22
+ private function update_if_submitted() {
23
+ if ( isset( $_POST )
24
+ && !empty( $_POST[ self::FORM_NAME ] )
25
+ && is_admin()
26
+ && check_admin_referer( self::NONCE_NAME )
27
+ && $this->show_newsletter_signup()
28
+ && current_user_can( Capabilities::KEY_VIEW ) ) {
29
+
30
+ $user = wp_get_current_user();
31
+ $locale = explode('_', get_user_locale($user->ID));
32
+ wp_remote_get('https://api.matomo.org/1.0/subscribeNewsletter/?' . http_build_query(array(
33
+ 'email' => $user->user_email,
34
+ 'wordpress' => 1,
35
+ 'language' => $locale[0],
36
+ )));
37
+ update_user_meta($user->ID, self::FORM_NAME, '1');
38
+
39
+ return true;
40
+ }
41
+ }
42
+
43
+ private function show_newsletter_signup() {
44
+ if (!is_user_logged_in()) {
45
+ return false;
46
+ }
47
+
48
+ $user = wp_get_current_user();
49
+ return !get_user_meta($user->ID, self::FORM_NAME, true);
50
+ }
51
 
52
  public function show() {
53
+ $this->render('info');
54
  }
55
 
56
  public function show_multisite() {
57
+ $this->render('info_multisite');
58
+ }
59
+
60
+ private function render($template) {
61
+ $signedup_newsletter = $this->update_if_submitted();
62
+ $show_newsletter = $this->show_newsletter_signup();
63
+
64
+ include dirname( __FILE__ ) . '/views/' . $template . '.php';
65
  }
66
 
67
 
classes/WpMatomo/Admin/Menu.php CHANGED
@@ -178,8 +178,8 @@ class Menu {
178
  if ( $can_matomo_be_managed ) {
179
  add_submenu_page(
180
  self::$parent_slug,
181
- __( 'System Report', 'matomo' ),
182
- __( 'System Report', 'matomo' ),
183
  Capabilities::KEY_SUPERUSER,
184
  self::SLUG_SYSTEM_REPORT,
185
  array(
178
  if ( $can_matomo_be_managed ) {
179
  add_submenu_page(
180
  self::$parent_slug,
181
+ __( 'Diagnostics', 'matomo' ),
182
+ __( 'Diagnostics', 'matomo' ),
183
  Capabilities::KEY_SUPERUSER,
184
  self::SLUG_SYSTEM_REPORT,
185
  array(
classes/WpMatomo/Admin/SystemReport.php CHANGED
@@ -283,8 +283,8 @@ class SystemReport {
283
  'comment' => $tmp_dir,
284
  );
285
 
286
- if ( ! empty( $_ENV['MATOMO_WP_ROOT_PATH'] ) ) {
287
- $custom_path = rtrim( $_ENV['MATOMO_WP_ROOT_PATH'], '/' ) . '/wp-load.php';
288
  $path_exists = file_exists( $custom_path );
289
  $comment = '';
290
  if ( ! $path_exists ) {
@@ -690,6 +690,21 @@ class SystemReport {
690
  }
691
  }
692
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
  return $rows;
694
  }
695
 
283
  'comment' => $tmp_dir,
284
  );
285
 
286
+ if ( ! empty( $_SERVER['MATOMO_WP_ROOT_PATH'] ) ) {
287
+ $custom_path = rtrim( $_SERVER['MATOMO_WP_ROOT_PATH'], '/' ) . '/wp-load.php';
288
  $path_exists = file_exists( $custom_path );
289
  $comment = '';
290
  if ( ! $path_exists ) {
690
  }
691
  }
692
 
693
+ $compatible_content_dir = matomo_has_compatible_content_dir();
694
+ if ($compatible_content_dir === true) {
695
+ $rows[] = array(
696
+ 'name' => 'Compatible content directory',
697
+ 'value' => true,
698
+ );
699
+ } else {
700
+ $rows[] = array(
701
+ 'name' => 'Compatible content directory',
702
+ 'value' => $compatible_content_dir,
703
+ 'is_warning' => true,
704
+ 'comment' => __( 'It looks like you are maybe using a custom WordPress content directory. The Matomo reporting/admin pages might not work. You may be able to workaround this.', 'matomo' ) . ' ' . __( 'Learn more', 'matomo' ) . ': https://matomo.org/faq/wordpress/how-do-i-make-matomo-for-wordpress-work-when-i-have-a-custom-content-directory/'
705
+ );
706
+ }
707
+
708
  return $rows;
709
  }
710
 
classes/WpMatomo/Admin/views/access.php CHANGED
@@ -58,7 +58,8 @@ use WpMatomo\Admin\AccessSettings;
58
  rel="noopener"><?php esc_html_e( 'Write', 'matomo' ); ?></a>,
59
  <a href="https://matomo.org/faq/general/faq_69/" target="_blank" rel="noopener"><?php esc_html_e( 'Admin', 'matomo' ); ?></a>,
60
  <a href="https://matomo.org/faq/general/faq_35/" target="_blank"
61
- rel="noopener"><?php esc_html_e( 'Super User', 'matomo' ); ?></a>
 
62
  </p>
63
 
64
  <h2><?php esc_html_e( 'Roles', 'matomo' ); ?></h2>
@@ -93,4 +94,4 @@ esc_html_e(
93
  ?>
94
  <li><?php echo esc_html( $matomo_cap_name ); ?></li>
95
  <?php } ?>
96
- </ul>
58
  rel="noopener"><?php esc_html_e( 'Write', 'matomo' ); ?></a>,
59
  <a href="https://matomo.org/faq/general/faq_69/" target="_blank" rel="noopener"><?php esc_html_e( 'Admin', 'matomo' ); ?></a>,
60
  <a href="https://matomo.org/faq/general/faq_35/" target="_blank"
61
+ rel="noopener"><?php esc_html_e( 'Super User', 'matomo' ); ?></a><br/>
62
+ <?php esc_html_e( 'Want to redirect to the home page when not logged in?', 'matomo' ); ?> <a href="https://matomo.org/faq/wordpress/how-do-i-hide-my-wordpress-login-url-when-someone-accesses-a-matomo-report-directly/" target="_blank" rel="noreferrer noopener"><?php esc_html_e( 'Learn more', 'matomo' ); ?></a>
63
  </p>
64
 
65
  <h2><?php esc_html_e( 'Roles', 'matomo' ); ?></h2>
94
  ?>
95
  <li><?php echo esc_html( $matomo_cap_name ); ?></li>
96
  <?php } ?>
97
+ </ul>
classes/WpMatomo/Admin/views/advanced_settings.php ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ * Code Based on
9
+ * @author Andr&eacute; Br&auml;kling
10
+ * https://github.com/braekling/WP-Matomo
11
+ *
12
+ */
13
+
14
+ use WpMatomo\Admin\AdvancedSettings;
15
+
16
+ if ( ! defined( 'ABSPATH' ) ) {
17
+ exit;
18
+ }
19
+ /** @var bool $was_updated */
20
+ /** @var string $matomo_detected_ip */
21
+ /** @var array $matomo_client_headers */
22
+ ?>
23
+
24
+ <?php
25
+ if ( $was_updated ) {
26
+ include 'update_notice_clear_cache.php';
27
+ }
28
+ ?>
29
+ <form method="post">
30
+ <?php wp_nonce_field( AdvancedSettings::NONCE_NAME ); ?>
31
+
32
+ <p><?php esc_html_e( 'Advanced settings', 'matomo' ); ?></p>
33
+ <table class="matomo-tracking-form widefat">
34
+ <tbody>
35
+ <tr>
36
+ <th width="20%" scope="row"><label for="matomo[proxy_client_header]"><?php esc_html_e( 'Proxy IP headers', 'matomo' ) ?>:</label>
37
+ </th>
38
+ <td>
39
+ <?php
40
+ echo '<span style="white-space: nowrap;display: inline-block;"><input type="radio" ' . ( empty($matomo_client_headers) ? 'checked="checked" ' : '' ) . ' value="REMOTE_ADDR" name="matomo[proxy_client_header]" /> <code>REMOTE_ADDR</code> ' . ( ! empty( $_SERVER[ 'REMOTE_ADDR' ] ) ? esc_html( $_SERVER[ 'REMOTE_ADDR' ] ) : esc_html__( 'No value found', 'matomo' ) ) . ' (' . esc_html__( 'Default', 'matomo' ) .')</span>';
41
+ foreach ( AdvancedSettings::$valid_host_headers as $host_header ) {
42
+ echo '<span style="white-space: nowrap;display: inline-block;"><input type="radio" ' . ( in_array( $host_header, $matomo_client_headers, true ) ? 'checked="checked" ' : '' ) . 'value="'. esc_attr($host_header).'" name="matomo[proxy_client_header]" /> <code>' . $host_header . '</code> ' . ( ! empty( $_SERVER[ $host_header ] ) ? esc_html( $_SERVER[ $host_header ] ) : esc_html__( 'No value found', 'matomo' ) ) . ' &nbsp; </span>';
43
+ }
44
+ ?>
45
+ </td>
46
+ <td width="50%">
47
+ <?php esc_html_e( 'We detected you have the following IP address:', 'matomo' ) ?>
48
+ <?php echo esc_html( $matomo_detected_ip ) ?> <br>
49
+ <?php echo sprintf(esc_html__( 'To compare this value with your actual IP address %1$splease click here%2$s.', 'matomo' ), '<a rel="noreferrer noopener" target="_blank" href="https://matomo.org/ip.php">', '</a>') ?><br><br>
50
+ <?php esc_html_e( 'Should your IP address not match the above value, your WordPress might be behind a proxy and you may need to select a different HTTP header depending on which of the values on the left shows your correct IP address.', 'matomo' ) ?>
51
+ </td>
52
+ </tr>
53
+ <tr>
54
+ <td colspan="3"><p class="submit"><input name="Submit" type="submit" class="button-primary"
55
+ value="<?php esc_attr_e( 'Save Changes', 'matomo' ) ?>"/></p></td>
56
+ </tr>
57
+ </tbody>
58
+ </table>
59
+ </form>
classes/WpMatomo/Admin/views/exclusion_settings.php CHANGED
@@ -42,8 +42,7 @@ if ( $was_updated ) {
42
  <tbody>
43
 
44
  <tr>
45
- <th width="20%" scope="row"><label
46
- for="%2$s"><?php esc_html_e( 'Tracking filter', 'matomo' ); ?></label>:
47
  </th>
48
  <td>
49
  <?php
@@ -59,8 +58,7 @@ if ( $was_updated ) {
59
  </td>
60
  </tr>
61
  <tr>
62
- <th width="20%" scope="row"><label
63
- for="%2$s"><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedIps' ) ); ?></label>:
64
  </th>
65
  <td width="30%">
66
  <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_ips', esc_html( $excluded_ips ) ); ?>
@@ -81,8 +79,7 @@ if ( $was_updated ) {
81
  </td>
82
  </tr>
83
  <tr>
84
- <th scope="row"><label
85
- for="%2$s"><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedQueryParameters' ) ); ?></label>:
86
  </th>
87
  <td>
88
  <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_query_parameters', esc_html( $excluded_query_params ) ); ?>
@@ -93,8 +90,7 @@ if ( $was_updated ) {
93
  </td>
94
  </tr>
95
  <tr>
96
- <th scope="row"><label
97
- for="%2$s"><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedUserAgents' ) ); ?></label>:
98
  </th>
99
  <td>
100
  <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_user_agents', esc_html( $excluded_user_agents ) ); ?>
@@ -109,8 +105,7 @@ if ( $was_updated ) {
109
  </td>
110
  </tr>
111
  <tr>
112
- <th scope="row"><label
113
- for="%2$s"><?php echo esc_html( Piwik::translate( 'SitesManager_KeepURLFragmentsLong' ) ); ?></label>:
114
  </th>
115
  <td>
116
  <?php echo sprintf( '<input type="checkbox" value="1" %2$s name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">', 'keep_url_fragments', $keep_url_fragments ? ' checked="checked"' : '' ); ?>
42
  <tbody>
43
 
44
  <tr>
45
+ <th width="20%" scope="row"><label><?php esc_html_e( 'Tracking filter', 'matomo' ); ?></label>:
 
46
  </th>
47
  <td>
48
  <?php
58
  </td>
59
  </tr>
60
  <tr>
61
+ <th width="20%" scope="row"><label><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedIps' ) ); ?></label>:
 
62
  </th>
63
  <td width="30%">
64
  <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_ips', esc_html( $excluded_ips ) ); ?>
79
  </td>
80
  </tr>
81
  <tr>
82
+ <th scope="row"><label><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedQueryParameters' ) ); ?></label>:
 
83
  </th>
84
  <td>
85
  <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_query_parameters', esc_html( $excluded_query_params ) ); ?>
90
  </td>
91
  </tr>
92
  <tr>
93
+ <th scope="row"><label><?php echo esc_html( Piwik::translate( 'SitesManager_GlobalListExcludedUserAgents' ) ); ?></label>:
 
94
  </th>
95
  <td>
96
  <?php echo sprintf( '<textarea cols="40" rows="4" id="%1$s" name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">%2$s</textarea>', 'excluded_user_agents', esc_html( $excluded_user_agents ) ); ?>
105
  </td>
106
  </tr>
107
  <tr>
108
+ <th scope="row"><label><?php echo esc_html( Piwik::translate( 'SitesManager_KeepURLFragmentsLong' ) ); ?></label>:
 
109
  </th>
110
  <td>
111
  <?php echo sprintf( '<input type="checkbox" value="1" %2$s name="' . esc_attr( ExclusionSettings::FORM_NAME ) . '[%1$s]">', 'keep_url_fragments', $keep_url_fragments ? ' checked="checked"' : '' ); ?>
classes/WpMatomo/Admin/views/geolocation_settings.php ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ * Code Based on
9
+ * @author Andr&eacute; Br&auml;kling
10
+ * @package WP_Matomo
11
+ * https://github.com/braekling/matomo
12
+ *
13
+ */
14
+
15
+ use WpMatomo\Admin\GeolocationSettings;
16
+
17
+ if ( ! defined( 'ABSPATH' ) ) {
18
+ exit;
19
+ }
20
+ /** @var bool $was_updated */
21
+ /** @var bool $invalid_format */
22
+ /** @var string $current_maxmind_license */
23
+
24
+ if ($invalid_format) { ?>
25
+ <div class="updated notice error">
26
+ <p><?php esc_html_e( 'It looks like the MaxMind license key has a wrong format.', 'matomo' ); ?></p>
27
+ </div>
28
+ <?php
29
+ }
30
+ ?>
31
+
32
+ <form method="post">
33
+ <?php wp_nonce_field( GeolocationSettings::NONCE_NAME ); ?>
34
+
35
+ <p>
36
+ <?php esc_html_e( 'On this page you can configure how Matomo detects the locations of your visitors.', 'matomo' ); ?>
37
+ </p>
38
+ <p>
39
+ <?php esc_html_e('To detect the location of a visitor, the IP address of a visitor is looked up in a so called geolocation database. This is automatically taken care of for you. However, the freely available database DB-IP we are using is sometimes less accurate than other freely available geolocation databases. This applies to the free and paid version of DB-IP. An alternative geolocation database is called MaxMind which has a free and a paid version as well. Because of GDPR we cannot configure this database automatically for you.', 'matomo'); ?>
40
+ <br><br>
41
+ <?php
42
+ echo sprintf(
43
+ __( 'To use MaxMind instead of the default DB-IP geolocation database %1$s get a MaxMind license key%2$s and then configure this key below.', 'matomo' ),
44
+ '<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/faq/how-to/how-do-i-get-a-license-key-for-the-maxmind-geolocation-database/">', '</a>'
45
+ );
46
+ ?>
47
+ </p>
48
+
49
+ <table class="matomo-tracking-form widefat">
50
+ <tbody>
51
+ <tr>
52
+ <th scope="row" style="vertical-align: top;">
53
+ <label for="<?php echo esc_attr( GeolocationSettings::FORM_NAME ) ?>"><?php esc_html_e( 'MaxMind License Key', 'matomo' ); ?></label>:
54
+ </th>
55
+ <td>
56
+ <input size="20" type="text" maxlength="20"
57
+ id="<?php echo esc_attr( GeolocationSettings::FORM_NAME ) ?>"
58
+ name="<?php echo esc_attr( GeolocationSettings::FORM_NAME ) ?>" value="<?php echo esc_attr($current_maxmind_license) ?>">
59
+ </td>
60
+ <td>
61
+ <?php esc_html_e('Leave the field empty and click on "Save Changes" to configure the default DB-IP database.', 'matomo') ?>
62
+ <?php esc_html_e('When configured, your WordPress will send an HTTP request to a MaxMind server to download an approx. 60MB database and store it in your "wp-content/uploads/matomo" directory.', 'matomo') ?>
63
+ </td>
64
+ </tr>
65
+ <tr>
66
+ <td colspan="3">
67
+ <p class="submit"><input name="Submit" type="submit" class="button-primary"
68
+ value="<?php echo esc_attr__( 'Save Changes', 'matomo' ); ?>"/></p>
69
+ </td>
70
+ </tr>
71
+
72
+ </tbody>
73
+ </table>
74
+ </form>
classes/WpMatomo/Admin/views/get_started.php CHANGED
@@ -38,7 +38,7 @@ if ( empty( $show_this_page ) ) {
38
  ?>
39
 
40
  <?php if ( $settings->is_tracking_enabled() ) { ?>
41
- <h2>1. <?php esc_html_e( 'Tracking is enabled', 'matomo' ); ?> <span class="dashicons dashicons-yes"></span></h2>
42
  <p><a href="<?php echo AdminSettings::make_url( AdminSettings::TAB_TRACKING ); ?>"><?php esc_html_e( 'Click here to configure your tracking code.', 'matomo' ); ?></a></p>
43
 
44
  <?php } else { ?>
38
  ?>
39
 
40
  <?php if ( $settings->is_tracking_enabled() ) { ?>
41
+ <h2>1. <?php esc_html_e( 'Tracking is enabled', 'matomo' ); ?> <span class="dashicons dashicons-yes" style="color: green;"></span></h2>
42
  <p><a href="<?php echo AdminSettings::make_url( AdminSettings::TAB_TRACKING ); ?>"><?php esc_html_e( 'Click here to configure your tracking code.', 'matomo' ); ?></a></p>
43
 
44
  <?php } else { ?>
classes/WpMatomo/Admin/views/info.php CHANGED
@@ -29,14 +29,14 @@ if ( ! defined( 'ABSPATH' ) ) {
29
  <?php
30
  echo sprintf(
31
  esc_html__(
32
- 'Matomo is a collaborative project brought to you by %1$sMatomo team%2$s members as well as many other contributors around the globe. If you\'re a fan of Matomo,
33
- %3$shere\'s how you can participate!%4$s',
34
  'matomo'
35
  ),
36
  '<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/team/">',
37
  '</a>',
38
- '<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/get-involved/">',
39
- '</a>'
40
  );
41
  ?>
42
  <br/><br/>
@@ -55,6 +55,8 @@ if ( ! defined( 'ABSPATH' ) ) {
55
  ?>
56
  </p>
57
 
 
 
58
  <h2><?php esc_html_e( 'High traffic websites', 'matomo' ); ?></h2>
59
  <?php require 'info_high_traffic.php'; ?>
60
 
29
  <?php
30
  echo sprintf(
31
  esc_html__(
32
+ 'Matomo is a collaborative project brought to you by %1$sMatomo team%2$s members as well as many other contributors around the globe. If you like Matomo,
33
+ %3$splease give us a review%4$s and spread the word about us.',
34
  'matomo'
35
  ),
36
  '<a target="_blank" rel="noreferrer noopener" href="https://matomo.org/team/">',
37
  '</a>',
38
+ '<a target="_blank" rel="noreferrer noopener" href="https://wordpress.org/support/plugin/matomo/reviews/?rate=5#new-post">',
39
+ '<span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span><span class="dashicons-before dashicons-star-filled" style="color:gold;"></span></a>'
40
  );
41
  ?>
42
  <br/><br/>
55
  ?>
56
  </p>
57
 
58
+ <?php require 'info_newsletter.php'; ?>
59
+
60
  <h2><?php esc_html_e( 'High traffic websites', 'matomo' ); ?></h2>
61
  <?php require 'info_high_traffic.php'; ?>
62
 
classes/WpMatomo/Admin/views/info_help.php CHANGED
@@ -13,7 +13,7 @@ if ( ! defined( 'ABSPATH' ) ) {
13
  ?>
14
  <h2><?php esc_html_e( 'How can we help?', 'matomo' ); ?></h2>
15
 
16
- <form method="get" action="https://matomo.org" target="_blank">
17
  <input type="text" name="s" style="width:300px;"><input type="submit" class="button-secondary"
18
  value="Search on matomo.org">
19
  </form>
13
  ?>
14
  <h2><?php esc_html_e( 'How can we help?', 'matomo' ); ?></h2>
15
 
16
+ <form method="get" action="https://matomo.org" target="_blank" rel="noreferrer noopener">
17
  <input type="text" name="s" style="width:300px;"><input type="submit" class="button-secondary"
18
  value="Search on matomo.org">
19
  </form>
classes/WpMatomo/Admin/views/info_multisite.php CHANGED
@@ -11,12 +11,12 @@ if ( ! defined( 'ABSPATH' ) ) {
11
  }
12
 
13
  /** @var \WpMatomo\Settings $settings */
14
- /** @var bool $canUserEdit */
15
  ?>
16
 
17
  <div class="wrap">
18
  <div id="icon-plugins" class="icon32"></div>
19
  <h1><?php esc_html_e( 'Matomo Analytics in Multi Site mode', 'matomo' ); ?></h1>
 
20
  <p><?php esc_html_e( 'You are seeing this page as you are viewing the network admin. Matomo differentiates between two different multi site modes:', 'matomo' ); ?></p>
21
  <h2><?php esc_html_e( 'Matomo is network enabled', 'matomo' ); ?></h2>
22
  <p><?php esc_html_e( 'In this mode, the tracking and access settings are managed in the network admin in one place and apply to all blogs.', 'matomo' ); ?>
11
  }
12
 
13
  /** @var \WpMatomo\Settings $settings */
 
14
  ?>
15
 
16
  <div class="wrap">
17
  <div id="icon-plugins" class="icon32"></div>
18
  <h1><?php esc_html_e( 'Matomo Analytics in Multi Site mode', 'matomo' ); ?></h1>
19
+ <?php require 'info_newsletter.php'; ?>
20
  <p><?php esc_html_e( 'You are seeing this page as you are viewing the network admin. Matomo differentiates between two different multi site modes:', 'matomo' ); ?></p>
21
  <h2><?php esc_html_e( 'Matomo is network enabled', 'matomo' ); ?></h2>
22
  <p><?php esc_html_e( 'In this mode, the tracking and access settings are managed in the network admin in one place and apply to all blogs.', 'matomo' ); ?>
classes/WpMatomo/Admin/views/info_newsletter.php ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ use \WpMatomo\Admin\Info;
11
+
12
+ if ( ! defined( 'ABSPATH' ) ) {
13
+ exit;
14
+ }
15
+ /** @var bool $signedup_newsletter */
16
+ /** @var bool $show_newsletter */
17
+
18
+ if ($signedup_newsletter) {
19
+ ?>
20
+ <div class="notice notice-success is-dismissible">
21
+ <p><?php esc_html_e('Thank you for signing up to our newsletter.', 'matomo'); ?></p>
22
+ </div>
23
+ <?php
24
+ return;
25
+ }
26
+ if (!$show_newsletter) {
27
+ return;
28
+ }
29
+ ?>
30
+
31
+ <div class="notice notice-success">
32
+ <h2><?php esc_html_e( 'Newsletter', 'matomo' ); ?></h2>
33
+ <form method="post">
34
+ <p>
35
+ <?php wp_nonce_field( Info::NONCE_NAME ); ?>
36
+ <input type="checkbox" id="<?php echo Info::FORM_NAME ?>" name="<?php echo Info::FORM_NAME ?>" value="1">
37
+ <label for="<?php echo Info::FORM_NAME ?>">
38
+ <?php esc_html_e('Subscribe to our newsletter to receive regular information about Matomo, web analytics, and privacy. You can unsubscribe from it any time.', 'matomo'); ?>
39
+ <?php esc_html_e('This service uses MadMimi.', 'matomo'); ?>
40
+ <?php echo sprintf(esc_html__('Learn more about it on our %1$sPrivacy Policy page%2$s.', 'matomo'), '<a href="https://matomo.org/privacy-policy/" target="_blank" rel="noreferrer noopener">', '</a>'); ?>
41
+ </label>
42
+ <br><br>
43
+ <input type="submit" class="button-secondary" value="<?php esc_attr_e('Subscribe', 'matomo');?>">
44
+ </p>
45
+ </form>
46
+ </div>
classes/WpMatomo/Admin/views/info_shared.php CHANGED
@@ -11,7 +11,7 @@ if ( ! defined( 'ABSPATH' ) ) {
11
  exit; // if accessed directly
12
  }
13
  ?>
14
- <h1><?php esc_html_e( 'About Matomo Analytics', 'matomo' ); ?></h1>
15
 
16
  <p>
17
  <?php
11
  exit; // if accessed directly
12
  }
13
  ?>
14
+ <h1><?php esc_html_e( 'About', 'matomo' ); ?> <?php matomo_header_icon(true); ?> </h1>
15
 
16
  <p>
17
  <?php
classes/WpMatomo/Admin/views/marketplace.php CHANGED
@@ -30,7 +30,7 @@ $matomo_extra_url_params = '&' . http_build_query(
30
 
31
  <div id="icon-plugins" class="icon32"></div>
32
 
33
- <h1><?php esc_html_e( 'Discover new functionality for your Matomo', 'matomo' ); ?></h1>
34
  <p>
35
  <?php esc_html_e( 'Take your Matomo (formerly Piwik) to the next level and drive your conversions & revenue with these premium features. All features are fully hosted on your WordPress and come with 100% data ownership and no limitations.', 'matomo' ); ?>
36
  <?php if ( is_plugin_active( MATOMO_MARKETPLACE_PLUGIN_NAME ) ) { ?>
@@ -43,7 +43,7 @@ $matomo_extra_url_params = '&' . http_build_query(
43
  <h2><?php echo sprintf( esc_html__( 'Easily install over 100 free plugins & %1$spremium features%2$s for Matomo with just a click' ), '<span style="white-space: nowrap;">', '</span>' ); ?></h2>
44
  <a href="https://builds.matomo.org/matomo-marketplace-for-wordpress-latest.zip" rel="noreferrer noopener" class="button matomo-cta-button"><?php esc_html_e( 'Download Matomo Marketplace for WordPress', 'matomo' ); ?></a>
45
  <br>
46
- <a target="_blank" href="https://matomo.org/faq/wordpress/how-do-i-install-a-matomo-marketplace-plugin-in-matomo-for-wordpress/"><?php esc_html_e( 'Learn more', 'matomo' ); ?></a>
47
  <a target="_blank" href="https://plugins.matomo.org/?wp=1" rel="noreferrer noopener" class="matomo-next-link"><?php esc_html_e( 'Browse Marketplace', 'matomo' ); ?></a>
48
  </div>
49
  <?php } ?>
@@ -52,6 +52,8 @@ $matomo_extra_url_params = '&' . http_build_query(
52
  $matomo_feature_sections = array(
53
  array(
54
  'title' => 'Top free plugins',
 
 
55
  'features' =>
56
  array(
57
  array(
@@ -89,14 +91,14 @@ $matomo_extra_url_params = '&' . http_build_query(
89
  'description' => 'Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions.',
90
  'price' => '99EUR / 119USD',
91
  'url' => 'https://plugins.matomo.org/HeatmapSessionRecording?wp=1',
92
- 'image' => plugins_url( 'assets/img/heatmap.jpg', MATOMO_ANALYTICS_FILE ),
93
  ),
94
  array(
95
  'name' => 'Custom Reports',
96
  'description' => 'Pull out the information you need in order to be successful. Develop your custom strategy to meet your individualized goals while saving money & time.',
97
  'price' => '99EUR / 119USD',
98
  'url' => 'https://plugins.matomo.org/CustomReports?wp=1',
99
- 'image' => plugins_url( 'assets/img/custom_reports.png', MATOMO_ANALYTICS_FILE ),
100
  ),
101
 
102
  array(
@@ -104,7 +106,7 @@ $matomo_extra_url_params = '&' . http_build_query(
104
  'description' => 'All premium features in one bundle, make the most out of your Matomo for WordPress and enjoy discounts of over 20%!',
105
  'price' => '499EUR / 579USD',
106
  'url' => 'https://plugins.matomo.org/WpPremiumBundle?wp=1',
107
- 'image' => plugins_url( 'assets/img/premiumbundle.png', MATOMO_ANALYTICS_FILE ),
108
  ),
109
  ),
110
  ),
@@ -117,21 +119,21 @@ $matomo_extra_url_params = '&' . http_build_query(
117
  'description' => 'Increase conversions on your online forms and lose less visitors by learning everything about your users behavior and their pain points on your forms.',
118
  'price' => '79EUR / 89USD',
119
  'url' => 'https://plugins.matomo.org/FormAnalytics?wp=1',
120
- 'image' => plugins_url( 'assets/img/form_analytics.jpg', MATOMO_ANALYTICS_FILE ),
121
  ),
122
  array(
123
  'name' => 'Media Analytics',
124
  'description' => 'Grow your business with advanced video & audio analytics. Get powerful insights into how your audience watches your videos and listens to your audio.',
125
  'price' => '79EUR / 89USD',
126
  'url' => 'https://plugins.matomo.org/MediaAnalytics?wp=1',
127
- 'image' => plugins_url( 'assets/img/media_analytics.jpg', MATOMO_ANALYTICS_FILE ),
128
  ),
129
  array(
130
  'name' => 'Users Flow',
131
  'description' => 'Users Flow is a visual representation of the most popular paths your users take through your website & app which lets you understand your users needs.',
132
  'price' => '39EUR / 39USD',
133
  'url' => 'https://plugins.matomo.org/UsersFlow?wp=1',
134
- 'image' => plugins_url( 'assets/img/users_flow.png', MATOMO_ANALYTICS_FILE ),
135
  ),
136
  ),
137
  ),
@@ -144,14 +146,14 @@ $matomo_extra_url_params = '&' . http_build_query(
144
  'description' => 'Identify and understand where your visitors drop off to increase your conversions, sales and revenue with your existing traffic.',
145
  'price' => '89EUR / 99USD',
146
  'url' => 'https://plugins.matomo.org/Funnels?wp=1',
147
- 'image' => plugins_url( 'assets/img/funnels.png', MATOMO_ANALYTICS_FILE ),
148
  ),
149
  array(
150
  'name' => 'Multi Attribution',
151
  'description' => 'Get a clear understanding of how much credit each of your marketing channel is actually responsible for to shift your marketing efforts wisely.',
152
  'price' => '39EUR / 39USD',
153
  'url' => 'https://plugins.matomo.org/MultiChannelConversionAttribution?wp=1',
154
- 'image' => plugins_url( 'assets/img/multi_attribution.png', MATOMO_ANALYTICS_FILE ),
155
  ),
156
  ),
157
  ),
@@ -164,14 +166,14 @@ $matomo_extra_url_params = '&' . http_build_query(
164
  'description' => 'Track your retention efforts over time and keep your visitors engaged and coming back for more.',
165
  'price' => '49EUR / 59USD',
166
  'url' => 'https://plugins.matomo.org/Cohorts?wp=1',
167
- 'image' => plugins_url( 'assets/img/cohorts.png', MATOMO_ANALYTICS_FILE ),
168
  ),
169
  array(
170
  'name' => 'Search Engine Keywords Performance',
171
  'description' => 'All keywords searched by your users on search engines are now visible into your Referrers reports! The ultimate solution to \'Keyword not defined\'.',
172
  'price' => '69EUR / 79USD',
173
  'url' => 'https://plugins.matomo.org/SearchEngineKeywordsPerformance?wp=1',
174
- 'image' => plugins_url( 'assets/img/search_engine_keywords.png', MATOMO_ANALYTICS_FILE ),
175
  ),
176
  /*
177
  array(
@@ -179,7 +181,7 @@ $matomo_extra_url_params = '&' . http_build_query(
179
  'description' => 'Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions',
180
  'price' => '19EUR / 19USD',
181
  'url' => 'https://plugins.matomo.org/ActivityLog?wp=1',
182
- 'image' => plugins_url( 'assets/img/activity_log.jpg', MATOMO_ANALYTICS_FILE ),
183
  ),*/
184
  ),
185
  ),
@@ -213,7 +215,7 @@ $matomo_extra_url_params = '&' . http_build_query(
213
  <?php
214
  if ( ! $matomo_is_3_columns ) {
215
  ?>
216
- name column-name<?php } ?>" style="margin-right: 0">
217
  <h3>
218
  <a href="<?php echo esc_url( $matomo_feature['url'] ); ?>"
219
  rel="noreferrer noopener" target="_blank"
@@ -237,7 +239,7 @@ $matomo_extra_url_params = '&' . http_build_query(
237
  if ( ! $matomo_is_3_columns ) {
238
  ?>
239
  desc column-description<?php } ?>"
240
- style="margin-right: 0">
241
  <p class="matomo-description"><?php echo esc_html( $matomo_feature['description'] ); ?></p>
242
  <p class="authors"><a class="button-primary"
243
  rel="noreferrer noopener" target="_blank"
@@ -256,7 +258,12 @@ $matomo_extra_url_params = '&' . http_build_query(
256
  </div>
257
  <?php
258
  }
259
- echo '<div style="clear:both;"></div></div></div>';
 
 
 
 
 
260
  }
261
  ?>
262
  </div>
30
 
31
  <div id="icon-plugins" class="icon32"></div>
32
 
33
+ <h1><?php matomo_header_icon(); ?> <?php esc_html_e( 'Discover new functionality for your Matomo', 'matomo' ); ?></h1>
34
  <p>
35
  <?php esc_html_e( 'Take your Matomo (formerly Piwik) to the next level and drive your conversions & revenue with these premium features. All features are fully hosted on your WordPress and come with 100% data ownership and no limitations.', 'matomo' ); ?>
36
  <?php if ( is_plugin_active( MATOMO_MARKETPLACE_PLUGIN_NAME ) ) { ?>
43
  <h2><?php echo sprintf( esc_html__( 'Easily install over 100 free plugins & %1$spremium features%2$s for Matomo with just a click' ), '<span style="white-space: nowrap;">', '</span>' ); ?></h2>
44
  <a href="https://builds.matomo.org/matomo-marketplace-for-wordpress-latest.zip" rel="noreferrer noopener" class="button matomo-cta-button"><?php esc_html_e( 'Download Matomo Marketplace for WordPress', 'matomo' ); ?></a>
45
  <br>
46
+ <a target="_blank" href="https://matomo.org/faq/wordpress/how-do-i-install-a-matomo-marketplace-plugin-in-matomo-for-wordpress/"><span class="dashicons-before dashicons-video-alt3"></span></a> <a target="_blank" href="https://matomo.org/faq/wordpress/how-do-i-install-a-matomo-marketplace-plugin-in-matomo-for-wordpress/"><?php esc_html_e( 'Install instructions', 'matomo' ); ?></a>
47
  <a target="_blank" href="https://plugins.matomo.org/?wp=1" rel="noreferrer noopener" class="matomo-next-link"><?php esc_html_e( 'Browse Marketplace', 'matomo' ); ?></a>
48
  </div>
49
  <?php } ?>
52
  $matomo_feature_sections = array(
53
  array(
54
  'title' => 'Top free plugins',
55
+ 'more_url' => 'https://plugins.matomo.org/free?wp=1',
56
+ 'more_text' => 'Browse all free plugins',
57
  'features' =>
58
  array(
59
  array(
91
  'description' => 'Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions.',
92
  'price' => '99EUR / 119USD',
93
  'url' => 'https://plugins.matomo.org/HeatmapSessionRecording?wp=1',
94
+ 'image' => '',
95
  ),
96
  array(
97
  'name' => 'Custom Reports',
98
  'description' => 'Pull out the information you need in order to be successful. Develop your custom strategy to meet your individualized goals while saving money & time.',
99
  'price' => '99EUR / 119USD',
100
  'url' => 'https://plugins.matomo.org/CustomReports?wp=1',
101
+ 'image' => '',
102
  ),
103
 
104
  array(
106
  'description' => 'All premium features in one bundle, make the most out of your Matomo for WordPress and enjoy discounts of over 20%!',
107
  'price' => '499EUR / 579USD',
108
  'url' => 'https://plugins.matomo.org/WpPremiumBundle?wp=1',
109
+ 'image' => '',
110
  ),
111
  ),
112
  ),
119
  'description' => 'Increase conversions on your online forms and lose less visitors by learning everything about your users behavior and their pain points on your forms.',
120
  'price' => '79EUR / 89USD',
121
  'url' => 'https://plugins.matomo.org/FormAnalytics?wp=1',
122
+ 'image' => '',
123
  ),
124
  array(
125
  'name' => 'Media Analytics',
126
  'description' => 'Grow your business with advanced video & audio analytics. Get powerful insights into how your audience watches your videos and listens to your audio.',
127
  'price' => '79EUR / 89USD',
128
  'url' => 'https://plugins.matomo.org/MediaAnalytics?wp=1',
129
+ 'image' => '',
130
  ),
131
  array(
132
  'name' => 'Users Flow',
133
  'description' => 'Users Flow is a visual representation of the most popular paths your users take through your website & app which lets you understand your users needs.',
134
  'price' => '39EUR / 39USD',
135
  'url' => 'https://plugins.matomo.org/UsersFlow?wp=1',
136
+ 'image' => '',
137
  ),
138
  ),
139
  ),
146
  'description' => 'Identify and understand where your visitors drop off to increase your conversions, sales and revenue with your existing traffic.',
147
  'price' => '89EUR / 99USD',
148
  'url' => 'https://plugins.matomo.org/Funnels?wp=1',
149
+ 'image' => '',
150
  ),
151
  array(
152
  'name' => 'Multi Attribution',
153
  'description' => 'Get a clear understanding of how much credit each of your marketing channel is actually responsible for to shift your marketing efforts wisely.',
154
  'price' => '39EUR / 39USD',
155
  'url' => 'https://plugins.matomo.org/MultiChannelConversionAttribution?wp=1',
156
+ 'image' => '',
157
  ),
158
  ),
159
  ),
166
  'description' => 'Track your retention efforts over time and keep your visitors engaged and coming back for more.',
167
  'price' => '49EUR / 59USD',
168
  'url' => 'https://plugins.matomo.org/Cohorts?wp=1',
169
+ 'image' => '',
170
  ),
171
  array(
172
  'name' => 'Search Engine Keywords Performance',
173
  'description' => 'All keywords searched by your users on search engines are now visible into your Referrers reports! The ultimate solution to \'Keyword not defined\'.',
174
  'price' => '69EUR / 79USD',
175
  'url' => 'https://plugins.matomo.org/SearchEngineKeywordsPerformance?wp=1',
176
+ 'image' => '',
177
  ),
178
  /*
179
  array(
181
  'description' => 'Truly understand your visitors by seeing where they click, hover, type and scroll. Replay their actions in a video and ultimately increase conversions',
182
  'price' => '19EUR / 19USD',
183
  'url' => 'https://plugins.matomo.org/ActivityLog?wp=1',
184
+ 'image' => '',
185
  ),*/
186
  ),
187
  ),
215
  <?php
216
  if ( ! $matomo_is_3_columns ) {
217
  ?>
218
+ name column-name<?php } ?>" style="margin-right: 0;<?php if ( empty( $matomo_feature['image'] )) { echo 'margin-left: 0;'; } ?>">
219
  <h3>
220
  <a href="<?php echo esc_url( $matomo_feature['url'] ); ?>"
221
  rel="noreferrer noopener" target="_blank"
239
  if ( ! $matomo_is_3_columns ) {
240
  ?>
241
  desc column-description<?php } ?>"
242
+ style="margin-right: 0;<?php if ( empty( $matomo_feature['image'] )) { echo 'margin-left: 0;'; } ?>">
243
  <p class="matomo-description"><?php echo esc_html( $matomo_feature['description'] ); ?></p>
244
  <p class="authors"><a class="button-primary"
245
  rel="noreferrer noopener" target="_blank"
258
  </div>
259
  <?php
260
  }
261
+ echo '<div style="clear:both;"></div>';
262
+ echo '</div>';
263
+ if (!empty($matomo_feature_section['more_url'])) {
264
+ echo '<a target="_blank" rel="noreferrer noopener" href="'.esc_attr($matomo_feature_section['more_url']).'"><span class="dashicons dashicons-arrow-right-alt2"></span>'. esc_html($matomo_feature_section['more_text']).'</a>';
265
+ }
266
+ echo '</div>';
267
  }
268
  ?>
269
  </div>
classes/WpMatomo/Admin/views/settings.php CHANGED
@@ -21,6 +21,7 @@ if ( ! defined( 'ABSPATH' ) ) {
21
  ?>
22
  <div class="wrap">
23
  <div id="icon-plugins" class="icon32"></div>
 
24
  <h2 class="nav-tab-wrapper">
25
  <?php foreach ( $setting_tabs as $matomo_setting_slug => $matomo_setting_tab ) { ?>
26
  <a href="<?php echo AdminSettings::make_url( $matomo_setting_slug ); ?>"
21
  ?>
22
  <div class="wrap">
23
  <div id="icon-plugins" class="icon32"></div>
24
+ <h1><?php matomo_header_icon(); ?> <?php esc_html_e( 'Settings', 'matomo' ); ?></h1>
25
  <h2 class="nav-tab-wrapper">
26
  <?php foreach ( $setting_tabs as $matomo_setting_slug => $matomo_setting_tab ) { ?>
27
  <a href="<?php echo AdminSettings::make_url( $matomo_setting_slug ); ?>"
classes/WpMatomo/Admin/views/summary.php CHANGED
@@ -28,7 +28,7 @@ global $wp;
28
  <?php } ?>
29
  <div class="wrap">
30
  <div id="icon-plugins" class="icon32"></div>
31
- <h1><?php esc_html_e( 'Summary', 'matomo' ); ?></h1>
32
  <?php
33
  if ( Dates::TODAY === $report_date ) {
34
  echo '<div class="notice notice-info" style="padding:8px;">' . esc_html__( 'Reports for today are only refreshed approximately every hour through the WordPress cronjob.', 'matomo' ) . '</div>';
@@ -79,7 +79,7 @@ global $wp;
79
  )
80
  );
81
  ?>
82
- " style="color: inherit;" target="_blank" rel="noreferrer noopener"
83
  class="dashicons-before dashicons-external" aria-hidden="true"></a></button>
84
  <?php } ?>
85
  <h2 class="hndle ui-sortable-handle"
28
  <?php } ?>
29
  <div class="wrap">
30
  <div id="icon-plugins" class="icon32"></div>
31
+ <h1><?php matomo_header_icon(); ?> <?php esc_html_e( 'Summary', 'matomo' ); ?></h1>
32
  <?php
33
  if ( Dates::TODAY === $report_date ) {
34
  echo '<div class="notice notice-info" style="padding:8px;">' . esc_html__( 'Reports for today are only refreshed approximately every hour through the WordPress cronjob.', 'matomo' ) . '</div>';
79
  )
80
  );
81
  ?>
82
+ " style="color: inherit;text-decoration: none;" target="_blank" rel="noreferrer noopener"
83
  class="dashicons-before dashicons-external" aria-hidden="true"></a></button>
84
  <?php } ?>
85
  <h2 class="hndle ui-sortable-handle"
classes/WpMatomo/Admin/views/systemreport.php CHANGED
@@ -57,6 +57,8 @@ if ( ! function_exists( 'matomo_format_value_text' ) ) {
57
  </div>
58
  <?php } ?>
59
  <div id="icon-plugins" class="icon32"></div>
 
 
60
  <h2 class="nav-tab-wrapper">
61
  <a href="?page=<?php echo Menu::SLUG_SYSTEM_REPORT; ?>"
62
  class="nav-tab <?php echo empty( $matomo_active_tab ) ? 'nav-tab-active' : ''; ?>"> System report</a>
57
  </div>
58
  <?php } ?>
59
  <div id="icon-plugins" class="icon32"></div>
60
+ <h1><?php matomo_header_icon(); ?> <?php esc_html_e( 'Diagnostics', 'matomo' ); ?></h1>
61
+
62
  <h2 class="nav-tab-wrapper">
63
  <a href="?page=<?php echo Menu::SLUG_SYSTEM_REPORT; ?>"
64
  class="nav-tab <?php echo empty( $matomo_active_tab ) ? 'nav-tab-active' : ''; ?>"> System report</a>
classes/WpMatomo/Installer.php CHANGED
@@ -25,6 +25,8 @@ if ( ! defined( 'ABSPATH' ) ) {
25
 
26
  class Installer {
27
 
 
 
28
  /**
29
  * @var Settings
30
  */
@@ -111,6 +113,8 @@ class Installer {
111
  // also to set up all the other users
112
  wp_schedule_single_event( time() + 35, ScheduledTasks::EVENT_SYNC );
113
 
 
 
114
  $this->create_website();
115
  $this->create_user(); // we sync users as early as possible to make sure things are set up correctly
116
  $this->install_tracker();
25
 
26
  class Installer {
27
 
28
+ const OPTION_NAME_INSTALL_DATE = 'matomo-install-date';
29
+
30
  /**
31
  * @var Settings
32
  */
113
  // also to set up all the other users
114
  wp_schedule_single_event( time() + 35, ScheduledTasks::EVENT_SYNC );
115
 
116
+ update_option(self::OPTION_NAME_INSTALL_DATE, time());
117
+
118
  $this->create_website();
119
  $this->create_user(); // we sync users as early as possible to make sure things are set up correctly
120
  $this->install_tracker();
classes/WpMatomo/Referral.php ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 WP_Roles;
13
+ use WpMatomo\Admin\Menu;
14
+
15
+ if ( ! defined( 'ABSPATH' ) ) {
16
+ exit; // if accessed directly
17
+ }
18
+
19
+ /**
20
+ * Every 90 days we show a please review notice until the user dismisses this notice or clicks on rate us.
21
+ * We only show this notice on Matomo screens.
22
+ *
23
+ * @package WpMatomo
24
+ */
25
+ class Referral {
26
+
27
+ const OPTION_NAME_REFERRAL_DISMISSED = 'matomo-referral-dismissed';
28
+
29
+ /**
30
+ * @var int
31
+ */
32
+ private $time;
33
+
34
+ public function __construct() {
35
+ $this->time = time();
36
+ }
37
+
38
+ /**
39
+ * @internal for tests only
40
+ * @param int $time
41
+ */
42
+ public function set_time( $time ) {
43
+ $this->time = $time;
44
+ }
45
+
46
+ public function register_hooks() {
47
+ $self = $this;
48
+
49
+ add_action(
50
+ 'wp_ajax_matomo_referral_dismiss_admin_notice',
51
+ function () use ( $self ) {
52
+ if ( is_admin() && $self->should_show() && $self->can_refer() ) {
53
+ // no need for an nonce check here as it's nothing critical
54
+ if ( ! empty( $_POST['forever'] ) ) {
55
+ $self->dismiss_forever();
56
+ } else {
57
+ $self->dismiss();
58
+ }
59
+ }
60
+ }
61
+ );
62
+ add_action(
63
+ 'admin_notices',
64
+ function () use ( $self ) {
65
+ if ( $self->can_refer() && $self->should_show_on_screen() ) {
66
+ $self->render();
67
+ }
68
+ }
69
+ );
70
+ }
71
+
72
+ public function render() {
73
+ include 'views/referral.php';
74
+ }
75
+
76
+ public function should_show_on_screen() {
77
+ if ( ! is_admin() ) {
78
+ return false;
79
+ }
80
+ $screen = get_current_screen();
81
+ return $screen && $screen->id && strpos( $screen->id, 'matomo-' ) === 0;
82
+ }
83
+
84
+ public function can_refer() {
85
+ return current_user_can( Capabilities::KEY_VIEW );
86
+ }
87
+
88
+ public function dismiss_forever() {
89
+ $tenYears = 60 * 60 * 24 * 365 * 10;
90
+ update_option( self::OPTION_NAME_REFERRAL_DISMISSED, $this->time + $tenYears );
91
+ }
92
+
93
+ public function dismiss() {
94
+ update_option( self::OPTION_NAME_REFERRAL_DISMISSED, $this->time, true );
95
+ }
96
+
97
+ public function get_last_dismissed() {
98
+ return get_option( self::OPTION_NAME_REFERRAL_DISMISSED );
99
+ }
100
+
101
+ private function get_days_in_seconds( $num_days ) {
102
+ return 60 * 60 * 24 * $num_days;
103
+ }
104
+
105
+ public function should_show() {
106
+ $dismissed = $this->get_last_dismissed();
107
+
108
+ if ( ! $dismissed ) {
109
+ // the first time we check... we set it back 30 days cause we want to see first rating after 60 days
110
+ $this->time = $this->time - $this->get_days_in_seconds( 30 );
111
+ $this->dismiss();
112
+ return false;
113
+ }
114
+
115
+ $ninetyDaysInSeconds = $this->get_days_in_seconds( 90 );
116
+
117
+ if ( $this->time > ( $dismissed + $ninetyDaysInSeconds ) ) {
118
+ return true;
119
+ }
120
+
121
+ return false;
122
+ }
123
+
124
+
125
+ }
classes/WpMatomo/ScheduledTasks.php CHANGED
@@ -47,9 +47,9 @@ class ScheduledTasks {
47
  }
48
 
49
  public function add_weekly_schedule( $schedules ) {
50
- $schedules['matomo_weekly'] = array(
51
- 'interval' => 60 * 60 * 24 * 7, // 604,800, seconds in a week
52
- 'display' => __( 'Weekly', 'matomo' ),
53
  );
54
 
55
  return $schedules;
@@ -126,7 +126,7 @@ class ScheduledTasks {
126
  ),
127
  self::EVENT_GEOIP => array(
128
  'name' => 'Update GeoIP DB',
129
- 'interval' => 'matomo_weekly',
130
  'method' => 'update_geo_ip2_db',
131
  ),
132
  );
@@ -148,7 +148,15 @@ class ScheduledTasks {
148
  $this->logger->log( 'Scheduled tasks update geoip database' );
149
  try {
150
  Bootstrap::do_bootstrap();
151
- Option::set( GeoIP2AutoUpdater::LOC_URL_OPTION_NAME, GeoIp2::getDbIpLiteUrl());
 
 
 
 
 
 
 
 
152
  $updater = new GeoIP2AutoUpdater();
153
  $updater->update();
154
  if ( LocationProvider::getCurrentProviderId() !== Php::ID && LocationProvider::getProviderById( Php::ID ) ) {
47
  }
48
 
49
  public function add_weekly_schedule( $schedules ) {
50
+ $schedules['matomo_monthly'] = array(
51
+ 'interval' => 60 * 60 * 24 * 30,
52
+ 'display' => __( 'Monthly', 'matomo' ),
53
  );
54
 
55
  return $schedules;
126
  ),
127
  self::EVENT_GEOIP => array(
128
  'name' => 'Update GeoIP DB',
129
+ 'interval' => 'matomo_monthly',
130
  'method' => 'update_geo_ip2_db',
131
  ),
132
  );
148
  $this->logger->log( 'Scheduled tasks update geoip database' );
149
  try {
150
  Bootstrap::do_bootstrap();
151
+
152
+ $maxmind_license = $this->settings->get_global_option('maxmind_license_key');
153
+ if (empty($maxmind_license)) {
154
+ $db_url = GeoIp2::getDbIpLiteUrl();
155
+ } else {
156
+ $db_url = 'https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&suffix=tar.gz&license_key=' . $maxmind_license;
157
+ }
158
+
159
+ Option::set( GeoIP2AutoUpdater::LOC_URL_OPTION_NAME, $db_url);
160
  $updater = new GeoIP2AutoUpdater();
161
  $updater->update();
162
  if ( LocationProvider::getCurrentProviderId() !== Php::ID && LocationProvider::getProviderById( Php::ID ) ) {
classes/WpMatomo/Settings.php CHANGED
@@ -91,6 +91,7 @@ class Settings {
91
  'track_user_id' => 'disabled',
92
  'track_datacfasync' => false,
93
  'force_protocol' => 'disabled',
 
94
  self::SHOW_GET_STARTED_PAGE => 1,
95
  );
96
 
91
  'track_user_id' => 'disabled',
92
  'track_datacfasync' => false,
93
  'force_protocol' => 'disabled',
94
+ 'maxmind_license_key' => '',
95
  self::SHOW_GET_STARTED_PAGE => 1,
96
  );
97
 
classes/WpMatomo/Site/Sync.php CHANGED
@@ -233,8 +233,11 @@ class Sync {
233
 
234
  private function check_and_try_to_set_default_timezone( $timezone ) {
235
  try {
236
- SitesManager\API::unsetInstance(); // make sure we're loading the latest instance with all up to date dependencies... mainly needed for tests
237
- SitesManager\API::getInstance()->setDefaultTimezone( $timezone );
 
 
 
238
  } catch ( \Exception $e ) {
239
  return false;
240
  }
233
 
234
  private function check_and_try_to_set_default_timezone( $timezone ) {
235
  try {
236
+ Access::doAsSuperUser(function () use ($timezone) {
237
+ // make sure we're loading the latest instance with all up to date dependencies... mainly needed for tests
238
+ SitesManager\API::unsetInstance();
239
+ SitesManager\API::getInstance()->setDefaultTimezone( $timezone );
240
+ });
241
  } catch ( \Exception $e ) {
242
  return false;
243
  }
classes/WpMatomo/TrackingCode.php CHANGED
@@ -54,9 +54,11 @@ class TrackingCode {
54
  add_filter( 'wp_redirect', array( $this, 'forward_cross_domain_visitor_id' ) );
55
  }
56
 
57
- if ( ! is_admin() || $this->settings->is_admin_tracking_enabled() ) {
 
 
58
  $prefix = 'wp';
59
- if ( is_admin() ) {
60
  $prefix = 'admin';
61
  }
62
 
54
  add_filter( 'wp_redirect', array( $this, 'forward_cross_domain_visitor_id' ) );
55
  }
56
 
57
+ $is_admin = is_admin() || !empty($GLOBALS['MATOMO_LOADED_DIRECTLY']);
58
+
59
+ if ( ! $is_admin || $this->settings->is_admin_tracking_enabled() ) {
60
  $prefix = 'wp';
61
+ if ( $is_admin ) {
62
  $prefix = 'admin';
63
  }
64
 
classes/WpMatomo/Updater.php CHANGED
@@ -130,7 +130,7 @@ class Updater {
130
  @file_put_contents( $upload_dir . '/index.htm', '//hello' );
131
  @file_put_contents(
132
  $upload_dir . '/.htaccess',
133
- '<Files GeoLite2-City.mmdb>
134
  ' . ServerFilesGenerator::getDenyHtaccessContent() . '
135
  </Files>
136
  <Files ~ "(\.js)$">
130
  @file_put_contents( $upload_dir . '/index.htm', '//hello' );
131
  @file_put_contents(
132
  $upload_dir . '/.htaccess',
133
+ '<Files ~ "(\.mmdb)$">
134
  ' . ServerFilesGenerator::getDenyHtaccessContent() . '
135
  </Files>
136
  <Files ~ "(\.js)$">
classes/WpMatomo/views/referral.php ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ if ( ! defined( 'ABSPATH' ) ) {
11
+ exit;
12
+ }
13
+ ?>
14
+ <div class="notice notice-info is-dismissible" id="matomo-referral">
15
+ <p>
16
+ <?php esc_html_e( 'Like Matomo? We would really appreciate if you took 1 minute to rate us.', 'matomo' ); ?>
17
+
18
+ <a href="https://wordpress.org/support/plugin/matomo/reviews/?rate=5#new-post" target="_blank" rel="noreferrer noopener"
19
+ class="button matomo-dismiss-forever"><?php esc_html_e( 'Rate Matomo', 'matomo' ) ?></a>
20
+ </p>
21
+ <div style="clear:both;"></div>
22
+ </div>
matomo.php CHANGED
@@ -4,10 +4,10 @@
4
  * Description: The #1 Google Analytics alternative that gives you full control over your data and protects the privacy for your users. Free, secure and open.
5
  * Author: Matomo
6
  * Author URI: https://matomo.org
7
- * Version: 1.0.3
8
  * Domain Path: /languages
9
  * WC requires at least: 2.4.0
10
- * WC tested up to: 3.9.2
11
  *
12
  * Matomo - free/libre analytics platform
13
  *
@@ -36,7 +36,8 @@ $GLOBALS['MATOMO_PLUGINS_ENABLED'] = array();
36
  $GLOBALS['MATOMO_PLUGIN_FILES'] = array( MATOMO_ANALYTICS_FILE );
37
 
38
  function matomo_has_compatible_content_dir() {
39
- if ( !empty( $_ENV['MATOMO_WP_ROOT_PATH'] ) && is_dir( $_ENV['MATOMO_WP_ROOT_PATH'] ) ) {
 
40
  return true;
41
  }
42
 
@@ -54,7 +55,52 @@ function matomo_has_compatible_content_dir() {
54
  $absPath . DIRECTORY_SEPARATOR . 'wp-content'
55
  );
56
 
57
- return in_array($contentDir, $absPaths, true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
 
60
  function matomo_is_app_request() {
4
  * Description: The #1 Google Analytics alternative that gives you full control over your data and protects the privacy for your users. Free, secure and open.
5
  * Author: Matomo
6
  * Author URI: https://matomo.org
7
+ * Version: 1.0.4
8
  * Domain Path: /languages
9
  * WC requires at least: 2.4.0
10
+ * WC tested up to: 4.0.0
11
  *
12
  * Matomo - free/libre analytics platform
13
  *
36
  $GLOBALS['MATOMO_PLUGIN_FILES'] = array( MATOMO_ANALYTICS_FILE );
37
 
38
  function matomo_has_compatible_content_dir() {
39
+ if ( !empty( $_SERVER['MATOMO_WP_ROOT_PATH'] )
40
+ && file_exists( rtrim($_SERVER['MATOMO_WP_ROOT_PATH'], '/') . '/wp-load.php' ) ) {
41
  return true;
42
  }
43
 
55
  $absPath . DIRECTORY_SEPARATOR . 'wp-content'
56
  );
57
 
58
+ if (in_array($contentDir, $absPaths, true)) {
59
+ return true;
60
+ }
61
+
62
+ $wpload_base = '../../../wp-load.php';
63
+ $wpload_full = dirname( __FILE__ ) . '/' . $wpload_base;
64
+ if ( file_exists($wpload_full ) && is_readable( $wpload_full ) ) {
65
+ return true;
66
+ } elseif (realpath( $wpload_full ) && file_exists(realpath( $wpload_full )) && is_readable(realpath( $wpload_full ))) {
67
+ return true;
68
+ } elseif (!empty($_SERVER['SCRIPT_FILENAME']) && file_exists($_SERVER['SCRIPT_FILENAME'])) {
69
+ // seems symlinked... eg the wp-content dir or wp-content/plugins dir is symlinked from some very much other place...
70
+ $wpload_full = dirname($_SERVER['SCRIPT_FILENAME']) . '/' . $wpload_base;
71
+ if ( file_exists($wpload_full ) ) {
72
+ return true;
73
+ } elseif (realpath( $wpload_full ) && file_exists(realpath( $wpload_full ))) {
74
+ return true;
75
+ } elseif (file_exists(dirname( $_SERVER['SCRIPT_FILENAME'] )) . '/wp-load.php') {
76
+ return true;
77
+ }
78
+ }
79
+
80
+ // look in plugins directory if there is a config file for us
81
+ $wpload_config = dirname(__FILE__) . '/../matomo.wpload_dir.php';
82
+ if (file_exists( $wpload_config) && is_readable($wpload_config)) {
83
+ $content = @file_get_contents($wpload_config); // we do not include that file for security reasons
84
+ if (!empty($content)) {
85
+ $content = str_replace(array('<?php', 'exit;'), '', $content);
86
+ $content = preg_replace('/\s/', '', $content);
87
+ $content = trim(ltrim(trim($content), '#')); // the path may be commented out # /abs/path
88
+ if (strpos($content, DIRECTORY_SEPARATOR) === 0) {
89
+ $wpload_file = rtrim($content, DIRECTORY_SEPARATOR) . '/wp-load.php';
90
+ return file_exists($wpload_file) && is_readable($wpload_file);
91
+ }
92
+ }
93
+ }
94
+
95
+ return false;
96
+ }
97
+
98
+ function matomo_header_icon( $full = false ) {
99
+ $file = 'logo';
100
+ if ($full) {
101
+ $file = 'logo-full';
102
+ }
103
+ echo '<img height="32" src="' . plugins_url( 'assets/img/'.$file.'.png', MATOMO_ANALYTICS_FILE ) . '" class="matomo-header-icon">';
104
  }
105
 
106
  function matomo_is_app_request() {
plugins/WordPress/Controller.php CHANGED
@@ -18,7 +18,8 @@ class Controller extends \Piwik\Plugin\Controller
18
  public function index()
19
  {
20
  if (!is_user_logged_in()) {
21
- wp_safe_redirect(wp_login_url(\WpMatomo\Admin\Menu::get_reporting_url()));
 
22
  }
23
  }
24
 
18
  public function index()
19
  {
20
  if (!is_user_logged_in()) {
21
+ $redirect_url = WordPress::getWpLoginUrl();
22
+ wp_safe_redirect($redirect_url);
23
  }
24
  }
25
 
plugins/WordPress/WordPress.php CHANGED
@@ -334,6 +334,22 @@ class WordPress extends Plugin
334
  }
335
  }
336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  public function noAccess(Exception $exception)
338
  {
339
  if (Common::isXmlHttpRequest()) {
@@ -342,7 +358,8 @@ class WordPress extends Plugin
342
  return;
343
  }
344
 
345
- wp_redirect(wp_login_url(\WpMatomo\Admin\Menu::get_reporting_url()));
 
346
  exit;
347
  }
348
 
334
  }
335
  }
336
 
337
+ public static function getWpLoginUrl()
338
+ {
339
+ $forceFrontPage = defined('MATOMO_LOGIN_REDIRECT') && MATOMO_LOGIN_REDIRECT === 'frontpage';
340
+ $forceLoginUrl = defined('MATOMO_LOGIN_REDIRECT') && MATOMO_LOGIN_REDIRECT === 'login';
341
+
342
+ if (!$forceLoginUrl &&
343
+ ($forceFrontPage
344
+ || is_plugin_active('wps-hide-login/wps-hide-login.php'))) {
345
+ $redirect_url = home_url();
346
+ } else {
347
+ $redirect_url = wp_login_url(\WpMatomo\Admin\Menu::get_reporting_url());
348
+ }
349
+
350
+ return $redirect_url;
351
+ }
352
+
353
  public function noAccess(Exception $exception)
354
  {
355
  if (Common::isXmlHttpRequest()) {
358
  return;
359
  }
360
 
361
+ $redirect_url = WordPress::getWpLoginUrl();
362
+ wp_safe_redirect($redirect_url);
363
  exit;
364
  }
365
 
plugins/WordPress/WpAssetManager.php CHANGED
@@ -57,7 +57,8 @@ class WpAssetManager extends AssetManager
57
  $jsFiles[] = 'jquery/ui/effect.min.js';
58
 
59
  foreach ($jsFiles as $jsFile) {
60
- $result .= sprintf(self::JS_IMPORT_DIRECTIVE, '../../../../wp-includes/js/' . $jsFile);
 
61
  }
62
 
63
  $result .= "<script type=\"text/javascript\">window.$ = jQuery;</script>";
57
  $jsFiles[] = 'jquery/ui/effect.min.js';
58
 
59
  foreach ($jsFiles as $jsFile) {
60
+ $jQueryPath = includes_url('js/' . $jsFile);
61
+ $result .= sprintf(self::JS_IMPORT_DIRECTIVE, $jQueryPath);
62
  }
63
 
64
  $result .= "<script type=\"text/javascript\">window.$ = jQuery;</script>";
readme.txt CHANGED
@@ -4,7 +4,7 @@ Donate link: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_i
4
  Tags: matomo,piwik,analytics,statistics,stats,tracking,ecommerce
5
  Requires at least: 4.8
6
  Tested up to: 5.4
7
- Stable tag: 1.0.3
8
  Requires PHP: 7.2
9
  License: GPLv3 or later
10
  License URI: https://www.gnu.org/licenses/gpl-3.0.html
@@ -13,6 +13,8 @@ Matomo is the #1 Google Analytics alternative that gives you full control over y
13
 
14
  == Description ==
15
 
 
 
16
  For all you WordPress website owners wanting an easier way to get customer insights to grow your business, you can now get the solution the professionals use, for free!
17
 
18
  Matomo Analytics is the #1 used Google Analytics alternative that offers a powerful range of features, security and protects the privacy of your users. This enables you to learn how to improve your website, make the right decisions for your business and stand out in the crowd in a safe and trustworthy way.
@@ -158,3 +160,20 @@ Needing to know more? [Click here to view all of our FAQs on our website](https:
158
  9. Options to anonymize data so you don't track personal data.
159
  10. Automatically delete old data you no longer need to be privacy compliant and to free your server from not needed data.
160
  11. Summary page for getting a quick overview.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  Tags: matomo,piwik,analytics,statistics,stats,tracking,ecommerce
5
  Requires at least: 4.8
6
  Tested up to: 5.4
7
+ Stable tag: 1.0.4
8
  Requires PHP: 7.2
9
  License: GPLv3 or later
10
  License URI: https://www.gnu.org/licenses/gpl-3.0.html
13
 
14
  == Description ==
15
 
16
+ If you are already using Matomo On-Premise (formerly Piwik) or Matomo Cloud, please use the [WP-Matomo plugin](https://wordpress.org/plugins/wp-piwik/).
17
+
18
  For all you WordPress website owners wanting an easier way to get customer insights to grow your business, you can now get the solution the professionals use, for free!
19
 
20
  Matomo Analytics is the #1 used Google Analytics alternative that offers a powerful range of features, security and protects the privacy of your users. This enables you to learn how to improve your website, make the right decisions for your business and stand out in the crowd in a safe and trustworthy way.
160
  9. Options to anonymize data so you don't track personal data.
161
  10. Automatically delete old data you no longer need to be privacy compliant and to free your server from not needed data.
162
  11. Summary page for getting a quick overview.
163
+
164
+ == Changelog ==
165
+
166
+ = 1.0.4 =
167
+ * Update Matomo core to 3.13.4
168
+ * Fix the website's timezone may be set to UTC instead of the WP timezone
169
+ * Improve compatibility with PHP 7.4 by fixing more notices
170
+ * Add a review link to the About page
171
+ * Add a newsletter signup possibility to the About page.
172
+ * Support MaxMind geolocation database
173
+ * Better support for hiding login URLs eg with WPS plugin
174
+ * Show header icon images
175
+ * Update GeoIP DB monthly instead of weekly
176
+ * Ask for a review every 90 days unless dismissed
177
+ * Possibility to configure proxy client header
178
+
179
+ [See changelog for all versions](https://github.com/matomo-org/wp-matomo/blob/develop/CHANGELOG.md).