PluginProbe ʕ •ᴥ•ʔ
Matomo Analytics – Powerful, Privacy-First Insights for WordPress / 4.1.2
Matomo Analytics – Powerful, Privacy-First Insights for WordPress v4.1.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 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
424 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 /**
35 * @var Parameters
36 */
37 protected $params;
38
39 /**
40 * @var ArchiveInvalidator
41 */
42 private $invalidator;
43
44 /**
45 * @var \Matomo\Cache\Cache
46 */
47 private $cache;
48
49 /**
50 * @var LoggerInterface
51 */
52 private $logger;
53
54 /**
55 * @var RawLogDao
56 */
57 private $rawLogDao;
58
59 /**
60 * @var Model
61 */
62 private $dataAccessModel;
63
64 public function __construct(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 /**
76 * @return bool
77 */
78 protected function isThereSomeVisits($visits)
79 {
80 return $visits > 0;
81 }
82
83 /**
84 * @return bool
85 */
86 protected function mustProcessVisitCount($visits)
87 {
88 return $visits === false;
89 }
90
91 public function prepareArchive($pluginName)
92 {
93 return Context::changeIdSite($this->params->getSite()->getId(), function () use ($pluginName) {
94 return $this->prepareArchiveImpl($pluginName);
95 });
96 }
97
98 private function prepareArchiveImpl($pluginName)
99 {
100 $this->params->setRequestedPlugin($pluginName);
101
102 if (SettingsServer::isArchivePhpTriggered()) {
103 $requestedReport = Common::getRequestVar('requestedReport', '', 'string');
104 if (!empty($requestedReport)) {
105 $this->params->setArchiveOnlyReport($requestedReport);
106 }
107 }
108
109 // NOTE: $idArchives will contain the latest DONE_OK/DONE_INVALIDATED archive as well as any partial archives
110 // with a ts_archived >= the DONE_OK/DONE_INVALIDATED date.
111 list($idArchives, $visits, $visitsConverted, $isAnyArchiveExists) = $this->loadExistingArchiveIdFromDb();
112 if (!empty($idArchives)
113 && !$this->params->getArchiveOnlyReport()
114 && !Rules::isForceArchivingSinglePlugin()
115 ) {
116 // we have a usable idarchive (it's not invalidated and it's new enough), and we are not archiving
117 // a single report
118 return [$idArchives, $visits];
119 }
120
121 // NOTE: this optimization helps when archiving large periods. eg, if archiving a year w/ a segment where
122 // there are not visits in the entire year, we don't have to go through and do anything. but, w/o this
123 // code, we will end up launching archiving for each month, week and day, even though we don't have to.
124 //
125 // we don't create an archive in this case, because the archive may be in progress in some way, so a 0
126 // visits archive can be inaccurate in the long run.
127 if ($this->canSkipThisArchive()) {
128 return [false, 0];
129 }
130
131 // if there is an archive, but we can't use it for some reason, invalidate existing archives before
132 // we start archiving. if the archive is made invalid, we will correctly re-archive below.
133 if ($this->invalidateBeforeArchiving
134 && $isAnyArchiveExists
135 ) {
136 $this->invalidatedReportsIfNeeded();
137 }
138
139 /** @var ArchivingStatus $archivingStatus */
140 $archivingStatus = StaticContainer::get(ArchivingStatus::class);
141 $locked = $archivingStatus->archiveStarted($this->params);
142
143 try {
144 list($visits, $visitsConverted) = $this->prepareCoreMetricsArchive($visits, $visitsConverted);
145 list($idArchive, $visits) = $this->prepareAllPluginsArchive($visits, $visitsConverted);
146 } finally {
147 if ($locked) {
148 $archivingStatus->archiveFinished();
149 }
150 }
151
152 if ($this->isThereSomeVisits($visits) || PluginsArchiver::doesAnyPluginArchiveWithoutVisits()) {
153 return [[$idArchive], $visits];
154 }
155
156 return [false, false];
157 }
158
159 /**
160 * Prepares the core metrics if needed.
161 *
162 * @param $visits
163 * @return array
164 */
165 protected function prepareCoreMetricsArchive($visits, $visitsConverted)
166 {
167 $createSeparateArchiveForCoreMetrics = $this->mustProcessVisitCount($visits)
168 && !$this->doesRequestedPluginIncludeVisitsSummary();
169
170 if ($createSeparateArchiveForCoreMetrics) {
171 $requestedPlugin = $this->params->getRequestedPlugin();
172 $requestedReport = $this->params->getArchiveOnlyReport();
173
174 $this->params->setRequestedPlugin('VisitsSummary');
175 $this->params->setArchiveOnlyReport(null);
176
177 $pluginsArchiver = new PluginsArchiver($this->params);
178 $metrics = $pluginsArchiver->callAggregateCoreMetrics();
179 $pluginsArchiver->finalizeArchive();
180
181 $this->params->setRequestedPlugin($requestedPlugin);
182 $this->params->setArchiveOnlyReport($requestedReport);
183
184 $visits = $metrics['nb_visits'];
185 $visitsConverted = $metrics['nb_visits_converted'];
186 }
187
188 return array($visits, $visitsConverted);
189 }
190
191 protected function prepareAllPluginsArchive($visits, $visitsConverted)
192 {
193 $pluginsArchiver = new PluginsArchiver($this->params);
194
195 if ($this->mustProcessVisitCount($visits)
196 || $this->doesRequestedPluginIncludeVisitsSummary()
197 ) {
198 $metrics = $pluginsArchiver->callAggregateCoreMetrics();
199 $visits = $metrics['nb_visits'];
200 $visitsConverted = $metrics['nb_visits_converted'];
201 }
202
203 $forceArchivingWithoutVisits = !$this->isThereSomeVisits($visits) && $this->shouldArchiveForSiteEvenWhenNoVisits();
204 $pluginsArchiver->callAggregateAllPlugins($visits, $visitsConverted, $forceArchivingWithoutVisits);
205
206 $idArchive = $pluginsArchiver->finalizeArchive();
207
208 return array($idArchive, $visits);
209 }
210
211 protected function doesRequestedPluginIncludeVisitsSummary()
212 {
213 $processAllReportsIncludingVisitsSummary =
214 Rules::shouldProcessReportsAllPlugins(array($this->params->getSite()->getId()), $this->params->getSegment(), $this->params->getPeriod()->getLabel());
215 $doesRequestedPluginIncludeVisitsSummary = $processAllReportsIncludingVisitsSummary
216 || $this->params->getRequestedPlugin() == 'VisitsSummary';
217 return $doesRequestedPluginIncludeVisitsSummary;
218 }
219
220 protected function isArchivingForcedToTrigger()
221 {
222 $period = $this->params->getPeriod()->getLabel();
223 $debugSetting = 'always_archive_data_period'; // default
224
225 if ($period == 'day') {
226 $debugSetting = 'always_archive_data_day';
227 } elseif ($period == 'range') {
228 $debugSetting = 'always_archive_data_range';
229 }
230
231 return (bool) Config::getInstance()->Debug[$debugSetting];
232 }
233
234 /**
235 * Returns the idArchive if the archive is available in the database for the requested plugin.
236 * Returns false if the archive needs to be processed.
237 *
238 * (public for tests)
239 *
240 * @return array
241 */
242 public function loadExistingArchiveIdFromDb()
243 {
244 if ($this->isArchivingForcedToTrigger()) {
245 $this->logger->debug("Archiving forced to trigger for {$this->params}.");
246
247 // return no usable archive found, and no existing archive. this will skip invalidation, which should
248 // be fine since we just force archiving.
249 return [false, false, false, false];
250 }
251
252 $minDatetimeArchiveProcessedUTC = $this->getMinTimeArchiveProcessed();
253 $result = ArchiveSelector::getArchiveIdAndVisits($this->params, $minDatetimeArchiveProcessedUTC);
254 return $result;
255 }
256
257 /**
258 * Returns the minimum archive processed datetime to look at. Only public for tests.
259 *
260 * @return int|bool Datetime timestamp, or false if must look at any archive available
261 */
262 protected function getMinTimeArchiveProcessed()
263 {
264 $endDateTimestamp = self::determineIfArchivePermanent($this->params->getDateEnd());
265 if ($endDateTimestamp) {
266 // past archive
267 return $endDateTimestamp;
268 }
269 $dateStart = $this->params->getDateStart();
270 $period = $this->params->getPeriod();
271 $segment = $this->params->getSegment();
272 $site = $this->params->getSite();
273 // in-progress archive
274 return Rules::getMinTimeProcessedForInProgressArchive($dateStart, $period, $segment, $site);
275 }
276
277 protected static function determineIfArchivePermanent(Date $dateEnd)
278 {
279 $now = time();
280 $endTimestampUTC = strtotime($dateEnd->getDateEndUTC());
281
282 if ($endTimestampUTC <= $now) {
283 // - if the period we are looking for is finished, we look for a ts_archived that
284 // is greater than the last day of the archive
285 return $endTimestampUTC;
286 }
287
288 return false;
289 }
290
291 private function shouldArchiveForSiteEvenWhenNoVisits()
292 {
293 $idSitesToArchive = $this->getIdSitesToArchiveWhenNoVisits();
294 return in_array($this->params->getSite()->getId(), $idSitesToArchive);
295 }
296
297 private function getIdSitesToArchiveWhenNoVisits()
298 {
299 $cache = Cache::getTransientCache();
300 $cacheKey = 'Archiving.getIdSitesToArchiveWhenNoVisits';
301
302 if (!$cache->contains($cacheKey)) {
303 $idSites = array();
304
305 // leaving undocumented unless decided otherwise
306 Piwik::postEvent('Archiving.getIdSitesToArchiveWhenNoVisits', array(&$idSites));
307
308 $cache->save($cacheKey, $idSites);
309 }
310
311 return $cache->fetch($cacheKey);
312 }
313
314 // public for tests
315 public function getReportsToInvalidate()
316 {
317 $sitesPerDays = $this->invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
318
319 foreach ($sitesPerDays as $dateStr => $siteIds) {
320 if (empty($siteIds)
321 || !in_array($this->params->getSite()->getId(), $siteIds)
322 ) {
323 unset($sitesPerDays[$dateStr]);
324 }
325
326 $date = Date::factory($dateStr);
327 if ($date->isEarlier($this->params->getPeriod()->getDateStart())
328 || $date->isLater($this->params->getPeriod()->getDateEnd())
329 ) { // date in list is not the current date, so ignore it
330 unset($sitesPerDays[$dateStr]);
331 }
332 }
333
334 return $sitesPerDays;
335 }
336
337 private function invalidatedReportsIfNeeded()
338 {
339 $sitesPerDays = $this->getReportsToInvalidate();
340 if (empty($sitesPerDays)) {
341 return;
342 }
343
344 foreach ($sitesPerDays as $date => $siteIds) {
345 try {
346 $this->invalidator->markArchivesAsInvalidated([$this->params->getSite()->getId()], array(Date::factory($date)), false, $this->params->getSegment());
347 } catch (\Exception $e) {
348 Site::clearCache();
349 throw $e;
350 }
351 }
352
353 Site::clearCache();
354 }
355
356 public function canSkipThisArchive()
357 {
358 $params = $this->params;
359 $idSite = $params->getSite()->getId();
360
361 $isWebsiteUsingTracker = $this->isWebsiteUsingTheTracker($idSite);
362 $isArchivingForcedWhenNoVisits = $this->shouldArchiveForSiteEvenWhenNoVisits();
363 $hasSiteVisitsBetweenTimeframe = $this->hasSiteVisitsBetweenTimeframe($idSite, $params->getPeriod());
364 $hasChildArchivesInPeriod = $this->dataAccessModel->hasChildArchivesInPeriod($idSite, $params->getPeriod());
365
366 return $isWebsiteUsingTracker
367 && !$isArchivingForcedWhenNoVisits
368 && !$hasSiteVisitsBetweenTimeframe
369 && !$hasChildArchivesInPeriod;
370 }
371
372 private function isWebsiteUsingTheTracker($idSite)
373 {
374 $idSitesNotUsingTracker = self::getSitesNotUsingTracker();
375
376 $isUsingTracker = !in_array($idSite, $idSitesNotUsingTracker);
377
378 return $isUsingTracker;
379 }
380
381 public static function getSitesNotUsingTracker()
382 {
383 $cache = Cache::getTransientCache();
384
385 $cacheKey = 'Archiving.isWebsiteUsingTheTracker';
386 $idSitesNotUsingTracker = $cache->fetch($cacheKey);
387 if ($idSitesNotUsingTracker === false || !isset($idSitesNotUsingTracker)) {
388 // we want to trigger event only once
389 $idSitesNotUsingTracker = array();
390
391 /**
392 * This event is triggered when detecting whether there are sites that do not use the tracker.
393 *
394 * By default we only archive a site when there was actually any visit since the last archiving.
395 * However, some plugins do import data from another source instead of using the tracker and therefore
396 * will never have any visits for this site. To make sure we still archive data for such a site when
397 * archiving for this site is requested, you can listen to this event and add the idSite to the list of
398 * sites that do not use the tracker.
399 *
400 * @param bool $idSitesNotUsingTracker The list of idSites that rather import data instead of using the tracker
401 */
402 Piwik::postEvent('CronArchive.getIdSitesNotUsingTracker', array(&$idSitesNotUsingTracker));
403
404 $cache->save($cacheKey, $idSitesNotUsingTracker);
405 }
406 return $idSitesNotUsingTracker;
407 }
408
409 private function hasSiteVisitsBetweenTimeframe($idSite, Period $period)
410 {
411 $timezone = Site::getTimezoneFor($idSite);
412 list($date1, $date2) = $period->getBoundsInTimezone($timezone);
413
414 return $this->rawLogDao->hasSiteVisitsBetweenTimeframe($date1->getDatetime(), $date2->getDatetime(), $idSite);
415 }
416
417 public static function invalidateMinVisitTimeCache($idSite)
418 {
419 $cache = Cache::getLazyCache();
420 $cacheKey = 'Archiving.minVisitTime.' . $idSite;
421 $cache->delete($cacheKey);
422 }
423 }
424