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