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
9namespace Piwik\Plugins\UserId;
10use Piwik\Config;
11use Piwik\DataArray;
12use Piwik\DataTable;
13use Piwik\Metrics as PiwikMetrics;
14use Piwik\RankingQuery;
15
16/**
17 * Archiver that aggregates metrics per user ID (user_id field).
18 */
19class Archiver extends \Piwik\Plugin\Archiver
20{
21    const USERID_ARCHIVE_RECORD = "UserId_users";
22
23    const VISITOR_ID_FIELD = 'idvisitor';
24    const USER_ID_FIELD = 'user_id';
25
26    protected $maximumRowsInDataTableLevelZero;
27
28    function __construct($processor)
29    {
30        parent::__construct($processor);
31
32        $this->maximumRowsInDataTableLevelZero = Config::getInstance()->General['datatable_archiving_maximum_rows_userid_users'];
33    }
34
35    /**
36     * @var DataArray
37     */
38    protected $arrays;
39
40    /**
41     * Array to save visitor IDs for every user ID met during archiving process. We use it to
42     * fill metadata before actual inserting rows to DB.
43     * @var array
44     */
45    protected $visitorIdsUserIdsMap = array();
46
47    /**
48     * Archives data for a day period.
49     */
50    public function aggregateDayReport()
51    {
52        $this->arrays = new DataArray();
53        $this->aggregateUsers();
54        $this->insertDayReports();
55    }
56    /**
57     * Period archiving: simply sums up daily archives
58     */
59    public function aggregateMultipleReports()
60    {
61        $dataTableRecords = array(self::USERID_ARCHIVE_RECORD);
62        $columnsAggregationOperation = null;
63        $this->getProcessor()->aggregateDataTableRecords(
64            $dataTableRecords,
65            $this->maximumRowsInDataTableLevelZero,
66            $this->maximumRowsInDataTableLevelZero,
67            $columnToSort = 'nb_visits',
68            $columnsAggregationOperation,
69            $columnsToRenameAfterAggregation = null,
70            $countRowsRecursive = array());
71    }
72
73    /**
74     * Used to aggregate daily data per user ID
75     */
76    protected function aggregateUsers()
77    {
78        $userIdFieldName = self::USER_ID_FIELD;
79        $visitorIdFieldName = self::VISITOR_ID_FIELD;
80
81        $rankingQueryLimit = $this->getRankingQueryLimit();
82
83        $rankingQuery = false;
84        if ($rankingQueryLimit > 0) {
85            $rankingQuery = new RankingQuery($rankingQueryLimit);
86            $rankingQuery->addLabelColumn($userIdFieldName);
87            $rankingQuery->addLabelColumn($visitorIdFieldName);
88        }
89
90        /** @var \Zend_Db_Statement $query */
91        $query = $this->getLogAggregator()->queryVisitsByDimension(
92            array(self::USER_ID_FIELD),
93            "log_visit.$userIdFieldName IS NOT NULL AND log_visit.$userIdFieldName != ''",
94            array("LOWER(HEX($visitorIdFieldName)) as $visitorIdFieldName"),
95            $metrics = false,
96            $rankingQuery,
97            self::USER_ID_FIELD . ' ASC'
98        );
99
100        if ($query === false) {
101            return;
102        }
103
104        $rowsCount = 0;
105        foreach ($query as $row) {
106            $rowsCount++;
107            $this->arrays->sumMetricsVisits($row[$userIdFieldName], $row);
108            $this->rememberVisitorId($row);
109        }
110    }
111
112    /**
113     * Insert aggregated daily data serialized
114     *
115     * @throws \Exception
116     */
117    protected function insertDayReports()
118    {
119        /** @var DataTable $dataTable */
120        $dataTable = $this->arrays->asDataTable();
121        $this->setVisitorIds($dataTable);
122        $report = $dataTable->getSerialized($this->maximumRowsInDataTableLevelZero, null, PiwikMetrics::INDEX_NB_VISITS);
123        $this->getProcessor()->insertBlobRecord(self::USERID_ARCHIVE_RECORD, $report);
124    }
125
126    /**
127     * Remember visitor ID per user. We use it to fill metadata before actual inserting rows to DB.
128     *
129     * @param array $row
130     */
131    private function rememberVisitorId($row)
132    {
133        if (!empty($row[self::USER_ID_FIELD]) && !empty($row[self::VISITOR_ID_FIELD])) {
134            $this->visitorIdsUserIdsMap[$row[self::USER_ID_FIELD]] = $row[self::VISITOR_ID_FIELD];
135        }
136    }
137
138    /**
139     * Fill visitor ID as metadata before actual inserting rows to DB.
140     *
141     * @param DataTable $dataTable
142     */
143    private function setVisitorIds(DataTable $dataTable)
144    {
145        foreach ($dataTable->getRows() as $row) {
146            $userId = $row->getColumn('label');
147            if (isset($this->visitorIdsUserIdsMap[$userId])) {
148                $row->setMetadata(self::VISITOR_ID_FIELD, $this->visitorIdsUserIdsMap[$userId]);
149            }
150        }
151    }
152
153    private function getRankingQueryLimit()
154    {
155        $configGeneral = Config::getInstance()->General;
156        $configLimit = $configGeneral['archiving_ranking_query_row_limit'];
157        $limit = $configLimit == 0 ? 0 : max(
158            $configLimit,
159            $this->maximumRowsInDataTableLevelZero
160        );
161        return $limit;
162    }
163
164}