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\CustomDimensions\Dao;
10
11use Piwik\Common;
12use Piwik\DataAccess\TableMetadata;
13use Piwik\DataTable;
14use Piwik\Db;
15use Piwik\DbHelper;
16use Piwik\Plugins\CustomDimensions\CustomDimensions;
17use Exception;
18
19class LogTable
20{
21    const DEFAULT_CUSTOM_DIMENSION_COUNT = 5;
22
23    private $scope = null;
24    private $table = null;
25
26    public function __construct($scope)
27    {
28        $this->scope = $scope;
29        $this->table = Common::prefixTable($this->getTableNameFromScope($scope));
30    }
31
32    private function getTableNameFromScope($scope)
33    {
34        // actually we should have a class for each scope but don't want to overengineer it for now
35        switch ($scope) {
36            case CustomDimensions::SCOPE_ACTION:
37                return 'log_link_visit_action';
38            case CustomDimensions::SCOPE_VISIT:
39                return 'log_visit';
40            case CustomDimensions::SCOPE_CONVERSION:
41                return 'log_conversion';
42            default:
43                throw new Exception('Unsupported scope ' . $scope);
44        }
45    }
46
47    /**
48     * @see getHighestCustomDimensionIndex()
49     * @return int
50     */
51    public function getNumInstalledIndexes()
52    {
53        $indexes = $this->getInstalledIndexes();
54
55        return count($indexes);
56    }
57
58    public function getInstalledIndexes()
59    {
60        $columns = $this->getCustomDimensionColumnNames();
61
62        if (empty($columns)) {
63            return array();
64        }
65
66        $indexes = array_map(function ($column) {
67            $onlyNumber = str_replace('custom_dimension_', '', $column);
68
69            if (is_numeric($onlyNumber)) {
70                return (int) $onlyNumber;
71            }
72        }, $columns);
73
74        return array_values(array_unique($indexes));
75    }
76
77    private function getCustomDimensionColumnNames()
78    {
79        $tableMetadataAccess = new TableMetadata();
80        $columns = $tableMetadataAccess->getColumns($this->table);
81
82        $dimensionColumns = array_filter($columns, function ($column) {
83            return LogTable::isCustomDimensionColumn($column);
84        });
85
86        return $dimensionColumns;
87    }
88
89    public static function isCustomDimensionColumn($column)
90    {
91        return (bool) preg_match('/^custom_dimension_(\d+)$/', '' . $column);
92    }
93
94    public static function buildCustomDimensionColumnName($indexOrDimension)
95    {
96        if (is_array($indexOrDimension) && isset($indexOrDimension['index'])) {
97            $indexOrDimension = $indexOrDimension['index'];
98        }
99
100        $indexOrDimension = (int) $indexOrDimension;
101
102        if ($indexOrDimension >= 1) {
103            return 'custom_dimension_' . (int) $indexOrDimension;
104        }
105    }
106
107    public function removeCustomDimension($index)
108    {
109        if ($index < 1) {
110            return;
111        }
112
113        $field = self::buildCustomDimensionColumnName($index);
114
115        $this->dropColumn($field);
116    }
117
118    public function addManyCustomDimensions($count, $extraAlter = null)
119    {
120        if ($count < 0) {
121            return;
122        }
123
124        $indexes = $this->getInstalledIndexes();
125
126        if (empty($indexes)) {
127            $highestIndex = 0;
128        } else {
129            $highestIndex = max($indexes);
130        }
131
132        $total = $highestIndex + $count;
133
134        $queries = array();
135
136        if (isset($extraAlter)) {
137            // we make sure to install needed tracker request processor columns first, before installing custom dimensions
138            // if something fails custom dimensions can be added later any time
139            $queries[] = $extraAlter;
140        }
141
142        for ($index = $highestIndex; $index < $total; $index++) {
143            $queries[] = $this->getAddColumnQueryToAddCustomDimension($index + 1);
144        }
145
146        if (!empty($queries)) {
147            $sql = 'ALTER TABLE ' . $this->table . ' ' . implode(', ', $queries) . ';';
148            Db::exec($sql);
149        }
150    }
151
152    private function getAddColumnQueryToAddCustomDimension($index)
153    {
154        $field = self::buildCustomDimensionColumnName($index);
155
156        return sprintf('ADD COLUMN %s VARCHAR(255) DEFAULT NULL', $field);
157    }
158
159    public function install()
160    {
161        $numDimensionsInstalled = $this->getNumInstalledIndexes();
162        $numDimensionsToAdd = self::DEFAULT_CUSTOM_DIMENSION_COUNT - $numDimensionsInstalled;
163
164        $query = null;
165        if ($this->scope === CustomDimensions::SCOPE_VISIT && !$this->hasColumn('last_idlink_va')) {
166            $query = 'ADD COLUMN last_idlink_va BIGINT UNSIGNED DEFAULT NULL';
167        } elseif ($this->scope === CustomDimensions::SCOPE_ACTION && !$this->hasColumn('time_spent')) {
168            $query = 'ADD COLUMN time_spent INT UNSIGNED DEFAULT NULL';
169        }
170
171        $this->addManyCustomDimensions($numDimensionsToAdd, $query);
172    }
173
174    public function uninstall()
175    {
176        foreach ($this->getInstalledIndexes() as $index) {
177            $this->removeCustomDimension($index);
178        }
179
180        if ($this->scope === CustomDimensions::SCOPE_VISIT) {
181            $this->dropColumn('last_idlink_va');
182        } elseif ($this->scope === CustomDimensions::SCOPE_ACTION) {
183            $this->dropColumn('time_spent');
184        }
185    }
186
187    private function hasColumn($field)
188    {
189        $columns = DbHelper::getTableColumns($this->table);
190        return array_key_exists($field, $columns);
191    }
192
193    private function dropColumn($field)
194    {
195        if ($this->hasColumn($field)) {
196            $sql = sprintf('ALTER TABLE %s DROP COLUMN %s;', $this->table, $field);
197            Db::exec($sql);
198        }
199    }
200
201}
202
203