Welcome to mirror list, hosted at ThFree Co, Russian Federation.

VisitDimension.php « Dimension « Plugin « core - github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 030ec18f9fe6e5ab42dd6192bcb5b041fbd8901c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
<?php
/**
 * Matomo - free/libre analytics platform
 *
 * @link https://matomo.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 *
 */
namespace Piwik\Plugin\Dimension;

use Piwik\CacheId;
use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Db;
use Piwik\DbHelper;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
use Piwik\Tracker\Action;
use Piwik\Plugin;
use Exception;

/**
 * Defines a new visit dimension that records any visit related information during tracking.
 *
 * You can record any visit information by implementing one of the following events: {@link onNewVisit()},
 * {@link onExistingVisit()}, {@link onConvertedVisit()} or {@link onAnyGoalConversion()}. By defining a
 * {@link $columnName} and {@link $columnType} a new column will be created in the database (table `log_visit`)
 * automatically and the values you return in the previous mentioned events will be saved in this column.
 *
 * You can create a new dimension using the console command `./console generate:dimension`.
 *
 * @api
 * @since 2.5.0
 */
abstract class VisitDimension extends Dimension
{
    const INSTALLER_PREFIX = 'log_visit.';

    protected $dbTableName = 'log_visit';
    protected $category = 'General_Visitors';

    public function install()
    {
        if (empty($this->columnType) || empty($this->columnName)) {
            return array();
        }

        $changes = array(
            $this->dbTableName => array("ADD COLUMN `$this->columnName` $this->columnType")
        );

        if ($this->isHandlingLogConversion()) {
            $changes['log_conversion'] = array("ADD COLUMN `$this->columnName` $this->columnType");
        }

        return $changes;
    }

    /**
     * @see ActionDimension::update()
     * @return array
     * @ignore
     */
    public function update()
    {
        if (!$this->columnType) {
            return array();
        }

        $conversionColumns = DbHelper::getTableColumns(Common::prefixTable('log_conversion'));

        $changes = array();

        $changes[$this->dbTableName] = array("MODIFY COLUMN `$this->columnName` $this->columnType");

        $handlingConversion  = $this->isHandlingLogConversion();
        $hasConversionColumn = array_key_exists($this->columnName, $conversionColumns);

        if ($hasConversionColumn && $handlingConversion) {
            $changes['log_conversion'] = array("MODIFY COLUMN `$this->columnName` $this->columnType");
        } elseif (!$hasConversionColumn && $handlingConversion) {
            $changes['log_conversion'] = array("ADD COLUMN `$this->columnName` $this->columnType");
        } elseif ($hasConversionColumn && !$handlingConversion) {
            $changes['log_conversion'] = array("DROP COLUMN `$this->columnName`");
        }

        return $changes;
    }

    /**
     * @return string
     * @ignore
     */
    public function getVersion()
    {
        return $this->columnType . $this->isHandlingLogConversion();
    }

    private function isHandlingLogConversion()
    {
        if (empty($this->columnName) || empty($this->columnType)) {
            return false;
        }

        return $this->hasImplementedEvent('onAnyGoalConversion');
    }

    /**
     * Uninstalls the dimension if a {@link $columnName} and {@link columnType} is set. In case you perform any custom
     * actions during {@link install()} - for instance adding an index - you should make sure to undo those actions by
     * overwriting this method. Make sure to call this parent method to make sure the uninstallation of the column
     * will be done.
     * @throws Exception
     * @api
     */
    public function uninstall()
    {
        if (empty($this->columnName) || empty($this->columnType)) {
            return;
        }

        try {
            $sql = "ALTER TABLE `" . Common::prefixTable($this->dbTableName) . "` DROP COLUMN `$this->columnName`";
            Db::exec($sql);
        } catch (Exception $e) {
            if (!Db::get()->isErrNo($e, '1091')) {
                throw $e;
            }
        }

        try {
            if (!$this->isHandlingLogConversion()) {
                return;
            }

            $sql = "ALTER TABLE `" . Common::prefixTable('log_conversion') . "` DROP COLUMN `$this->columnName`";
            Db::exec($sql);
        } catch (Exception $e) {
            if (!Db::get()->isErrNo($e, '1091')) {
                throw $e;
            }
        }
    }

