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