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.php
matomo / app / core Last commit date
API 1 year ago Access 1 year ago Application 1 year ago Archive 1 year ago ArchiveProcessor 1 year ago Archiver 2 years ago AssetManager 1 year ago Auth 1 year ago Category 2 years ago Changes 1 year ago CliMulti 1 year ago Columns 1 year ago Concurrency 1 year ago Config 1 year ago Container 1 year ago CronArchive 1 year ago DataAccess 1 year ago DataFiles 2 years ago DataTable 1 year ago Db 1 year ago DeviceDetector 1 year ago Email 2 years ago Exception 1 year ago Http 1 year ago Intl 1 year ago Log 2 years ago Mail 1 year ago Measurable 1 year ago Menu 1 year ago Metrics 1 year ago Notification 1 year ago Period 1 year ago Plugin 1 year ago ProfessionalServices 1 year ago Report 1 year ago ReportRenderer 1 year ago Scheduler 1 year ago Segment 1 year ago Session 1 year ago Settings 1 year ago Tracker 1 year ago Translation 1 year ago Twig 1 year ago UpdateCheck 1 year ago Updater 1 year ago Updates 1 year ago Validators 1 year ago View 1 year ago ViewDataTable 1 year ago Visualization 1 year ago Widget 1 year ago .htaccess 2 years ago Access.php 1 year ago Archive.php 1 year ago ArchiveProcessor.php 1 year ago AssetManager.php 1 year ago Auth.php 2 years ago AuthResult.php 2 years ago BaseFactory.php 2 years ago Cache.php 2 years ago CacheId.php 1 year ago CliMulti.php 1 year ago Common.php 1 year ago Config.php 1 year ago Console.php 1 year ago Context.php 2 years ago Cookie.php 1 year ago CronArchive.php 1 year ago DI.php 1 year ago DataArray.php 1 year ago DataTable.php 1 year ago Date.php 1 year ago Db.php 1 year ago DbHelper.php 1 year ago Development.php 1 year ago ErrorHandler.php 1 year ago EventDispatcher.php 1 year ago ExceptionHandler.php 1 year ago FileIntegrity.php 1 year ago Filechecks.php 1 year ago Filesystem.php 1 year ago FrontController.php 1 year ago Http.php 1 year ago IP.php 1 year ago Log.php 2 years ago LogDeleter.php 1 year ago Mail.php 1 year ago Metrics.php 1 year ago NoAccessException.php 2 years ago Nonce.php 1 year ago Notification.php 1 year ago NumberFormatter.php 1 year ago Option.php 1 year ago Period.php 1 year ago Piwik.php 1 year ago Plugin.php 1 year ago Process.php 1 year ago Profiler.php 1 year ago ProxyHeaders.php 2 years ago ProxyHttp.php 1 year ago QuickForm2.php 1 year ago RankingQuery.php 1 year ago ReportRenderer.php 1 year ago Request.php 1 year ago Segment.php 1 year ago Sequence.php 2 years ago Session.php 1 year ago SettingsPiwik.php 1 year ago SettingsServer.php 1 year ago Singleton.php 2 years ago Site.php 1 year ago SiteContentDetector.php 1 year ago SupportedBrowser.php 2 years ago TCPDF.php 1 year ago Theme.php 1 year ago Timer.php 2 years ago Tracker.php 1 year ago Twig.php 1 year ago Unzip.php 1 year ago UpdateCheck.php 1 year ago Updater.php 1 year ago UpdaterErrorException.php 2 years ago Updates.php 1 year ago Url.php 1 year ago UrlHelper.php 1 year ago Version.php 1 year ago View.php 1 year ago bootstrap.php 1 year ago dispatch.php 2 years ago testMinimumPhpVersion.php 2 years ago
ArchiveProcessor.php
683 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;
10
11 use Exception;
12 use Piwik\Archive\DataTableFactory;
13 use Piwik\ArchiveProcessor\Parameters;
14 use Piwik\ArchiveProcessor\Rules;
15 use Piwik\Container\StaticContainer;
16 use Piwik\DataAccess\ArchiveWriter;
17 use Piwik\DataAccess\LogAggregator;
18 use Piwik\DataTable\Manager;
19 use Piwik\DataTable\Map;
20 use Piwik\DataTable\Row;
21 use Piwik\Segment\SegmentExpression;
22 use Piwik\Log\LoggerInterface;
23 /**
24 * Used by {@link Piwik\Plugin\Archiver} instances to insert and aggregate archive data.
25 *
26 * ### See also
27 *
28 * - **{@link Piwik\Plugin\Archiver}** - to learn how plugins should implement their own analytics
29 * aggregation logic.
30 * - **{@link Piwik\DataAccess\LogAggregator}** - to learn how plugins can perform data aggregation
31 * across Piwik's log tables.
32 *
33 * ### Examples
34 *
35 * **Inserting numeric data**
36 *
37 * // function in an Archiver descendant
38 * public function aggregateDayReport()
39 * {
40 * $archiveProcessor = $this->getProcessor();
41 *
42 * $myFancyMetric = // ... calculate the metric value ...
43 * $archiveProcessor->insertNumericRecord('MyPlugin_myFancyMetric', $myFancyMetric);
44 * }
45 *
46 * **Inserting serialized DataTables**
47 *
48 * // function in an Archiver descendant
49 * public function aggregateDayReport()
50 * {
51 * $archiveProcessor = $this->getProcessor();
52 *
53 * $maxRowsInTable = Config::getInstance()->General['datatable_archiving_maximum_rows_standard'];j
54 *
55 * $dataTable = // ... build by aggregating visits ...
56 * $serializedData = $dataTable->getSerialized($maxRowsInTable, $maxRowsInSubtable = $maxRowsInTable,
57 * $columnToSortBy = Metrics::INDEX_NB_VISITS);
58 *
59 * $archiveProcessor->insertBlobRecords('MyPlugin_myFancyReport', $serializedData);
60 * }
61 *
62 * **Aggregating archive data**
63 *
64 * // function in Archiver descendant
65 * public function aggregateMultipleReports()
66 * {
67 * $archiveProcessor = $this->getProcessor();
68 *
69 * // aggregate a metric
70 * $archiveProcessor->aggregateNumericMetrics('MyPlugin_myFancyMetric');
71 * $archiveProcessor->aggregateNumericMetrics('MyPlugin_mySuperFancyMetric', 'max');
72 *
73 * // aggregate a report
74 * $archiveProcessor->aggregateDataTableRecords('MyPlugin_myFancyReport');
75 * }
76 *
77 */
78 class ArchiveProcessor
79 {
80 /**
81 * @var bool
82 */
83 public static $isRootArchivingRequest = \true;
84 /**
85 * @var \Piwik\DataAccess\ArchiveWriter
86 */
87 private $archiveWriter;
88 /**
89 * @var \Piwik\DataAccess\LogAggregator
90 */
91 private $logAggregator;
92 /**
93 * @var Archive
94 */
95 public $archive = null;
96 /**
97 * @var Parameters
98 */
99 private $params;
100 /**
101 * @var int
102 */
103 private $numberOfVisits = \false;
104 private $numberOfVisitsConverted = \false;
105 private $processedDependentSegments = [];
106 public function __construct(Parameters $params, ArchiveWriter $archiveWriter, LogAggregator $logAggregator)
107 {
108 $this->params = $params;
109 $this->logAggregator = $logAggregator;
110 $this->archiveWriter = $archiveWriter;
111 }
112 protected function getArchive()
113 {
114 if (empty($this->archive)) {
115 $subPeriods = $this->params->getSubPeriods();
116 $idSites = $this->params->getIdSites();
117 $this->archive = \Piwik\Archive::factory($this->params->getSegment(), $subPeriods, $idSites);
118 /**
119 * @internal
120 */
121 \Piwik\Piwik::postEvent('ArchiveProcessor.getArchive', [$this->archive]);
122 }
123 return $this->archive;
124 }
125 public function setNumberOfVisits($visits, $visitsConverted)
126 {
127 $this->numberOfVisits = $visits;
128 $this->numberOfVisitsConverted = $visitsConverted;
129 }
130 /**
131 * Returns the {@link Parameters} object containing the site, period and segment we're archiving
132 * data for.
133 *
134 * @return Parameters
135 * @api
136 */
137 public function getParams()
138 {
139 return $this->params;
140 }
141 /**
142 * Returns a `{@link Piwik\DataAccess\LogAggregator}` instance for the site, period and segment this
143 * ArchiveProcessor will insert archive data for.
144 *
145 * @return LogAggregator
146 * @api
147 */
148 public function getLogAggregator()
149 {
150 return $this->logAggregator;
151 }
152 /**
153 * Array of (column name before => column name renamed) of the columns for which sum operation is invalid.
154 * These columns will be renamed as per this mapping.
155 * @var array
156 */
157 protected static $columnsToRenameAfterAggregation = array(\Piwik\Metrics::INDEX_NB_UNIQ_VISITORS => \Piwik\Metrics::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, \Piwik\Metrics::INDEX_NB_USERS => \Piwik\Metrics::INDEX_SUM_DAILY_NB_USERS);
158 /**
159 * Sums records for every subperiod of the current period and inserts the result as the record
160 * for this period.
161 *
162 * DataTables are summed recursively so subtables will be summed as well.
163 *
164 * @param string|array $recordNames Name(s) of the report we are aggregating, eg, `'Referrers_type'`.
165 * @param int $maximumRowsInDataTableLevelZero Maximum number of rows allowed in the top level DataTable.
166 * @param int $maximumRowsInSubDataTable Maximum number of rows allowed in each subtable.
167 * @param string|null $defaultColumnToSortByBeforeTruncation The name of the column to sort by before truncating a DataTable.
168 * If not set, and the table contains nb_visits or INDEX_NB_VISITS, we will
169 * sort by visits.
170 * @param array $columnsAggregationOperation Operations for aggregating columns, see {@link Row::sumRow()}.
171 * @param array $columnsToRenameAfterAggregation Columns mapped to new names for columns that must change names
172 * when summed because they cannot be summed, eg,
173 * `array('nb_uniq_visitors' => 'sum_daily_nb_uniq_visitors')`.
174 * @param bool|array $countRowsRecursive if set to true, will calculate the recursive rows count for all record names
175 * which makes it slower. If you only need it for some records pass an array of
176 * recordNames that defines for which ones you need a recursive row count.
177 * @return array Returns the row counts of each aggregated report before truncation, eg,
178 *
179 * array(
180 * 'report1' => array('level0' => $report1->getRowsCount,
181 * 'recursive' => $report1->getRowsCountRecursive()),
182 * 'report2' => array('level0' => $report2->getRowsCount,
183 * 'recursive' => $report2->getRowsCountRecursive()),
184 * ...
185 * )
186 * @api
187 */
188 public function aggregateDataTableRecords($recordNames, $maximumRowsInDataTableLevelZero = null, $maximumRowsInSubDataTable = null, $defaultColumnToSortByBeforeTruncation = null, &$columnsAggregationOperation = null, $columnsToRenameAfterAggregation = null, $countRowsRecursive = \true)
189 {
190 /** @var LoggerInterface $logger */
191 $logger = StaticContainer::get(LoggerInterface::class);
192 if (!is_array($recordNames)) {
193 $recordNames = array($recordNames);
194 }
195 $archiveDescription = $this->params . '';
196 $nameToCount = array();
197 foreach ($recordNames as $recordName) {
198 $latestUsedTableId = Manager::getInstance()->getMostRecentTableId();
199 $logger->debug("aggregating record {record} [archive = {archive}]", ['record' => $recordName, 'archive' => $archiveDescription]);
200 $table = $this->aggregateDataTableRecord($recordName, $columnsAggregationOperation, $columnsToRenameAfterAggregation);
201 $nameToCount[$recordName]['level0'] = $table->getRowsCount();
202 if ($countRowsRecursive === \true || is_array($countRowsRecursive) && in_array($recordName, $countRowsRecursive)) {
203 $nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive();
204 }
205 $columnToSortByBeforeTruncation = $defaultColumnToSortByBeforeTruncation;
206 if (empty($columnToSortByBeforeTruncation)) {
207 $columns = $table->getColumns();
208 if (in_array(\Piwik\Metrics::INDEX_NB_VISITS, $columns)) {
209 $columnToSortByBeforeTruncation = \Piwik\Metrics::INDEX_NB_VISITS;
210 } elseif (in_array('nb_visits', $columns)) {
211 $columnToSortByBeforeTruncation = 'nb_visits';
212 }
213 }
214 $blob = $table->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation);
215 \Piwik\Common::destroy($table);
216 $this->insertBlobRecord($recordName, $blob);
217 unset($blob);
218 \Piwik\DataTable\Manager::getInstance()->deleteAll($latestUsedTableId);
219 }
220 return $nameToCount;
221 }
222 /**
223 * Aggregates one or more metrics for every subperiod of the current period and inserts the results
224 * as metrics for the current period.
225 *
226 * @param array|string $columns Array of metric names to aggregate.
227 * @param bool|string|string[] $operationToApply The operation to apply to the metric. Either `'sum'`, `'max'` or `'min'`.
228 * Can also be an array mapping record names to operations.
229 * @return array|int Returns the array of aggregate values. If only one metric was aggregated,
230 * the aggregate value will be returned as is, not in an array.
231 * For example, if `array('nb_visits', 'nb_hits')` is supplied for `$columns`,
232 *
233 * array(
234 * 'nb_visits' => 3040,
235 * 'nb_hits' => 405
236 * )
237 *
238 * could be returned. If `array('nb_visits')` or `'nb_visits'` is used for `$columns`,
239 * then `3040` would be returned.
240 * @api
241 */
242 public function aggregateNumericMetrics($columns, $operationsToApply = \false)
243 {
244 $metrics = $this->getAggregatedNumericMetrics($columns, $operationsToApply);
245 foreach ($metrics as $column => $value) {
246 $this->insertNumericRecord($column, $value);
247 }
248 // if asked for only one field to sum
249 if (count($metrics) === 1) {
250 return reset($metrics);
251 }
252 // returns the array of records once summed
253 return $metrics;
254 }
255 public function getNumberOfVisits()
256 {
257 if ($this->numberOfVisits === \false) {
258 throw new Exception("visits should have been set here");
259 }
260 return $this->numberOfVisits;
261 }
262 public function getNumberOfVisitsConverted()
263 {
264 return $this->numberOfVisitsConverted;
265 }
266 /**
267 * Caches multiple numeric records in the archive for this processor's site, period
268 * and segment.
269 *
270 * @param array $numericRecords A name-value mapping of numeric values that should be
271 * archived, eg,
272 *
273 * array('Referrers_distinctKeywords' => 23, 'Referrers_distinctCampaigns' => 234)
274 * @api
275 */
276 public function insertNumericRecords($numericRecords)
277 {
278 foreach ($numericRecords as $name => $value) {
279 $this->insertNumericRecord($name, $value);
280 }
281 }
282 /**
283 * Caches a single numeric record in the archive for this processor's site, period and
284 * segment.
285 *
286 * Numeric values are not inserted if they equal `0`.
287 *
288 * @param string $name The name of the numeric value, eg, `'Referrers_distinctKeywords'`.
289 * @param float $value The numeric value.
290 * @api
291 */
292 public function insertNumericRecord($name, $value)
293 {
294 $value = round($value ?? 0, 2);
295 $value = \Piwik\Common::forceDotAsSeparatorForDecimalPoint($value);
296 $this->archiveWriter->insertRecord($name, $value);
297 }
298 /**
299 * Caches one or more blob records in the archive for this processor's site, period
300 * and segment.
301 *
302 * @param string $name The name of the record, eg, 'Referrers_type'.
303 * @param string|array $values A blob string or an array of blob strings. If an array
304 * is used, the first element in the array will be inserted
305 * with the `$name` name. The others will be inserted with
306 * `$name . '_' . $index` as the record name (where $index is
307 * the index of the blob record in `$values`).
308 * @api
309 */
310 public function insertBlobRecord($name, $values)
311 {
312 $this->archiveWriter->insertBlobRecord($name, $values);
313 }
314 /**
315 * This method selects all DataTables that have the name $name over the period.
316 * All these DataTables are then added together, and the resulting DataTable is returned.
317 *
318 * @param string $name
319 * @param array $columnsAggregationOperation Operations for aggregating columns, @see Row::sumRow()
320 * @param array $columnsToRenameAfterAggregation columns in the array (old name, new name) to be renamed as the sum operation is not valid on them (eg. nb_uniq_visitors->sum_daily_nb_uniq_visitors)
321 * @return DataTable
322 */
323 protected function aggregateDataTableRecord($name, $columnsAggregationOperation = null, $columnsToRenameAfterAggregation = null)
324 {
325 try {
326 \Piwik\ErrorHandler::pushFatalErrorBreadcrumb(__CLASS__, ['name' => $name]);
327 $blobs = $this->getArchive()->querySingleBlob($name);
328 $dataTable = $this->getAggregatedDataTableMapFromBlobs($blobs, $columnsAggregationOperation, $columnsToRenameAfterAggregation, $name);
329 } finally {
330 \Piwik\ErrorHandler::popFatalErrorBreadcrumb();
331 }
332 return $dataTable;
333 }
334 protected function getAggregatedDataTableMapFromBlobs(\Iterator $dataTableBlobs, $columnsAggregationOperation, $columnsToRenameAfterAggregation, $name)
335 {
336 // maps period & subtable ID in database to the Row instance in $result that subtable should be added to when encountered
337 // [$row['date1'].','.$row['date2']][$tableId] = $row in $result
338 /** @var Row[][] */
339 $tableIdToResultRowMapping = [];
340 $result = new \Piwik\DataTable();
341 if (!empty($columnsAggregationOperation)) {
342 $result->setMetadata(\Piwik\DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnsAggregationOperation);
343 }
344 foreach ($dataTableBlobs as $archiveDataRow) {
345 $period = $archiveDataRow['date1'] . ',' . $archiveDataRow['date2'];
346 $tableId = $archiveDataRow['name'] == $name ? null : $this->getSubtableIdFromBlobName($archiveDataRow['name']);
347 $blobTable = \Piwik\DataTable::fromSerializedArray($archiveDataRow['value']);
348 // see https://github.com/piwik/piwik/issues/4377
349 $blobTable->filter(function ($table) use($columnsToRenameAfterAggregation, $name) {
350 if ($this->areColumnsNotAlreadyRenamed($table)) {
351 /**
352 * This makes archiving and range dates a lot faster. Imagine we archive a week, then we will
353 * rename all columns of each 7 day archives. Afterwards we know the columns will be replaced in a
354 * week archive. When generating month archives, which uses mostly week archives, we do not have
355 * to replace those columns for the week archives again since we can be sure they were already
356 * replaced. Same when aggregating year and range archives. This can save up 10% or more when
357 * aggregating Month, Year and Range archives.
358 */
359 $this->renameColumnsAfterAggregation($table, $columnsToRenameAfterAggregation);
360 }
361 });
362 $tableToAddTo = null;
363 if ($tableId === null) {
364 $tableToAddTo = $result;
365 } elseif (empty($tableIdToResultRowMapping[$period][$tableId])) {
366 // sanity check
367 StaticContainer::get(LoggerInterface::class)->info('Unexpected state when aggregating DataTable, unknown period/table ID combination encountered: {period} - {tableId}.' . ' This either means the SQL to order blobs is behaving incorrectly or the blob data is corrupt in some way.', ['period' => $period, 'tableId' => $tableId]);
368 continue;
369 } else {
370 $rowToAddTo = $tableIdToResultRowMapping[$period][$tableId];
371 if (!$rowToAddTo->getIdSubDataTable()) {
372 $newTable = new \Piwik\DataTable();
373 $newTable->setMetadata(\Piwik\DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnsAggregationOperation);
374 $rowToAddTo->setSubtable($newTable);
375 }
376 $tableToAddTo = $rowToAddTo->getSubtable();
377 }
378 $tableToAddTo->addDataTable($blobTable);
379 // add subtable IDs for $blobTableRow to $tableIdToResultRowMapping
380 foreach ($blobTable->getRows() as $blobTableRow) {
381 $label = $blobTableRow->getColumn('label');
382 $subtableId = $blobTableRow->getIdSubDataTable();
383 if (empty($subtableId)) {
384 continue;
385 }
386 $rowToAddTo = $tableToAddTo->getRowFromLabel($label);
387 $tableIdToResultRowMapping[$period][$subtableId] = $rowToAddTo;
388 }
389 \Piwik\Common::destroy($blobTable);
390 unset($blobTable);
391 }
392 return $result;
393 }
394 private function getSubtableIdFromBlobName($recordName)
395 {
396 $parts = explode('_', $recordName);
397 $id = end($parts);
398 if (is_numeric($id)) {
399 return $id;
400 }
401 return null;
402 }
403 /**
404 * Note: public only for use in closure in PHP 5.3.
405 *
406 * @param $table
407 * @return \Piwik\Period
408 */
409 public function areColumnsNotAlreadyRenamed($table)
410 {
411 $period = $table->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX);
412 return !$period || $period->getLabel() === 'day';
413 }
414 protected function getOperationForColumns($columns, $defaultOperation)
415 {
416 $operationForColumn = array();
417 foreach ($columns as $name) {
418 $operation = is_array($defaultOperation) ? $defaultOperation[$name] ?? null : $defaultOperation;
419 if (empty($operation)) {
420 $operation = $this->guessOperationForColumn($name);
421 }
422 $operationForColumn[$name] = $operation;
423 }
424 return $operationForColumn;
425 }
426 protected function enrichWithUniqueVisitorsMetric(Row $row)
427 {
428 if ($row->getColumn('nb_uniq_visitors') === \false && $row->getColumn('nb_users') === \false) {
429 return;
430 }
431 $periodLabel = $this->getParams()->getPeriod()->getLabel();
432 if (!\Piwik\SettingsPiwik::isUniqueVisitorsEnabled($periodLabel)) {
433 $row->deleteColumn('nb_uniq_visitors');
434 $row->deleteColumn('nb_users');
435 return;
436 }
437 $sites = $this->getIdSitesToComputeNbUniques();
438 if (count($sites) > 1 && Rules::shouldSkipUniqueVisitorsCalculationForMultipleSites()) {
439 if ($periodLabel != 'day') {
440 // for day we still keep the aggregated metric but for other periods we remove it as it becomes to
441 // inaccurate
442 $row->deleteColumn('nb_uniq_visitors');
443 $row->deleteColumn('nb_users');
444 }
445 return;
446 }
447 if (empty($sites)) {
448 // a plugin disabled running below query by removing all sites.
449 $row->deleteColumn('nb_uniq_visitors');
450 $row->deleteColumn('nb_users');
451 return;
452 }
453 if (count($sites) === 1) {
454 $uniqueVisitorsMetric = \Piwik\Metrics::INDEX_NB_UNIQ_VISITORS;
455 } else {
456 if (!\Piwik\SettingsPiwik::isSameFingerprintAcrossWebsites()) {
457 throw new Exception("Processing unique visitors across websites is enabled for this instance,\n but to process this metric you must first set enable_fingerprinting_across_websites=1\n in the config file, under the [Tracker] section.");
458 }
459 $uniqueVisitorsMetric = \Piwik\Metrics::INDEX_NB_UNIQ_FINGERPRINTS;
460 }
461 $metrics = array(\Piwik\Metrics::INDEX_NB_USERS, $uniqueVisitorsMetric);
462 $uniques = $this->computeNbUniques($metrics, $sites);
463 // see edge case as described in https://github.com/piwik/piwik/issues/9357 where uniq_visitors might be higher
464 // than visits because we archive / process it after nb_visits. Between archiving nb_visits and nb_uniq_visitors
465 // there could have been a new visit leading to a higher nb_unique_visitors than nb_visits which is not possible
466 // by definition. In this case we simply use the visits metric instead of unique visitors metric.
467 $visits = $row->getColumn('nb_visits');
468 if ($visits !== \false && $uniques[$uniqueVisitorsMetric] !== \false) {
469 $uniques[$uniqueVisitorsMetric] = min($uniques[$uniqueVisitorsMetric], $visits);
470 }
471 $row->setColumn('nb_uniq_visitors', $uniques[$uniqueVisitorsMetric]);
472 $row->setColumn('nb_users', $uniques[\Piwik\Metrics::INDEX_NB_USERS]);
473 }
474 protected function guessOperationForColumn($column)
475 {
476 if (strpos($column, 'max_') === 0) {
477 return 'max';
478 }
479 if (strpos($column, 'min_') === 0) {
480 return 'min';
481 }
482 return 'sum';
483 }
484 private function getIdSitesToComputeNbUniques()
485 {
486 $params = $this->getParams();
487 $sites = array($params->getSite()->getId());
488 /**
489 * Triggered to change which site ids should be looked at when processing unique visitors and users.
490 *
491 * @param array &$idSites An array with one idSite. This site is being archived currently. To cancel the query
492 * you can change this value to an empty array. To include other sites in the query you
493 * can add more idSites to this list of idSites.
494 * @param Period $period The period that is being requested to be archived.
495 * @param Segment $segment The segment that is request to be archived.
496 */
497 \Piwik\Piwik::postEvent('ArchiveProcessor.ComputeNbUniques.getIdSites', array(&$sites, $params->getPeriod(), $params->getSegment()));
498 return $sites;
499 }
500 /**
501 * Processes number of unique visitors for the given period
502 *
503 * This is the only Period metric (ie. week/month/year/range) that we process from the logs directly,
504 * since unique visitors cannot be summed like other metrics.
505 *
506 * @param array $metrics Metrics Ids for which to aggregates count of values
507 * @param int[] $sites A list of idSites that should be included
508 * @return array|null An array of metrics, where the key is metricid and the value is the metric value or null if
509 * the query was cancelled and not executed.
510 */
511 protected function computeNbUniques($metrics, $sites)
512 {
513 $logAggregator = $this->getLogAggregator();
514 $sitesBackup = $logAggregator->getSites();
515 $logAggregator->setSites($sites);
516 try {
517 $query = $logAggregator->queryVisitsByDimension(array(), \false, array(), $metrics);
518 } finally {
519 $logAggregator->setSites($sitesBackup);
520 }
521 $data = $query->fetch();
522 return $data;
523 }
524 /**
525 * If the DataTable is a Map, sums all DataTable in the map and return the DataTable.
526 *
527 *
528 * @param $data DataTable|DataTable\Map
529 * @param $columnsToRenameAfterAggregation array
530 * @return DataTable
531 */
532 protected function getAggregatedDataTableMap($data, $columnsAggregationOperation)
533 {
534 $table = new \Piwik\DataTable();
535 if (!empty($columnsAggregationOperation)) {
536 $table->setMetadata(\Piwik\DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnsAggregationOperation);
537 }
538 if ($data instanceof \Piwik\DataTable\Map) {
539 // as $date => $tableToSum
540 $this->aggregatedDataTableMapsAsOne($data, $table);
541 } else {
542 $table->addDataTable($data);
543 }
544 return $table;
545 }
546 /**
547 * Aggregates the DataTable\Map into the destination $aggregated
548 * @param $map
549 * @param $aggregated
550 */
551 protected function aggregatedDataTableMapsAsOne(Map $map, \Piwik\DataTable $aggregated)
552 {
553 foreach ($map->getDataTables() as $tableToAggregate) {
554 if ($tableToAggregate instanceof Map) {
555 $this->aggregatedDataTableMapsAsOne($tableToAggregate, $aggregated);
556 } else {
557 $aggregated->addDataTable($tableToAggregate);
558 }
559 }
560 }
561 /**
562 * Note: public only for use in closure in PHP 5.3.
563 */
564 public function renameColumnsAfterAggregation(\Piwik\DataTable $table, $columnsToRenameAfterAggregation = null)
565 {
566 // Rename columns after aggregation
567 if (is_null($columnsToRenameAfterAggregation)) {
568 $columnsToRenameAfterAggregation = self::$columnsToRenameAfterAggregation;
569 }
570 if (empty($columnsToRenameAfterAggregation)) {
571 return;
572 }
573 foreach ($table->getRows() as $row) {
574 foreach ($columnsToRenameAfterAggregation as $oldName => $newName) {
575 $row->renameColumn($oldName, $newName);
576 }
577 $subTable = $row->getSubtable();
578 if ($subTable) {
579 $this->renameColumnsAfterAggregation($subTable, $columnsToRenameAfterAggregation);
580 }
581 }
582 }
583 protected function getAggregatedNumericMetrics($columns, $operationsToApply)
584 {
585 if (!is_array($columns)) {
586 $columns = array($columns);
587 }
588 $operationForColumn = $this->getOperationForColumns($columns, $operationsToApply);
589 $dataTable = $this->getArchive()->getDataTableFromNumeric($columns);
590 $results = $this->getAggregatedDataTableMap($dataTable, $operationForColumn);
591 if ($results->getRowsCount() > 1) {
592 throw new Exception("A DataTable is an unexpected state:" . var_export($results, \true));
593 }
594 $rowMetrics = $results->getFirstRow();
595 if ($rowMetrics === \false) {
596 $rowMetrics = new Row();
597 }
598 $this->enrichWithUniqueVisitorsMetric($rowMetrics);
599 $this->renameColumnsAfterAggregation($results, self::$columnsToRenameAfterAggregation);
600 $metrics = $rowMetrics->getColumns();
601 foreach ($columns as $name) {
602 if (!isset($metrics[$name])) {
603 $metrics[$name] = 0;
604 }
605 }
606 return $metrics;
607 }
608 /**
609 * Initiate archiving for a plugin during an ongoing archiving. The plugin can be another
610 * plugin or the same plugin.
611 *
612 * This method should be called during archiving when one plugin uses the report of another
613 * plugin with a segment. It will ensure reports for that segment & plugin will be archived
614 * without initiating archiving for every plugin with that segment (which would be a performance
615 * killer).
616 *
617 * @param string $plugin
618 * @param string $segment
619 */
620 public function processDependentArchive($plugin, $segment)
621 {
622 if (!self::$isRootArchivingRequest) {
623 // prevent all recursion
624 return;
625 }
626 $params = $this->getParams();
627 // range archives are always processed on demand, so pre-processing dependent archives is not required
628 // here
629 if (Rules::shouldProcessOnlyReportsRequestedInArchiveQuery($params->getPeriod()->getLabel())) {
630 return;
631 }
632 $idSites = [$params->getSite()->getId()];
633 // important to use the original segment string when combining. As the API itself would combine the original string.
634 // this prevents a bug where the API would use the segment
635 // userId!@%2540matomo.org;userId!=hello%2540matomo.org;visitorType==new
636 // vs here we would use
637 // userId!@%40matomo.org;userId!=hello%40matomo.org;visitorType==new
638 // thus these would result in different segment hashes and therefore the reports would either show 0 or archive the data twice
639 $originSegmentString = $params->getSegment()->getOriginalString();
640 $newSegment = \Piwik\Segment::combine($originSegmentString, SegmentExpression::AND_DELIMITER, $segment);
641 if (!empty($originSegmentString) && $newSegment === $segment && $params->getRequestedPlugin() === $plugin) {
642 // being processed now
643 return;
644 }
645 $newSegment = new \Piwik\Segment($newSegment, $idSites, $params->getDateTimeStart(), $params->getDateTimeEnd());
646 if (\Piwik\ArchiveProcessor\Rules::isSegmentPreProcessed($idSites, $newSegment)) {
647 // will be processed anyway
648 return;
649 }
650 // The below check is meant to avoid archiving the same dependency multiple times.
651 $processedSegmentKey = $params->getSite()->getId() . $params->getPeriod()->getDateStart() . $params->getPeriod()->getLabel() . $newSegment->getOriginalString();
652 if (in_array($processedSegmentKey . $plugin, $this->processedDependentSegments)) {
653 return;
654 }
655 self::$isRootArchivingRequest = \false;
656 try {
657 $invalidator = StaticContainer::get('Piwik\\Archive\\ArchiveInvalidator');
658 // Ensure to always invalidate VisitsSummary before any other plugin archive.
659 // Otherwise those archives might get build with outdated VisitsSummary data
660 if ($plugin !== 'VisitsSummary' && !in_array($processedSegmentKey . 'VisitsSummary', $this->processedDependentSegments)) {
661 $invalidator->markArchivesAsInvalidated($idSites, [$params->getPeriod()->getDateStart()], $params->getPeriod()->getLabel(), $newSegment, \false, \false, 'VisitsSummary', \false, \true);
662 $parameters = new \Piwik\ArchiveProcessor\Parameters($params->getSite(), $params->getPeriod(), $newSegment);
663 $parameters->onlyArchiveRequestedPlugin();
664 $archiveLoader = new \Piwik\ArchiveProcessor\Loader($parameters);
665 $archiveLoader->prepareArchive('VisitsSummary');
666 $this->processedDependentSegments[] = $processedSegmentKey . 'VisitsSummary';
667 }
668 $invalidator->markArchivesAsInvalidated($idSites, [$params->getPeriod()->getDateStart()], $params->getPeriod()->getLabel(), $newSegment, \false, \false, $plugin, \false, \true);
669 $parameters = new \Piwik\ArchiveProcessor\Parameters($params->getSite(), $params->getPeriod(), $newSegment);
670 $parameters->onlyArchiveRequestedPlugin();
671 $archiveLoader = new \Piwik\ArchiveProcessor\Loader($parameters);
672 $archiveLoader->prepareArchive($plugin);
673 $this->processedDependentSegments[] = $processedSegmentKey . $plugin;
674 } finally {
675 self::$isRootArchivingRequest = \true;
676 }
677 }
678 public function getArchiveWriter()
679 {
680 return $this->archiveWriter;
681 }
682 }
683