PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.0.3
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.0.3
5.13.0 5.12.1 5.12.0 5.11.1 5.11.0 5.10.2 5.10.1 trunk 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.3.0 1.3.1 1.3.2 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.1.0 4.1.1 4.1.2 4.1.3 4.10.0 4.11.0 4.12.0 4.13.0 4.13.2 4.13.3 4.13.4 4.13.5 4.14.0 4.14.1 4.14.2 4.15.0 4.15.1 4.15.2 4.15.3 4.2.0 4.3.0 4.3.1 4.4.1 4.4.2 4.5.0 4.6.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 5.0.8 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.10.0 5.2.0 5.2.1 5.2.2 5.3.0 5.3.1 5.3.2 5.3.3 5.6.0 5.6.1 5.7.0 5.7.1 5.8.0 5.8.1 5.8.2
matomo / app / core / ArchiveProcessor / Loader.php
matomo / app / core / ArchiveProcessor Last commit date
ArchivingStatus.php 5 years ago Loader.php 5 years ago Parameters.php 5 years ago PluginsArchiver.php 5 years ago PluginsArchiverException.php 5 years ago Rules.php 5 years ago
Loader.php
454 lines
1 <?php
2 /**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9 namespace Piwik\ArchiveProcessor;
10
11 use Piwik\Archive\ArchiveInvalidator;
12 use Piwik\Cache;
13 use Piwik\Common;
14 use Piwik\Config;
15 use Piwik\Container\StaticContainer;
16 use Piwik\Context;
17 use Piwik\DataAccess\ArchiveSelector;
18 use Piwik\DataAccess\ArchiveTableCreator;
19 use Piwik\DataAccess\Model;
20 use Piwik\DataAccess\RawLogDao;
21 use Piwik\Date;
22 use Piwik\Db;
23 use Piwik\Period;
24 use Piwik\Piwik;
25 use Piwik\SettingsServer;
26 use Piwik\Site;
27 use Psr\Log\LoggerInterface;
28
29 /**
30 * This class uses PluginsArchiver class to trigger data aggregation and create archives.
31 */
32 class Loader
33 {
34 const MIN_VISIT_TIME_TTL = 3600;
35
36 /**
37 * @var Parameters
38 */
39 protected $params;
40
41 /**
42 * @var ArchiveInvalidator
43 */
44 private $invalidator;
45
46 /**
47 * @var \Matomo\Cache\Cache
48 */
49 private $cache;
50
51 /**
52 * @var LoggerInterface
53 */
54 private $logger;
55
56 /**
57 * @var RawLogDao
58 */
59 private $rawLogDao;
60
61 /**
62 * @var Model
63 */
64 private $dataAccessModel;
65
66 public function __construct(Parameters $params, $invalidateBeforeArchiving = false)
67 {
68 $this->params = $params;
69 $this->invalidateBeforeArchiving = $invalidateBeforeArchiving;
70 $this->invalidator = StaticContainer::get(ArchiveInvalidator::class);
71 $this->cache = Cache::getTransientCache();
72 $this->logger = StaticContainer::get(LoggerInterface::class);
73 $this->rawLogDao = new RawLogDao();
74 $this->dataAccessModel = new Model();
75 }
76
77 /**
78 * @return bool
79 */
80 protected function isThereSomeVisits($visits)
81 {
82 return $visits > 0;
83 }
84
85 /**
86 * @return bool
87 */
88 protected function mustProcessVisitCount($visits)
89 {
90 return $visits === false;
91 }
92
93 public function prepareArchive($pluginName)
94 {
95 return Context::changeIdSite($this->params->getSite()->getId(), function () use ($pluginName) {
96 return $this->prepareArchiveImpl($pluginName);
97 });
98 }
99
100 private function prepareArchiveImpl($pluginName)
101 {
102 $this->params->setRequestedPlugin($pluginName);
103
104 if (SettingsServer::isArchivePhpTriggered()) {
105 $requestedReport = Common::getRequestVar('requestedReport', '', 'string');
106 if (!empty($requestedReport)) {
107 $this->params->setArchiveOnlyReport($requestedReport);
108 }
109 }
110
111 // NOTE: $idArchives will contain the latest DONE_OK/DONE_INVALIDATED archive as well as any partial archives
112 // with a ts_archived >= the DONE_OK/DONE_INVALIDATED date.
113 list($idArchives, $visits, $visitsConverted, $isAnyArchiveExists) = $this->loadExistingArchiveIdFromDb();
114 if (!empty($idArchives)
115 && !$this->params->getArchiveOnlyReport()
116 && !Rules::isForceArchivingSinglePlugin()
117 ) {
118 // we have a usable idarchive (it's not invalidated and it's new enough), and we are not archiving
119 // a single report
120 return [$idArchives, $visits];
121 }
122
123 // NOTE: this optimization helps when archiving large periods. eg, if archiving a year w/ a segment where
124 // there are not visits in the entire year, we don't have to go through and do anything. but, w/o this
125 // code, we will end up launching archiving for each month, week and day, even though we don't have to.
126 //
127 // we don't create an archive in this case, because the archive may be in progress in some way, so a 0
128 // visits archive can be inaccurate in the long run.
129 if ($this->canSkipThisArchive()) {
130 return [false, 0];
131 }
132
133 // if there is an archive, but we can't use it for some reason, invalidate existing archives before
134 // we start archiving. if the archive is made invalid, we will correctly re-archive below.
135 if ($this->invalidateBeforeArchiving
136 && $isAnyArchiveExists
137 ) {
138 $this->invalidatedReportsIfNeeded();
139 }
140
141 /** @var ArchivingStatus $archivingStatus */
142 $archivingStatus = StaticContainer::get(ArchivingStatus::class);
143 $locked = $archivingStatus->archiveStarted($this->params);
144
145 try {
146 list($visits, $visitsConverted) = $this->prepareCoreMetricsArchive($visits, $visitsConverted);
147 list($idArchive, $visits) = $this->prepareAllPluginsArchive($visits, $visitsConverted);
148 } finally {
149 if ($locked) {
150 $archivingStatus->archiveFinished();
151 }
152 }
153
154 if ($this->isThereSomeVisits($visits) || PluginsArchiver::doesAnyPluginArchiveWithoutVisits()) {
155 return [[$idArchive], $visits];
156 }
157
158 return [false, false];
159 }
160
161 /**
162 * Prepares the core metrics if needed.
163 *
164 * @param $visits
165 * @return array
166 */
167 protected function prepareCoreMetricsArchive($visits, $visitsConverted)
168 {
169 $createSeparateArchiveForCoreMetrics = $this->mustProcessVisitCount($visits)
170 && !$this->doesRequestedPluginIncludeVisitsSummary();
171
172 if ($createSeparateArchiveForCoreMetrics) {
173 $requestedPlugin = $this->params->getRequestedPlugin();
174 $requestedReport = $this->params->getArchiveOnlyReport();
175
176 $this->params->setRequestedPlugin('VisitsSummary');
177 $this->params->setArchiveOnlyReport(null);
178
179 $pluginsArchiver = new PluginsArchiver($this->params);
180 $metrics = $pluginsArchiver->callAggregateCoreMetrics();
181 $pluginsArchiver->finalizeArchive();
182
183 $this->params->setRequestedPlugin($requestedPlugin);
184 $this->params->setArchiveOnlyReport($requestedReport);
185
186 $visits = $metrics['nb_visits'];
187 $visitsConverted = $metrics['nb_visits_converted'];
188 }
189
190 return array($visits, $visitsConverted);
191 }
192
193 protected function prepareAllPluginsArchive($visits, $visitsConverted)
194 {
195 $pluginsArchiver = new PluginsArchiver($this->params);
196
197 if ($this->mustProcessVisitCount($visits)
198 || $this->doesRequestedPluginIncludeVisitsSummary()
199 ) {
200 $metrics = $pluginsArchiver->callAggregateCoreMetrics();
201 $visits = $metrics['nb_visits'];
202 $visitsConverted = $metrics['nb_visits_converted'];
203 }
204
205 $forceArchivingWithoutVisits = !$this->isThereSomeVisits($visits) && $this->shouldArchiveForSiteEvenWhenNoVisits();
206 $pluginsArchiver->callAggregateAllPlugins($visits, $visitsConverted, $forceArchivingWithoutVisits);
207
208 $idArchive = $pluginsArchiver->finalizeArchive();
209
210 return array($idArchive, $visits);
211 }
212
213 protected function doesRequestedPluginIncludeVisitsSummary()
214 {
215 $processAllReportsIncludingVisitsSummary =
216 Rules::shouldProcessReportsAllPlugins(array($this->params->getSite()->getId()), $this->params->getSegment(), $this->params->getPeriod()->getLabel());
217 $doesRequestedPluginIncludeVisitsSummary = $processAllReportsIncludingVisitsSummary
218 || $this->params->getRequestedPlugin() == 'VisitsSummary';
219 return $doesRequestedPluginIncludeVisitsSummary;
220 }
221
222 protected function isArchivingForcedToTrigger()
223 {
224 $period = $this->params->getPeriod()->getLabel();
225 $debugSetting = 'always_archive_data_period'; // default
226
227 if ($period == 'day') {
228 $debugSetting = 'always_archive_data_day';
229 } elseif ($period == 'range') {
230 $debugSetting = 'always_archive_data_range';
231 }
232
233 return (bool) Config::getInstance()->Debug[$debugSetting];
234 }
235
236 /**
237 * Returns the idArchive if the archive is available in the database for the requested plugin.
238 * Returns false if the archive needs to be processed.
239 *
240 * (public for tests)
241 *
242 * @return array
243 */
244 public function loadExistingArchiveIdFromDb()
245 {
246 if ($this->isArchivingForcedToTrigger()) {
247 $this->logger->debug("Archiving forced to trigger for {$this->params}.");
248
249 // return no usable archive found, and no existing archive. this will skip invalidation, which should
250 // be fine since we just force archiving.
251 return [false, false, false, false];
252 }
253
254 $minDatetimeArchiveProcessedUTC = $this->getMinTimeArchiveProcessed();
255 $result = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC);
256 return $result;
257 }
258
259 /**
260 * Returns the minimum archive processed datetime to look at. Only public for tests.
261 *
262 * @return int|bool Datetime timestamp, or false if must look at any archive available
263 */
264 protected function getMinTimeArchiveProcessed()
265 {
266 $endDateTimestamp = self::determineIfArchivePermanent($this->params->getDateEnd());
267 if ($endDateTimestamp) {
268 // past archive
269 return $endDateTimestamp;
270 }
271 $dateStart = $this->params->getDateStart();
272 $period = $this->params->getPeriod();
273 $segment = $this->params->getSegment();
274 $site = $this->params->getSite();
275 // in-progress archive
276 return Rules::getMinTimeProcessedForInProgressArchive($dateStart, $period, $segment, $site);
277 }
278
279 protected static function determineIfArchivePermanent(Date $dateEnd)
280 {
281 $now = time();
282 $endTimestampUTC = strtotime($dateEnd->getDateEndUTC());
283
284 if ($endTimestampUTC <= $now) {
285 // - if the period we are looking for is finished, we look for a ts_archived that
286 // is greater than the last day of the archive
287 return $endTimestampUTC;
288 }
289
290 return false;
291 }
292
293 private function shouldArchiveForSiteEvenWhenNoVisits()
294 {
295 $idSitesToArchive = $this->getIdSitesToArchiveWhenNoVisits();
296 return in_array($this->params->getSite()->getId(), $idSitesToArchive);
297 }
298
299 private function getIdSitesToArchiveWhenNoVisits()
300 {
301 $cache = Cache::getTransientCache();
302 $cacheKey = 'Archiving.getIdSitesToArchiveWhenNoVisits';
303
304 if (!$cache->contains($cacheKey)) {
305 $idSites = array();
306
307 // leaving undocumented unless decided otherwise
308 Piwik::postEvent('Archiving.getIdSitesToArchiveWhenNoVisits', array(&$idSites));
309
310 $cache->save($cacheKey, $idSites);
311 }
312
313 return $cache->fetch($cacheKey);
314 }
315
316 // public for tests
317 public function getReportsToInvalidate()
318 {
319 $sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
320
321 foreach ($sitesPerDays as $dateStr => $siteIds) {
322 if (empty($siteIds)
323 || !in_array($this->params->getSite()->getId(), $siteIds)
324 ) {
325 unset($sitesPerDays[$dateStr]);
326 }
327
328 $date = Date::factory($dateStr);
329 if ($date->isEarlier($this->params->getPeriod()->getDateStart())
330 || $date->isLater($this->params->getPeriod()->getDateEnd())
331 ) { // date in list is not the current date, so ignore it
332 unset($sitesPerDays[$dateStr]);
333 }
334 }
335
336 return $sitesPerDays;
337 }
338
339 private function invalidatedReportsIfNeeded()
340 {
341 $sitesPerDays = $this->getReportsToInvalidate();
342 if (empty($sitesPerDays)) {
343 return;
344 }
345
346 foreach ($sitesPerDays as $date => $siteIds) {
347 try {
348 $this->invalidator->markArchivesAsInvalidated([$this->params->getSite()->getId()], array(Date::factory($date)), false, $this->params->getSegment());
349 } catch (\Exception $e) {
350 Site::clearCache();
351 throw $e;
352 }
353 }
354
355 Site::clearCache();
356 }
357
358 public function canSkipThisArchive()
359 {
360 $params = $this->params;
361 $idSite = $params->getSite()->getId();
362
363 $isWebsiteUsingTracker = $this->isWebsiteUsingTheTracker($idSite);
364 $isArchivingForcedWhenNoVisits = $this->shouldArchiveForSiteEvenWhenNoVisits();
365 $hasSiteVisitsBetweenTimeframe = $this->hasSiteVisitsBetweenTimeframe($idSite, $params->getPeriod());
366 $hasChildArchivesInPeriod = $this->dataAccessModel->hasChildArchivesInPeriod($idSite, $params->getPeriod());
367
368 return $isWebsiteUsingTracker
369 && !$isArchivingForcedWhenNoVisits
370 && !$hasSiteVisitsBetweenTimeframe
371 && !$hasChildArchivesInPeriod;
372 }
373
374 private function isWebsiteUsingTheTracker($idSite)
375 {
376 $idSitesNotUsingTracker = self::getSitesNotUsingTracker();
377
378 $isUsingTracker = !in_array($idSite, $idSitesNotUsingTracker);
379
380 return $isUsingTracker;
381 }
382
383 public static function getSitesNotUsingTracker()
384 {
385 $cache = Cache::getTransientCache();
386
387 $cacheKey = 'Archiving.isWebsiteUsingTheTracker';
388 $idSitesNotUsingTracker = $cache->fetch($cacheKey);
389 if ($idSitesNotUsingTracker === false || !isset($idSitesNotUsingTracker)) {
390 // we want to trigger event only once
391 $idSitesNotUsingTracker = array();
392
393 /**
394 * This event is triggered when detecting whether there are sites that do not use the tracker.
395 *
396 * By default we only archive a site when there was actually any visit since the last archiving.
397 * However, some plugins do import data from another source instead of using the tracker and therefore
398 * will never have any visits for this site. To make sure we still archive data for such a site when
399 * archiving for this site is requested, you can listen to this event and add the idSite to the list of
400 * sites that do not use the tracker.
401 *
402 * @param bool $idSitesNotUsingTracker The list of idSites that rather import data instead of using the tracker
403 */
404 Piwik::postEvent('CronArchive.getIdSitesNotUsingTracker', array(&$idSitesNotUsingTracker));
405
406 $cache->save($cacheKey, $idSitesNotUsingTracker);
407 }
408 return $idSitesNotUsingTracker;
409 }
410
411 private function hasSiteVisitsBetweenTimeframe($idSite, Period $period)
412 {
413 $minVisitTimesPerSite = $this->getMinVisitTimesPerSite($idSite);
414 if (empty($minVisitTimesPerSite)) {
415 return false;
416 }
417
418 $timezone = Site::getTimezoneFor($idSite);
419 list($date1, $date2) = $period->getBoundsInTimezone($timezone);
420 if ($date2->isEarlier($minVisitTimesPerSite)) {
421 return false;
422 }
423
424 return $this->rawLogDao->hasSiteVisitsBetweenTimeframe($date1->getDatetime(), $date2->getDatetime(), $idSite);
425 }
426
427 private function getMinVisitTimesPerSite($idSite)
428 {
429 $cache = Cache::getLazyCache();
430 $cacheKey = 'Archiving.minVisitTime.' . $idSite;
431
432 $value = $cache->fetch($cacheKey);
433 if ($value === false) {
434 $value = $this->rawLogDao->getMinimumVisitTimeForSite($idSite);
435 if (!empty($value)) {
436 $cache->save($cacheKey, $value, $ttl = self::MIN_VISIT_TIME_TTL);
437 }
438 }
439
440 if (!empty($value)) {
441 $value = Date::factory($value);
442 }
443
444 return $value;
445 }
446
447 public static function invalidateMinVisitTimeCache($idSite)
448 {
449 $cache = Cache::getLazyCache();
450 $cacheKey = 'Archiving.minVisitTime.' . $idSite;
451 $cache->delete($cacheKey);
452 }
453 }
454