    /**
     * Sometimes you may want to make sure another dimension is executed before your dimension so you can persist
     * this dimensions' value depending on the value of other dimensions. You can do this by defining an array of
     * dimension names. If you access any value of any other column within your events, you should require them here.
     * Otherwise those values may not be available.
     * @return array
     * @api
     */
    public function getRequiredVisitFields()
    {
        return array();
    }

    /**
     * The `onNewVisit` method is triggered when a new visitor is detected. This means you can define an initial
     * value for this user here. By returning boolean `false` no value will be saved. Once the user makes another action
     * the event "onExistingVisit" is executed. Meaning for each visitor this method is executed once.
     *
     * @param Request $request
     * @param Visitor $visitor
     * @param Action|null $action
     * @return mixed|false
     * @api
     */
    public function onNewVisit(Request $request, Visitor $visitor, $action)
    {
        return false;
    }

    /**
     * The `onExistingVisit` method is triggered when a visitor was recognized meaning it is not a new visitor.
     * You can overwrite any previous value set by the event `onNewVisit` by implemting this event. By returning boolean
     * `false` no value will be updated.
     *
     * @param Request $request
     * @param Visitor $visitor
     * @param Action|null $action
     * @return mixed|false
     * @api
     */
    public function onExistingVisit(Request $request, Visitor $visitor, $action)
    {
        return false;
    }

    /**
     * This event is executed shortly after `onNewVisit` or `onExistingVisit` in case the visitor converted a goal.
     * Usually this event is not needed and you can simply remove this method therefore. An example would be for
     * instance to persist the last converted action url. Return boolean `false` if you do not want to change the
     * current value.
     *
     * @param Request $request
     * @param Visitor $visitor
     * @param Action|null $action
     * @return mixed|false
     * @api
     */
    public function onConvertedVisit(Request $request, Visitor $visitor, $action)
    {
        return false;
    }

    /**
     * By implementing this event you can persist a value to the `log_conversion` table in case a conversion happens.
     * The persisted value will be logged along the conversion and will not be changed afterwards. This allows you to
     * generate reports that shows for instance which url was called how often for a specific conversion. Once you
     * implement this event and a $columnType is defined a column in the `log_conversion` MySQL table will be
     * created automatically.
     *
     * @param Request $request
     * @param Visitor $visitor
     * @param Action|null $action
     * @return mixed|false
     * @api
     */
    public function onAnyGoalConversion(Request $request, Visitor $visitor, $action)
    {
        return false;
    }

    /**
     * This hook is executed by the tracker when determining if an action is the start of a new visit
     * or part of an existing one. Derived classes can use it to force new visits based on dimension
     * data.
     *
     * For example, the Campaign dimension in the Referrers plugin will force a new visit if the
     * campaign information for the current action is different from the last.
     *
     * @param Request $request The current tracker request information.
     * @param Visitor $visitor The information for the currently recognized visitor.
     * @param Action|null $action The current action information (if any).
     * @return bool Return true to force a visit, false if otherwise.
     * @api
     */
    public function shouldForceNewVisit(Request $request, Visitor $visitor, Action $action = null)
    {
        return false;
    }

    /**
     * Get all visit dimensions that are defined by all activated plugins.
     * @return VisitDimension[]
     */
    public static function getAllDimensions()
    {
        $cacheId = CacheId::pluginAware('VisitDimensions');
        $cache   = PiwikCache::getTransientCache();

        if (!$cache->contains($cacheId)) {
            $plugins   = PluginManager::getInstance()->getPluginsLoadedAndActivated();
            $instances = array();

            foreach ($plugins as $plugin) {
                foreach (self::getDimensions($plugin) as $instance) {
                    $instances[] = $instance;
                }
            }

            $instances = self::sortDimensions($instances);

            $cache->save($cacheId, $instances);
        }

        return $cache->fetch($cacheId);
    }

