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
|
<?php
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://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
{
public 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 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 implementing this event. By returning boolean
* `false` no value will be updated.
*
* @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 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 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.
* @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 occurrences of the key in the supplied table and column
*
* @param array $array Key value array
* @param DataTable $table Datatable from which to count occurrences
* @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 occurrences, 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);
}
}
|