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 / Plugin / Archiver.php
matomo / app / core / Plugin Last commit date
ConsoleCommand 2 years ago Dimension 1 year ago API.php 1 year ago AggregatedMetric.php 2 years ago ArchivedMetric.php 1 year ago Archiver.php 1 year ago Categories.php 2 years ago ComponentFactory.php 2 years ago ComputedMetric.php 1 year ago ConsoleCommand.php 1 year ago Controller.php 1 year ago ControllerAdmin.php 1 year ago Dependency.php 1 year ago LogTablesProvider.php 2 years ago Manager.php 1 year ago Menu.php 1 year ago MetadataLoader.php 1 year ago Metric.php 1 year ago PluginException.php 1 year ago ProcessedMetric.php 1 year ago ReleaseChannels.php 2 years ago Report.php 1 year ago ReportsProvider.php 2 years ago RequestProcessors.php 2 years ago Segment.php 1 year ago SettingsProvider.php 2 years ago Tasks.php 1 year ago ThemeStyles.php 2 years ago ViewDataTable.php 1 year ago Visualization.php 1 year ago WidgetsProvider.php 2 years ago
Archiver.php
390 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\Plugin;
10
11 use Piwik\ArchiveProcessor;
12 use Piwik\Cache;
13 use Piwik\CacheId;
14 use Piwik\Config as PiwikConfig;
15 use Piwik\Container\StaticContainer;
16 use Piwik\ErrorHandler;
17 use Piwik\Log;
18 use Piwik\Piwik;
19 /**
20 * The base class that should be extended by plugins that compute their own
21 * analytics data.
22 *
23 * Descendants should implement the {@link aggregateDayReport()} and {@link aggregateMultipleReports()}
24 * methods.
25 *
26 * Both of these methods should persist analytics data using the {@link \Piwik\ArchiveProcessor}
27 * instance returned by {@link getProcessor()}. The {@link aggregateDayReport()} method should
28 * compute analytics data using the {@link \Piwik\DataAccess\LogAggregator} instance
29 * returned by {@link getLogAggregator()}.
30 *
31 * ### Examples
32 *
33 * **Extending Archiver**
34 *
35 * class MyArchiver extends Archiver
36 * {
37 * public function aggregateDayReport()
38 * {
39 * $logAggregator = $this->getLogAggregator();
40 *
41 * $data = $logAggregator->queryVisitsByDimension(...);
42 *
43 * $dataTable = new DataTable();
44 * $dataTable->addRowsFromSimpleArray($data);
45 *
46 * $archiveProcessor = $this->getProcessor();
47 * $archiveProcessor->insertBlobRecords('MyPlugin_myReport', $dataTable->getSerialized(500));
48 * }
49 *
50 * public function aggregateMultipleReports()
51 * {
52 * $archiveProcessor = $this->getProcessor();
53 * $archiveProcessor->aggregateDataTableRecords('MyPlugin_myReport', 500);
54 * }
55 * }
56 *
57 * @api
58 */
59 class Archiver
60 {
61 public static $ARCHIVE_DEPENDENT = \true;
62 /**
63 * @var \Piwik\ArchiveProcessor
64 */
65 private $processor;
66 /**
67 * @var bool
68 */
69 private $enabled;
70 /**
71 * @var mixed
72 */
73 protected $maximumRows;
74 /**
75 * Used if a plugin has RecordBuilders but no Archiver subclass.
76 *
77 * @var string|null
78 */
79 private $pluginName = null;
80 /**
81 * Constructor.
82 *
83 * @param ArchiveProcessor $processor The ArchiveProcessor instance to use when persisting archive
84 * data.
85 */
86 public function __construct(ArchiveProcessor $processor, ?string $pluginName = null)
87 {
88 $this->maximumRows = PiwikConfig::getInstance()->General['datatable_archiving_maximum_rows_standard'];
89 $this->processor = $processor;
90 $this->enabled = \true;
91 $this->pluginName = $pluginName;
92 }
93 private function getPluginName() : string
94 {
95 return $this->pluginName ?: Piwik::getPluginNameOfMatomoClass(get_class($this));
96 }
97 /**
98 * @return ArchiveProcessor\RecordBuilder[]
99 * @throws \DI\DependencyException
100 * @throws \DI\NotFoundException
101 */
102 private function getRecordBuilders(string $pluginName) : array
103 {
104 $transientCache = Cache::getTransientCache();
105 $cacheKey = CacheId::siteAware('Archiver.RecordBuilders') . '.' . $pluginName;
106 $recordBuilders = $transientCache->fetch($cacheKey);
107 if ($recordBuilders === \false) {
108 $recordBuilderClasses = $this->getAllRecordBuilderClasses();
109 // only select RecordBuilders for the selected plugin
110 $recordBuilderClasses = array_filter($recordBuilderClasses, function ($className) use($pluginName) {
111 return Piwik::getPluginNameOfMatomoClass($className) == $pluginName;
112 });
113 $recordBuilders = array_map(function ($className) {
114 return StaticContainer::getContainer()->make($className);
115 }, $recordBuilderClasses);
116 /**
117 * Triggered to add new RecordBuilders that cannot be picked up automatically by the platform.
118 * If you define RecordBuilders that take a parameter, for example, an ID to an entity your plugin
119 * manages, use this event to add instances of that RecordBuilder to the global list.
120 *
121 * **Example**
122 *
123 * public function addRecordBuilders(&$recordBuilders)
124 * {
125 * $recordBuilders[] = new MyParameterizedRecordBuilder($idOfThingToArchiveFor);
126 * }
127 *
128 * @param ArchiveProcessor\RecordBuilder[] $recordBuilders An array of RecordBuilder instances
129 * @api
130 */
131 Piwik::postEvent('Archiver.addRecordBuilders', [&$recordBuilders], \false, [$pluginName]);
132 $transientCache->save($cacheKey, $recordBuilders);
133 }
134 /**
135 * Triggered to filter / restrict reports.
136 *
137 * **Example**
138 *
139 * public function filterRecordBuilders(&$recordBuilders)
140 * {
141 * foreach ($reports as $index => $recordBuilder) {
142 * if ($recordBuilders instanceof AnotherPluginRecordBuilder) {
143 * unset($reports[$index]);
144 * }
145 * }
146 * }
147 *
148 * @param ArchiveProcessor\RecordBuilder[] $recordBuilders An array of RecordBuilder instances
149 * @api
150 */
151 Piwik::postEvent('Archiver.filterRecordBuilders', [&$recordBuilders]);
152 return $recordBuilders;
153 }
154 private function filterRecordBuildersByRequestedRecords(array $recordBuilders, array $requestedReports) : array
155 {
156 // No record builders might be provided if the plugin does not (yet) provide any
157 if (empty($recordBuilders)) {
158 return $recordBuilders;
159 }
160 if (!empty($requestedReports)) {
161 $recordBuilders = array_filter($recordBuilders, function (ArchiveProcessor\RecordBuilder $builder) use($requestedReports) {
162 return $builder->isBuilderForAtLeastOneOf($this->processor, $requestedReports);
163 });
164 }
165 if (0 === count($recordBuilders)) {
166 Log::debug('Archiver: No record builders found for requested records %s', implode(',', $this->processor->getParams()->getArchiveOnlyReportAsArray()));
167 }
168 return $recordBuilders;
169 }
170 /**
171 * @ignore
172 */
173 public final function callAggregateDayReport()
174 {
175 try {
176 ErrorHandler::pushFatalErrorBreadcrumb(static::class);
177 $pluginName = $this->getPluginName();
178 if (\Piwik\Plugin\Manager::getInstance()->isPluginLoaded($pluginName)) {
179 $allRecordBuilders = $this->getRecordBuilders($pluginName);
180 $recordBuilders = $this->filterRecordBuildersByRequestedRecords($allRecordBuilders, $this->processor->getParams()->getArchiveOnlyReportAsArray());
181 // If the plugin provides record builders and only a specific record was requested, we mark the archive as partial
182 if (count($allRecordBuilders) > 0 && $this->processor->getParams()->getArchiveOnlyReport()) {
183 $this->processor->getParams()->setIsPartialArchive(\true);
184 }
185 foreach ($recordBuilders as $recordBuilder) {
186 if (!$recordBuilder->isEnabled($this->getProcessor())) {
187 continue;
188 }
189 $originalQueryHint = $this->getProcessor()->getLogAggregator()->getQueryOriginHint();
190 $newQueryHint = $originalQueryHint . ' ' . $recordBuilder->getQueryOriginHint();
191 try {
192 $this->getProcessor()->getLogAggregator()->setQueryOriginHint($newQueryHint);
193 $recordBuilder->buildFromLogs($this->getProcessor());
194 } finally {
195 $this->getProcessor()->getLogAggregator()->setQueryOriginHint($originalQueryHint);
196 }
197 }
198 }
199 $this->aggregateDayReport();
200 $this->processDependentArchivesForPlugins();
201 } finally {
202 ErrorHandler::popFatalErrorBreadcrumb();
203 }
204 }
205 /**
206 * @ignore
207 */
208 public final function callAggregateMultipleReports()
209 {
210 try {
211 ErrorHandler::pushFatalErrorBreadcrumb(static::class);
212 $pluginName = $this->getPluginName();
213 if (\Piwik\Plugin\Manager::getInstance()->isPluginLoaded($pluginName)) {
214 $allRecordBuilders = $this->getRecordBuilders($pluginName);
215 $recordBuilders = $this->filterRecordBuildersByRequestedRecords($allRecordBuilders, $this->processor->getParams()->getArchiveOnlyReportAsArray());
216 // If the plugin provides record builders and only a specific record was requested, we mark the archive as partial
217 if (count($allRecordBuilders) > 0 && $this->processor->getParams()->getArchiveOnlyReport()) {
218 $this->processor->getParams()->setIsPartialArchive(\true);
219 }
220 foreach ($recordBuilders as $recordBuilder) {
221 if (!$recordBuilder->isEnabled($this->getProcessor())) {
222 continue;
223 }
224 $originalQueryHint = $this->getProcessor()->getLogAggregator()->getQueryOriginHint();
225 $newQueryHint = $originalQueryHint . ' ' . $recordBuilder->getQueryOriginHint();
226 try {
227 $this->getProcessor()->getLogAggregator()->setQueryOriginHint($newQueryHint);
228 $recordBuilder->buildForNonDayPeriod($this->getProcessor());
229 } finally {
230 $this->getProcessor()->getLogAggregator()->setQueryOriginHint($originalQueryHint);
231 }
232 }
233 }
234 $this->aggregateMultipleReports();
235 $this->processDependentArchivesForPlugins();
236 } finally {
237 ErrorHandler::popFatalErrorBreadcrumb();
238 }
239 }
240 /**
241 * Archives data for a day period.
242 *
243 * Implementations of this method should do more computation intensive activities such
244 * as aggregating data across log tables. Since this method only deals w/ data logged for a day,
245 * aggregating individual log table rows isn't a problem. Doing this for any larger period,
246 * however, would cause performance degradation.
247 *
248 * Aggregate log table rows using a {@link Piwik\DataAccess\LogAggregator} instance. Get a
249 * {@link Piwik\DataAccess\LogAggregator} instance using the {@link getLogAggregator()} method.
250 */
251 public function aggregateDayReport()
252 {
253 // empty
254 }
255 /**
256 * Archives data for a non-day period.
257 *
258 * Implementations of this method should only aggregate existing reports of subperiods of the
259 * current period. For example, it is more efficient to aggregate reports for each day of a
260 * week than to aggregate each log entry of the week.
261 *
262 * Use {@link Piwik\ArchiveProcessor::aggregateNumericMetrics()} and {@link Piwik\ArchiveProcessor::aggregateDataTableRecords()}
263 * to aggregate archived reports. Get the {@link Piwik\ArchiveProcessor} instance using the {@link getProcessor()}
264 * method.
265 */
266 public function aggregateMultipleReports()
267 {
268 // empty
269 }
270 /**
271 * Returns a {@link Piwik\ArchiveProcessor} instance that can be used to insert archive data for
272 * the period, segment and site we are archiving data for.
273 *
274 * @return \Piwik\ArchiveProcessor
275 * @api
276 */
277 protected function getProcessor()
278 {
279 return $this->processor;
280 }
281 /**
282 * Returns a {@link Piwik\DataAccess\LogAggregator} instance that can be used to aggregate log table rows
283 * for this period, segment and site.
284 *
285 * @return \Piwik\DataAccess\LogAggregator
286 * @api
287 */
288 protected function getLogAggregator()
289 {
290 return $this->getProcessor()->getLogAggregator();
291 }
292 public function disable()
293 {
294 $this->enabled = \false;
295 }
296 /**
297 * Whether this Archiver should be used or not.
298 *
299 * @return bool
300 */
301 public function isEnabled()
302 {
303 return $this->enabled;
304 }
305 /**
306 * By overwriting this method and returning true, a plugin archiver can force the archiving to run even when there
307 * was no visit for the website/date/period/segment combination
308 * (by default, archivers are skipped when there is no visit).
309 *
310 * @return bool
311 */
312 public static function shouldRunEvenWhenNoVisits()
313 {
314 return \false;
315 }
316 /**
317 * Returns a list of segments that should be pre-archived along with the segment currently being archived.
318 * The segments in this list will be added to the current segment via an AND condition and archiving
319 * for the current plugin will be launched. This process will not recurse further.
320 *
321 * If your plugin's API appends conditions to the requested segment when fetching data, you will want to
322 * use this method to make sure those segments get pre-archived. Otherwise, if browser archiving is disabled,
323 * the modified segments will appear to have no data.
324 *
325 * To archive another plugin, use an array instead of a string segment, for example:
326 *
327 * ```
328 * ['plugin' => 'VisitsSummary', 'segment' => '...']
329 * ```
330 *
331 * See the Goals and VisitFrequency plugins for examples.
332 *
333 * @return array
334 * @api
335 */
336 public function getDependentSegmentsToArchive() : array
337 {
338 return [];
339 }
340 protected function isRequestedReport(string $reportName)
341 {
342 $requestedReport = $this->getProcessor()->getParams()->getArchiveOnlyReport();
343 return empty($requestedReport) || $requestedReport == $reportName;
344 }
345 private function processDependentArchivesForPlugins()
346 {
347 if (!self::$ARCHIVE_DEPENDENT) {
348 return;
349 }
350 $dependentSegments = $this->getDependentSegmentsToArchive();
351 foreach ($dependentSegments as $dependentSegment) {
352 $plugin = $this->getPluginName();
353 $segment = $dependentSegment;
354 if (is_array($dependentSegment)) {
355 $plugin = $dependentSegment['plugin'] ?? $plugin;
356 $segment = $dependentSegment['segment'];
357 }
358 $this->getProcessor()->processDependentArchive($plugin, $segment);
359 }
360 }
361 private static function getDefaultConstructibleClasses(array $classes) : array
362 {
363 return array_filter($classes, function ($className) {
364 return (new \ReflectionClass($className))->getConstructor()->getNumberOfRequiredParameters() == 0;
365 });
366 }
367 private static function getAllRecordBuilderClasses() : array
368 {
369 $transientCache = Cache::getTransientCache();
370 $cacheKey = CacheId::siteAware('RecordBuilders.allRecordBuilders');
371 $recordBuilderClasses = $transientCache->fetch($cacheKey);
372 if ($recordBuilderClasses === \false) {
373 $recordBuilderClasses = \Piwik\Plugin\Manager::getInstance()->findMultipleComponents('RecordBuilders', ArchiveProcessor\RecordBuilder::class);
374 $recordBuilderClasses = self::getDefaultConstructibleClasses($recordBuilderClasses);
375 $transientCache->save($cacheKey, $recordBuilderClasses);
376 }
377 return $recordBuilderClasses;
378 }
379 public static function doesPluginHaveRecordBuilders(string $pluginName) : bool
380 {
381 $recordBuilders = self::getAllRecordBuilderClasses();
382 foreach ($recordBuilders as $builder) {
383 if ($pluginName === Piwik::getPluginNameOfMatomoClass($builder)) {
384 return \true;
385 }
386 }
387 return \false;
388 }
389 }
390