    /**
     * @ignore
     * @param VisitDimension[] $dimensions
     */
    public static function sortDimensions($dimensions)
    {
        $sorted = array();
        $exists = array();

        // we first handle all the once without dependency
        foreach ($dimensions as $index => $dimension) {
            $fields = $dimension->getRequiredVisitFields();
            if (empty($fields)) {
                $sorted[] = $dimension;
                $exists[] = $dimension->getColumnName();
                unset($dimensions[$index]);
            }
        }

        // find circular references
        // and remove dependencies whose column cannot be resolved because it is not installed / does not exist / is defined by core
        $dependencies = array();
        foreach ($dimensions as $dimension) {
            $dependencies[$dimension->getColumnName()] = $dimension->getRequiredVisitFields();
        }

        foreach ($dependencies as $column => $fields) {
            foreach ($fields as $key => $field) {
                if (empty($dependencies[$field]) && !in_array($field, $exists)) {
                    // we cannot resolve that dependency as it does not exist
                    unset($dependencies[$column][$key]);
                } elseif (!empty($dependencies[$field]) && in_array($column, $dependencies[$field])) {
                    throw new Exception("Circular reference detected for required field $field in dimension $column");
                }
            }
        }

        $count = 0;
        while (count($dimensions) > 0) {
            $count++;
            if ($count > 1000) {
                foreach ($dimensions as $dimension) {
                    $sorted[] = $dimension;
                }
                break; // to prevent an endless loop
            }
            foreach ($dimensions as $key => $dimension) {
                $fields = $dependencies[$dimension->getColumnName()];
                if (count(array_intersect($fields, $exists)) === count($fields)) {
                    $sorted[] = $dimension;
                    $exists[] = $dimension->getColumnName();
                    unset($dimensions[$key]);
                }
            }
        }

        return $sorted;
    }

    /**
     * Get all visit dimensions that are defined by the given plugin.
     * @param Plugin $plugin
     * @return VisitDimension[]
     * @ignore
     */
    public static function getDimensions(Plugin $plugin)
    {
        $dimensions = $plugin->findMultipleComponents('Columns', '\\Piwik\\Plugin\\Dimension\\VisitDimension');
        $instances  = array();

        foreach ($dimensions as $dimension) {
            $instances[] = new $dimension();
        }

        return $instances;
    }

    /**
     * Sort a key => value array descending by the number of occurances of the key in the supplied table and column
     *
     * @param array     $array              Key value array
     * @param DataTable $table              Datatable from which to count occurances
     * @param string    $keyColumn          Column in the datatable to match against the array key
     * @param int       $maxValuesToReturn  Limit the return array to this number of elements
     *
     * @return array    An array of values from the source array sorted by most occurances, descending
     */
    public function sortStaticListByUsage(array $array, DataTable $table, string $keyColumn, int $maxValuesToReturn) : array
    {
        // Convert to multi-dimensional array and count the number of visits for each browser name
        foreach ($array as $k => $v) {
            $array[$k] = ['count' => 0, 'name' => $v];
        }
        $array['xx'] = ['count' => 0, 'name' => 'Unknown'];

        foreach ($table->getRows() as $row) {
            if (isset($row[$keyColumn])) {
                if (isset($array[$row[$keyColumn]])) {
                    $array[$row[$keyColumn]]['count']++;
                } else {
                    $array['xx']['count']++;
                }
            }
        }
        // Sort by most visits descending
        uasort($array, function($a, $b) {
            return $a <=> $b;
        });
        $array = array_reverse($array, true);

        // Flatten and limit the return array
        $flat = [];
        $i = 0;
        foreach ($array as $k => $v) {
            $flat[$k] = $v['name'];
            $i++;
            if ($i == ($maxValuesToReturn)) {
                break;
            }
        }

        return array_values($flat);
    }
}