PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.15.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.15.2
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 4 years ago Loader.php 3 years ago LoaderLock.php 4 years ago Parameters.php 3 years ago PluginsArchiver.php 3 years ago PluginsArchiverException.php 5 years ago Rules.php 4 years ago
Loader.php
592 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\ArchiveProcessor;
13 use Piwik\Cache;
14 use Piwik\Common;
15 use Piwik\Config;
16 use Piwik\Container\StaticContainer;
17 use Piwik\Context;
18 use Piwik\DataAccess\ArchiveSelector;
19 use Piwik\DataAccess\ArchiveWriter;
20 use Piwik\DataAccess\Model;
21 use Piwik\DataAccess\RawLogDao;
22 use Piwik\Date;
23 use Piwik\Period;
24 use Piwik\Piwik;
25 use Piwik\SettingsServer;
26 use Piwik\Site;
27 use Psr\Log\LoggerInterface;
28 use Piwik\CronArchive\SegmentArchiving;
29
30 /**
31 * This class uses PluginsArchiver class to trigger data aggregation and create archives.
32 */
33 class Loader
34 {
35 private static $archivingDepth = 0;
36
37 /**
38 * @var Parameters
39 */
40 protected $params;
41
42 /**
43 * @var ArchiveInvalidator
44 */
45 private $invalidator;
46
47 /**
48 * @var \Matomo\Cache\Cache
49 */
50 private $cache;
51
52 /**
53 * @var LoggerInterface
54 */
55 private $logger;
56
57 /**
58 * @var RawLogDao
59 */
60 private $rawLogDao;
61
62 /**
63 * @var Model
64 */
65 private $dataAccessModel;
66
67 /**
68 * @var bool
69 */
70 private $invalidateBeforeArchiving;
71
72 public function __construct(Parameters $params, $invalidateBeforeArchiving = false)
73 {
74 $this->params = $params;
75 $this->invalidateBeforeArchiving = $invalidateBeforeArchiving;
76 $this->invalidator = StaticContainer::get(ArchiveInvalidator::class);
77 $this->cache = Cache::getTransientCache();
78 $this->logger = StaticContainer::get(LoggerInterface::class);
79 $this->rawLogDao = new RawLogDao();
80 $this->dataAccessModel = new Model();
81 }
82
83 /**
84 * @return bool
85 */
86 protected function isThereSomeVisits($visits)
87 {
88 return $visits > 0;
89 }
90
91 /**
92 * @return bool
93 */
94 protected function mustProcessVisitCount($visits)
95 {
96 return $visits === false;
97 }
98
99 public function prepareArchive($pluginName)
100 {
101 return Context::changeIdSite($this->params->getSite()->getId(), function () use ($pluginName) {
102 try {
103 ++self::$archivingDepth;
104 return $this->prepareArchiveImpl($pluginName);
105 } finally {
106 --self::$archivingDepth;
107 }
108 });
109 }
110
111 /**
112 * @throws \Exception
113 */
114 private function prepareArchiveImpl($pluginName)
115 {
116 $this->params->setRequestedPlugin($pluginName);
117
118 if (SettingsServer::isArchivePhpTriggered()) {
119 $requestedReport = Common::getRequestVar('requestedReport', '', 'string');
120 if (!empty($requestedReport)) {
121 $this->params->setArchiveOnlyReport($requestedReport);
122 }
123 }
124
125 // invalidate existing archives before we start archiving in case data was tracked in the past. if the archive is
126 // made invalid, we will correctly re-archive below.
127 if ($this->invalidateBeforeArchiving
128 && Rules::isBrowserTriggerEnabled()
129 ) {
130 $this->invalidatedReportsIfNeeded();
131 }
132 // load existing data from archive
133 $data = $this->loadArchiveData();
134 if (sizeof($data) == 2) {
135 return $data;
136 }
137 list($idArchives, $visits, $visitsConverted) = $data;
138
139 // only lock meet those conditions
140 if (ArchiveProcessor::$isRootArchivingRequest && !SettingsServer::isArchivePhpTriggered()) {
141 $lockId = $this->makeArchivingLockId();
142
143 //ini lock
144 $lock = new LoaderLock($lockId);
145
146 //set mysql lock the entire process if another process is running
147 $lock->setLock();
148
149 try {
150 $data = $this->loadArchiveData();
151
152 if (sizeof($data) == 2) {
153 return $data;
154 }
155
156 list($idArchives, $visits, $visitsConverted) = $data;
157
158 return $this->insertArchiveData($visits, $visitsConverted);
159 } finally {
160 $lock->unlock();
161 }
162 } else {
163
164 return $this->insertArchiveData($visits, $visitsConverted);
165 }
166 }
167
168
169 /**
170 * @param $visits
171 * @param $visitsConverted
172 * @return array|false[]
173 */
174 protected function insertArchiveData($visits, $visitsConverted)
175 {
176 if (SettingsServer::isArchivePhpTriggered()) {
177 $this->logger->info("initiating archiving via core:archive for " . $this->params);
178 }
179
180 list($visits, $visitsConverted) = $this->prepareCoreMetricsArchive($visits, $visitsConverted);
181 list($idArchive, $visits) = $this->prepareAllPluginsArchive($visits, $visitsConverted);
182
183 if ($this->isThereSomeVisits($visits) || PluginsArchiver::doesAnyPluginArchiveWithoutVisits()) {
184 return [[$idArchive], $visits];
185 }
186
187 return [false, false];
188 }
189
190 /**
191 * @return string
192 * @throws \Exception
193 */
194 private function makeArchivingLockId()
195 {
196 $doneFlag = Rules::getDoneStringFlagFor([$this->params->getSite()->getId()], $this->params->getSegment(),
197 $this->params->getPeriod()->getLabel(), $this->params->getRequestedPlugin());
198
199 return $this->params->getPeriod()->getDateStart()->toString() . $this->params->getPeriod()->getDateEnd()->toString() .'.'. $doneFlag;
200
201 }
202
203 /**
204 * @return array|false[]
205 */
206 protected function loadArchiveData()
207 {
208 // this hack was used to check the main function goes to return or continue
209 // NOTE: $idArchives will contain the latest DONE_OK/DONE_INVALIDATED archive as well as any partial archives
210 // with a ts_archived >= the DONE_OK/DONE_INVALIDATED date.
211 list($idArchives, $visits, $visitsConverted, $isAnyArchiveExists, $tsArchived, $value) = $this->loadExistingArchiveIdFromDb();
212
213 if (!empty($idArchives)
214 && !Rules::isActuallyForceArchivingSinglePlugin()
215 && !$this->shouldForceInvalidatedArchive($value, $tsArchived)) {
216 // we have a usable idarchive (it's not invalidated and it's new enough), and we are not archiving
217 // a single report
218 return [$idArchives, $visits];
219 }
220
221 // NOTE: this optimization helps when archiving large periods. eg, if archiving a year w/ a segment where
222 // there are not visits in the entire year, we don't have to go through and do anything. but, w/o this
223 // code, we will end up launching archiving for each month, week and day, even though we don't have to.
224 //
225 // we don't create an archive in this case, because the archive may be in progress in some way, so a 0
226 // visits archive can be inaccurate in the long run.
227 if ($this->canSkipThisArchive()) {
228 if (!empty($idArchives)) {
229 return [$idArchives, $visits];
230 } else {
231 return [false, 0];
232 }
233 }
234
235 return [$idArchives, $visits, $visitsConverted];
236 }
237
238 /**
239 * Prepares the core metrics if needed.
240 *
241 * @param $visits
242 * @return array
243 */
244 protected function prepareCoreMetricsArchive($visits, $visitsConverted)
245 {
246 $createSeparateArchiveForCoreMetrics = $this->mustProcessVisitCount($visits)
247 && !$this->doesRequestedPluginIncludeVisitsSummary();
248
249 if ($createSeparateArchiveForCoreMetrics) {
250 $requestedPlugin = $this->params->getRequestedPlugin();
251 $requestedReport = $this->params->getArchiveOnlyReport();
252 $isPartialArchive = $this->params->isPartialArchive();
253
254 $this->params->setRequestedPlugin('VisitsSummary');
255 $this->params->setArchiveOnlyReport(null);
256 $this->params->setIsPartialArchive(false);
257
258 $metrics = Context::executeWithQueryParameters(['requestedReport' => ''], function () {
259 $pluginsArchiver = new PluginsArchiver($this->params);
260 $metrics = $pluginsArchiver->callAggregateCoreMetrics();
261 $pluginsArchiver->finalizeArchive();
262 return $metrics;
263 });
264
265 $this->params->setRequestedPlugin($requestedPlugin);
266 $this->params->setArchiveOnlyReport($requestedReport);
267 $this->params->setIsPartialArchive($isPartialArchive);
268
269 $visits = $metrics['nb_visits'];
270 $visitsConverted = $metrics['nb_visits_converted'];
271 }
272
273 return array($visits, $visitsConverted);
274 }
275
276 protected function prepareAllPluginsArchive($visits, $visitsConverted)
277 {
278 $pluginsArchiver = new PluginsArchiver($this->params);
279
280 if ($this->mustProcessVisitCount($visits)
281 || $this->doesRequestedPluginIncludeVisitsSummary()
282 ) {
283 $metrics = $pluginsArchiver->callAggregateCoreMetrics();
284 $visits = $metrics['nb_visits'];
285 $visitsConverted = $metrics['nb_visits_converted'];
286 }
287
288 $forceArchivingWithoutVisits = !$this->isThereSomeVisits($visits) && $this->shouldArchiveForSiteEvenWhenNoVisits();
289 $pluginsArchiver->callAggregateAllPlugins($visits, $visitsConverted, $forceArchivingWithoutVisits);
290
291 $idArchive = $pluginsArchiver->finalizeArchive();
292
293 return array($idArchive, $visits);
294 }
295
296 protected function doesRequestedPluginIncludeVisitsSummary()
297 {
298 $processAllReportsIncludingVisitsSummary =
299 Rules::shouldProcessReportsAllPlugins(array($this->params->getSite()->getId()), $this->params->getSegment(), $this->params->getPeriod()->getLabel());
300 $doesRequestedPluginIncludeVisitsSummary = $processAllReportsIncludingVisitsSummary
301 || $this->params->getRequestedPlugin() == 'VisitsSummary';
302 return $doesRequestedPluginIncludeVisitsSummary;
303 }
304
305 protected function isArchivingForcedToTrigger()
306 {
307 $period = $this->params->getPeriod()->getLabel();
308 $debugSetting = 'always_archive_data_period'; // default
309
310 if ($period == 'day') {
311 $debugSetting = 'always_archive_data_day';
312 } elseif ($period == 'range') {
313 $debugSetting = 'always_archive_data_range';
314 }
315
316 return (bool) Config::getInstance()->Debug[$debugSetting];
317 }
318
319 /**
320 * Returns the idArchive if the archive is available in the database for the requested plugin.
321 * Returns false if the archive needs to be processed.
322 *
323 * (public for tests)
324 *
325 * @return array
326 */
327 public function loadExistingArchiveIdFromDb()
328 {
329 if ($this->isArchivingForcedToTrigger()) {
330 $this->logger->debug("Archiving forced to trigger for {$this->params}.");
331
332 // return no usable archive found, and no existing archive. this will skip invalidation, which should
333 // be fine since we just force archiving.
334 return [false, false, false, false, false, false];
335 }
336
337 $minDatetimeArchiveProcessedUTC = $this->getMinTimeArchiveProcessed();
338 $result = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC);
339 return $result;
340 }
341
342 /**
343 * Returns the minimum archive processed datetime to look at. Only public for tests.
344 *
345 * @return int|bool Datetime timestamp, or false if must look at any archive available
346 */
347 protected function getMinTimeArchiveProcessed()
348 {
349 // for range periods we can archive in a browser request request, make sure to check for the ttl no matter what
350 $isRangeArchiveAndArchivingEnabled = $this->params->getPeriod()->getLabel() == 'range'
351 && Rules::isArchivingEnabledFor([$this->params->getSite()->getId()], $this->params->getSegment(), $this->params->getPeriod()->getLabel());
352
353 if (!$isRangeArchiveAndArchivingEnabled) {
354 $endDateTimestamp = self::determineIfArchivePermanent($this->params->getDateEnd());
355 if ($endDateTimestamp) {
356 // past archive
357 return $endDateTimestamp;
358 }
359 }
360
361 $dateStart = $this->params->getDateStart();
362 $period = $this->params->getPeriod();
363 $segment = $this->params->getSegment();
364 $site = $this->params->getSite();
365 // in-progress archive
366 return Rules::getMinTimeProcessedForInProgressArchive($dateStart, $period, $segment, $site);
367 }
368
369 protected static function determineIfArchivePermanent(Date $dateEnd)
370 {
371 $now = time();
372 $endTimestampUTC = strtotime($dateEnd->getDateEndUTC());
373
374 if ($endTimestampUTC <= $now) {
375 // - if the period we are looking for is finished, we look for a ts_archived that
376 // is greater than the last day of the archive
377 return $endTimestampUTC;
378 }
379
380 return false;
381 }
382
383 private function shouldArchiveForSiteEvenWhenNoVisits()
384 {
385 $idSitesToArchive = $this->getIdSitesToArchiveWhenNoVisits();
386 return in_array($this->params->getSite()->getId(), $idSitesToArchive);
387 }
388
389 private function getIdSitesToArchiveWhenNoVisits()
390 {
391 $cache = Cache::getTransientCache();
392 $cacheKey = 'Archiving.getIdSitesToArchiveWhenNoVisits';
393
394 if (!$cache->contains($cacheKey)) {
395 $idSites = array();
396
397 // leaving undocumented unless decided otherwise
398 Piwik::postEvent('Archiving.getIdSitesToArchiveWhenNoVisits', array(&$idSites));
399
400 $cache->save($cacheKey, $idSites);
401 }
402
403 return $cache->fetch($cacheKey);
404 }
405
406 // public for tests
407 public function getReportsToInvalidate()
408 {
409 $sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
410
411 foreach ($sitesPerDays as $dateStr => $siteIds) {
412 if (empty($siteIds)
413 || !in_array($this->params->getSite()->getId(), $siteIds)
414 ) {
415 unset($sitesPerDays[$dateStr]);
416 }
417
418 $date = Date::factory($dateStr);
419 if ($date->isEarlier($this->params->getPeriod()->getDateStart())
420 || $date->isLater($this->params->getPeriod()->getDateEnd())
421 ) { // date in list is not the current date, so ignore it
422 unset($sitesPerDays[$dateStr]);
423 }
424 }
425
426 return $sitesPerDays;
427 }
428
429 private function invalidatedReportsIfNeeded()
430 {
431 $sitesPerDays = $this->getReportsToInvalidate();
432 if (empty($sitesPerDays)) {
433 return;
434 }
435
436 foreach ($sitesPerDays as $date => $siteIds) {
437 try {
438 $this->invalidator->markArchivesAsInvalidated([$this->params->getSite()->getId()], array(Date::factory($date)), false, $this->params->getSegment());
439 } catch (\Exception $e) {
440 Site::clearCache();
441 throw $e;
442 }
443 }
444
445 Site::clearCache();
446 }
447
448 public function canSkipThisArchive()
449 {
450 $params = $this->params;
451 $idSite = $params->getSite()->getId();
452
453 $isWebsiteUsingTracker = $this->isWebsiteUsingTheTracker($idSite);
454 $isArchivingForcedWhenNoVisits = $this->shouldArchiveForSiteEvenWhenNoVisits();
455 $hasSiteVisitsBetweenTimeframe = $this->hasSiteVisitsBetweenTimeframe($idSite, $params->getPeriod());
456 $hasChildArchivesInPeriod = $this->dataAccessModel->hasChildArchivesInPeriod($idSite, $params->getPeriod());
457
458 if ($this->canSkipArchiveForSegment()) {
459 return true;
460 }
461
462 return $isWebsiteUsingTracker
463 && !$isArchivingForcedWhenNoVisits
464 && !$hasSiteVisitsBetweenTimeframe
465 && !$hasChildArchivesInPeriod;
466 }
467
468 public function canSkipArchiveForSegment()
469 {
470 $params = $this->params;
471
472 if ($params->getSegment()->isEmpty()) {
473 return false;
474 }
475
476 if (!empty($params->getRequestedPlugin()) && Rules::isSegmentPluginArchivingDisabled($params->getRequestedPlugin(), $params->getSite()->getId())) {
477 return true;
478 }
479
480 /** @var SegmentArchiving */
481 $segmentArchiving = StaticContainer::get(SegmentArchiving::class);
482 $segmentInfo = $segmentArchiving->findSegmentForHash($params->getSegment()->getHash(), $params->getSite()->getId());
483
484 if (!$segmentInfo) {
485 return false;
486 }
487
488 $segmentArchiveStartDate = $segmentArchiving->getReArchiveSegmentStartDate($segmentInfo);
489
490 if ($segmentArchiveStartDate !==null && $segmentArchiveStartDate->isLater($params->getPeriod()->getDateEnd()->getEndOfDay())) {
491 $doneFlag = Rules::getDoneStringFlagFor(
492 [$params->getSite()->getId()],
493 $params->getSegment(),
494 $params->getPeriod()->getLabel(),
495 $params->getRequestedPlugin()
496 );
497
498 // if there is no invalidation where the report is null, we can skip
499 // if we have invalidations for the period and name, but only for a specific reports, we can skip
500 // if the report is not null we only want to rearchive if we have invalidation for that report
501 // if we don't find invalidation for that report, we can skip
502 return !$this->dataAccessModel->hasInvalidationForPeriodAndName($params->getSite()->getId(), $params->getPeriod(), $doneFlag, $params->getArchiveOnlyReport());
503 }
504
505 return false;
506 }
507
508 private function isWebsiteUsingTheTracker($idSite)
509 {
510 $idSitesNotUsingTracker = self::getSitesNotUsingTracker();
511
512 $isUsingTracker = !in_array($idSite, $idSitesNotUsingTracker);
513
514 return $isUsingTracker;
515 }
516
517 public static function getSitesNotUsingTracker()
518 {
519 $cache = Cache::getTransientCache();
520
521 $cacheKey = 'Archiving.isWebsiteUsingTheTracker';
522 $idSitesNotUsingTracker = $cache->fetch($cacheKey);
523 if ($idSitesNotUsingTracker === false || !isset($idSitesNotUsingTracker)) {
524 // we want to trigger event only once
525 $idSitesNotUsingTracker = array();
526
527 /**
528 * This event is triggered when detecting whether there are sites that do not use the tracker.
529 *
530 * By default we only archive a site when there was actually any visit since the last archiving.
531 * However, some plugins do import data from another source instead of using the tracker and therefore
532 * will never have any visits for this site. To make sure we still archive data for such a site when
533 * archiving for this site is requested, you can listen to this event and add the idSite to the list of
534 * sites that do not use the tracker.
535 *
536 * @param bool $idSitesNotUsingTracker The list of idSites that rather import data instead of using the tracker
537 */
538 Piwik::postEvent('CronArchive.getIdSitesNotUsingTracker', array(&$idSitesNotUsingTracker));
539
540 $cache->save($cacheKey, $idSitesNotUsingTracker);
541 }
542 return $idSitesNotUsingTracker;
543 }
544
545 private function hasSiteVisitsBetweenTimeframe($idSite, Period $period)
546 {
547 $timezone = Site::getTimezoneFor($idSite);
548 list($date1, $date2) = $period->getBoundsInTimezone($timezone);
549
550 return $this->rawLogDao->hasSiteVisitsBetweenTimeframe($date1->getDatetime(), $date2->getDatetime(), $idSite);
551 }
552
553 public static function getArchivingDepth()
554 {
555 return self::$archivingDepth;
556 }
557
558 private function shouldForceInvalidatedArchive($value, $tsArchived)
559 {
560 $params = $this->params;
561
562 // the archive is invalidated and we are in a browser request that is allowed archive it
563 if ($value == ArchiveWriter::DONE_INVALIDATED
564 && Rules::isArchivingEnabledFor([$params->getSite()->getId()], $params->getSegment(), $params->getPeriod()->getLabel())
565 ) {
566 // if coming from core:archive, force rearchiving, since if we don't the entry will be removed from archive_invalidations
567 // w/o being rearchived
568 if (SettingsServer::isArchivePhpTriggered()) {
569 return true;
570 }
571
572 // if coming from a browser request, and period does not contain today, force rearchiving
573 $timezone = $params->getSite()->getTimezone();
574 if (!$params->getPeriod()->isDateInPeriod(Date::factoryInTimezone('today', $timezone))) {
575 return true;
576 }
577
578 // if coming from a browser request, and period does contain today, check the ttl for the period (done just below this)
579 $minDatetimeArchiveProcessedUTC = Rules::getMinTimeProcessedForInProgressArchive(
580 $params->getDateStart(), $params->getPeriod(), $params->getSegment(), $params->getSite());
581 $minDatetimeArchiveProcessedUTC = Date::factory($minDatetimeArchiveProcessedUTC);
582 if ($minDatetimeArchiveProcessedUTC
583 && Date::factory($tsArchived)->isEarlier($minDatetimeArchiveProcessedUTC)
584 ) {
585 return true;
586 }
587 }
588
589 return false;
590 }
591 }
592