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