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