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