381 lines
15 KiB
PHP
381 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* Piwik - free/libre analytics platform
|
|
*
|
|
* @link http://piwik.org
|
|
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
|
*
|
|
*/
|
|
namespace Piwik\Columns;
|
|
|
|
use Piwik\Common;
|
|
use Piwik\DbHelper;
|
|
use Piwik\Plugin\Dimension\ActionDimension;
|
|
use Piwik\Plugin\Dimension\VisitDimension;
|
|
use Piwik\Plugin\Dimension\ConversionDimension;
|
|
use Piwik\Db;
|
|
use Piwik\Plugin\Manager;
|
|
use Piwik\Updater as PiwikUpdater;
|
|
use Piwik\Filesystem;
|
|
use Piwik\Cache as PiwikCache;
|
|
use Piwik\Updater\Migration;
|
|
|
|
/**
|
|
* Class that handles dimension updates
|
|
*/
|
|
class Updater extends \Piwik\Updates
|
|
{
|
|
private static $cacheId = 'AllDimensionModifyTime';
|
|
|
|
/**
|
|
* @var VisitDimension[]
|
|
*/
|
|
public $visitDimensions;
|
|
|
|
/**
|
|
* @var ActionDimension[]
|
|
*/
|
|
private $actionDimensions;
|
|
|
|
/**
|
|
* @var ConversionDimension[]
|
|
*/
|
|
private $conversionDimensions;
|
|
|
|
/**
|
|
* @param VisitDimension[]|null $visitDimensions
|
|
* @param ActionDimension[]|null $actionDimensions
|
|
* @param ConversionDimension[]|null $conversionDimensions
|
|
*/
|
|
public function __construct(array $visitDimensions = null, array $actionDimensions = null, array $conversionDimensions = null)
|
|
{
|
|
$this->visitDimensions = $visitDimensions;
|
|
$this->actionDimensions = $actionDimensions;
|
|
$this->conversionDimensions = $conversionDimensions;
|
|
}
|
|
|
|
/**
|
|
* @param PiwikUpdater $updater
|
|
* @return Migration\Db[]
|
|
*/
|
|
public function getMigrationQueries(PiwikUpdater $updater)
|
|
{
|
|
$sqls = array();
|
|
|
|
$changingColumns = $this->getUpdates($updater);
|
|
$errorCodes = array(
|
|
Migration\Db\Sql::ERROR_CODE_COLUMN_NOT_EXISTS,
|
|
Migration\Db\Sql::ERROR_CODE_DUPLICATE_COLUMN
|
|
);
|
|
|
|
foreach ($changingColumns as $table => $columns) {
|
|
if (empty($columns) || !is_array($columns)) {
|
|
continue;
|
|
}
|
|
|
|
$sql = "ALTER TABLE `" . Common::prefixTable($table) . "` " . implode(', ', $columns);
|
|
$sqls[] = new Migration\Db\Sql($sql, $errorCodes);
|
|
}
|
|
|
|
return $sqls;
|
|
}
|
|
|
|
public function doUpdate(PiwikUpdater $updater)
|
|
{
|
|
$updater->executeMigrationQueries(__FILE__, $this->getMigrationQueries($updater));
|
|
}
|
|
|
|
private function getVisitDimensions()
|
|
{
|
|
// see eg https://github.com/piwik/piwik/issues/8399 we fetch them only on demand to improve performance
|
|
if (!isset($this->visitDimensions)) {
|
|
$this->visitDimensions = VisitDimension::getAllDimensions();
|
|
}
|
|
|
|
return $this->visitDimensions;
|
|
}
|
|
|
|
private function getActionDimensions()
|
|
{
|
|
// see eg https://github.com/piwik/piwik/issues/8399 we fetch them only on demand to improve performance
|
|
if (!isset($this->actionDimensions)) {
|
|
$this->actionDimensions = ActionDimension::getAllDimensions();
|
|
}
|
|
|
|
return $this->actionDimensions;
|
|
}
|
|
|
|
private function getConversionDimensions()
|
|
{
|
|
// see eg https://github.com/piwik/piwik/issues/8399 we fetch them only on demand to improve performance
|
|
if (!isset($this->conversionDimensions)) {
|
|
$this->conversionDimensions = ConversionDimension::getAllDimensions();
|
|
}
|
|
|
|
return $this->conversionDimensions;
|
|
}
|
|
|
|
private function getUpdates(PiwikUpdater $updater)
|
|
{
|
|
$visitColumns = DbHelper::getTableColumns(Common::prefixTable('log_visit'));
|
|
$actionColumns = DbHelper::getTableColumns(Common::prefixTable('log_link_visit_action'));
|
|
$conversionColumns = DbHelper::getTableColumns(Common::prefixTable('log_conversion'));
|
|
|
|
$allUpdatesToRun = array();
|
|
|
|
foreach ($this->getVisitDimensions() as $dimension) {
|
|
$updates = $this->getUpdatesForDimension($updater, $dimension, 'log_visit.', $visitColumns);
|
|
$allUpdatesToRun = $this->mixinUpdates($allUpdatesToRun, $updates);
|
|
}
|
|
|
|
foreach ($this->getActionDimensions() as $dimension) {
|
|
$updates = $this->getUpdatesForDimension($updater, $dimension, 'log_link_visit_action.', $actionColumns);
|
|
$allUpdatesToRun = $this->mixinUpdates($allUpdatesToRun, $updates);
|
|
}
|
|
|
|
foreach ($this->getConversionDimensions() as $dimension) {
|
|
$updates = $this->getUpdatesForDimension($updater, $dimension, 'log_conversion.', $conversionColumns);
|
|
$allUpdatesToRun = $this->mixinUpdates($allUpdatesToRun, $updates);
|
|
}
|
|
|
|
return $allUpdatesToRun;
|
|
}
|
|
|
|
/**
|
|
* @param ActionDimension|ConversionDimension|VisitDimension $dimension
|
|
* @param string $componentPrefix
|
|
* @return array
|
|
*/
|
|
private function getUpdatesForDimension(PiwikUpdater $updater, $dimension, $componentPrefix, $existingColumnsInDb)
|
|
{
|
|
$column = $dimension->getColumnName();
|
|
$componentName = $componentPrefix . $column;
|
|
|
|
if (!$updater->hasNewVersion($componentName)) {
|
|
return array();
|
|
}
|
|
|
|
if (array_key_exists($column, $existingColumnsInDb)) {
|
|
$sqlUpdates = $dimension->update();
|
|
} else {
|
|
$sqlUpdates = $dimension->install();
|
|
}
|
|
|
|
return $sqlUpdates;
|
|
}
|
|
|
|
private function mixinUpdates($allUpdatesToRun, $updatesFromDimension)
|
|
{
|
|
if (!empty($updatesFromDimension)) {
|
|
foreach ($updatesFromDimension as $table => $col) {
|
|
if (empty($allUpdatesToRun[$table])) {
|
|
$allUpdatesToRun[$table] = $col;
|
|
} else {
|
|
$allUpdatesToRun[$table] = array_merge($allUpdatesToRun[$table], $col);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $allUpdatesToRun;
|
|
}
|
|
|
|
public function getAllVersions(PiwikUpdater $updater)
|
|
{
|
|
// to avoid having to load all dimensions on each request we check if there were any changes on the file system
|
|
// can easily save > 100ms for each request
|
|
$cachedTimes = self::getCachedDimensionFileChanges();
|
|
$currentTimes = self::getCurrentDimensionFileChanges();
|
|
$diff = array_diff_assoc($currentTimes, $cachedTimes);
|
|
|
|
if (empty($diff)) {
|
|
return array();
|
|
}
|
|
|
|
$versions = array();
|
|
|
|
$visitColumns = DbHelper::getTableColumns(Common::prefixTable('log_visit'));
|
|
$actionColumns = DbHelper::getTableColumns(Common::prefixTable('log_link_visit_action'));
|
|
$conversionColumns = DbHelper::getTableColumns(Common::prefixTable('log_conversion'));
|
|
|
|
foreach ($this->getVisitDimensions() as $dimension) {
|
|
$versions = $this->mixinVersions($updater, $dimension, VisitDimension::INSTALLER_PREFIX, $visitColumns, $versions);
|
|
}
|
|
|
|
foreach ($this->getActionDimensions() as $dimension) {
|
|
$versions = $this->mixinVersions($updater, $dimension, ActionDimension::INSTALLER_PREFIX, $actionColumns, $versions);
|
|
}
|
|
|
|
foreach ($this->getConversionDimensions() as $dimension) {
|
|
$versions = $this->mixinVersions($updater, $dimension, ConversionDimension::INSTALLER_PREFIX, $conversionColumns, $versions);
|
|
}
|
|
|
|
return $versions;
|
|
}
|
|
|
|
/**
|
|
* @param PiwikUpdater $updater
|
|
* @param Dimension $dimension
|
|
* @param string $componentPrefix
|
|
* @param array $columns
|
|
* @param array $versions
|
|
* @return array The modified versions array
|
|
*/
|
|
private function mixinVersions(PiwikUpdater $updater, $dimension, $componentPrefix, $columns, $versions)
|
|
{
|
|
$columnName = $dimension->getColumnName();
|
|
|
|
// dimensions w/o columns do not need DB updates
|
|
if (!$columnName || !$dimension->hasColumnType()) {
|
|
return $versions;
|
|
}
|
|
|
|
$component = $componentPrefix . $columnName;
|
|
$version = $dimension->getVersion();
|
|
|
|
// if the column exists in the table, but has no associated version, and was one of the core columns
|
|
// that was moved when the dimension refactor took place, then:
|
|
// - set the installed version in the DB to the current code version
|
|
// - and do not check for updates since we just set the version to the latest
|
|
if (array_key_exists($columnName, $columns)
|
|
&& false === $updater->getCurrentComponentVersion($component)
|
|
&& self::wasDimensionMovedFromCoreToPlugin($component, $version)
|
|
) {
|
|
$updater->markComponentSuccessfullyUpdated($component, $version);
|
|
return $versions;
|
|
}
|
|
|
|
$versions[$component] = $version;
|
|
|
|
return $versions;
|
|
}
|
|
|
|
public static function isDimensionComponent($name)
|
|
{
|
|
return 0 === strpos($name, 'log_visit.')
|
|
|| 0 === strpos($name, 'log_conversion.')
|
|
|| 0 === strpos($name, 'log_conversion_item.')
|
|
|| 0 === strpos($name, 'log_link_visit_action.');
|
|
}
|
|
|
|
public static function wasDimensionMovedFromCoreToPlugin($name, $version)
|
|
{
|
|
// maps names of core dimension columns that were part of the original dimension refactor with their
|
|
// initial "version" strings. The '1' that is sometimes appended to the end of the string (sometimes seen as
|
|
// NULL1) is from individual dimension "versioning" logic (eg, see VisitDimension::getVersion())
|
|
$initialCoreDimensionVersions = array(
|
|
'log_visit.config_resolution' => 'VARCHAR(9) NOT NULL',
|
|
'log_visit.config_device_brand' => 'VARCHAR( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL',
|
|
'log_visit.config_device_model' => 'VARCHAR( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL',
|
|
'log_visit.config_windowsmedia' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.config_silverlight' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.config_java' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.config_gears' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.config_pdf' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.config_quicktime' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.config_realplayer' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.config_device_type' => 'TINYINT( 100 ) NULL DEFAULT NULL',
|
|
'log_visit.visitor_localtime' => 'TIME NOT NULL',
|
|
'log_visit.location_region' => 'char(2) DEFAULT NULL1',
|
|
'log_visit.visitor_days_since_last' => 'SMALLINT(5) UNSIGNED NOT NULL',
|
|
'log_visit.location_longitude' => 'float(10, 6) DEFAULT NULL1',
|
|
'log_visit.visit_total_events' => 'SMALLINT(5) UNSIGNED NOT NULL',
|
|
'log_visit.config_os_version' => 'VARCHAR( 100 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL',
|
|
'log_visit.location_city' => 'varchar(255) DEFAULT NULL1',
|
|
'log_visit.location_country' => 'CHAR(3) NOT NULL1',
|
|
'log_visit.location_latitude' => 'float(10, 6) DEFAULT NULL1',
|
|
'log_visit.config_flash' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.config_director' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.visit_total_time' => 'SMALLINT(5) UNSIGNED NOT NULL',
|
|
'log_visit.visitor_count_visits' => 'SMALLINT(5) UNSIGNED NOT NULL1',
|
|
'log_visit.visit_entry_idaction_name' => 'INTEGER(11) UNSIGNED NOT NULL',
|
|
'log_visit.visit_entry_idaction_url' => 'INTEGER(11) UNSIGNED NOT NULL',
|
|
'log_visit.visitor_returning' => 'TINYINT(1) NOT NULL1',
|
|
'log_visit.visitor_days_since_order' => 'SMALLINT(5) UNSIGNED NOT NULL1',
|
|
'log_visit.visit_goal_buyer' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.visit_first_action_time' => 'DATETIME NOT NULL',
|
|
'log_visit.visit_goal_converted' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.visitor_days_since_first' => 'SMALLINT(5) UNSIGNED NOT NULL1',
|
|
'log_visit.visit_exit_idaction_name' => 'INTEGER(11) UNSIGNED NOT NULL',
|
|
'log_visit.visit_exit_idaction_url' => 'INTEGER(11) UNSIGNED NULL DEFAULT 0',
|
|
'log_visit.config_browser_version' => 'VARCHAR(20) NOT NULL',
|
|
'log_visit.config_browser_name' => 'VARCHAR(10) NOT NULL',
|
|
'log_visit.config_browser_engine' => 'VARCHAR(10) NOT NULL',
|
|
'log_visit.location_browser_lang' => 'VARCHAR(20) NOT NULL',
|
|
'log_visit.config_os' => 'CHAR(3) NOT NULL',
|
|
'log_visit.config_cookie' => 'TINYINT(1) NOT NULL',
|
|
'log_visit.referer_url' => 'TEXT NOT NULL',
|
|
'log_visit.visit_total_searches' => 'SMALLINT(5) UNSIGNED NOT NULL',
|
|
'log_visit.visit_total_actions' => 'SMALLINT(5) UNSIGNED NOT NULL',
|
|
'log_visit.referer_keyword' => 'VARCHAR(255) NULL1',
|
|
'log_visit.referer_name' => 'VARCHAR(70) NULL1',
|
|
'log_visit.referer_type' => 'TINYINT(1) UNSIGNED NULL1',
|
|
'log_visit.user_id' => 'VARCHAR(200) NULL',
|
|
'log_link_visit_action.idaction_name' => 'INTEGER(10) UNSIGNED',
|
|
'log_link_visit_action.idaction_url' => 'INTEGER(10) UNSIGNED DEFAULT NULL',
|
|
'log_link_visit_action.server_time' => 'DATETIME NOT NULL',
|
|
'log_link_visit_action.time_spent_ref_action' => 'INTEGER(10) UNSIGNED NOT NULL',
|
|
'log_link_visit_action.idaction_event_action' => 'INTEGER(10) UNSIGNED DEFAULT NULL',
|
|
'log_link_visit_action.idaction_event_category' => 'INTEGER(10) UNSIGNED DEFAULT NULL',
|
|
'log_conversion.revenue_discount' => 'float default NULL',
|
|
'log_conversion.revenue' => 'float default NULL',
|
|
'log_conversion.revenue_shipping' => 'float default NULL',
|
|
'log_conversion.revenue_subtotal' => 'float default NULL',
|
|
'log_conversion.revenue_tax' => 'float default NULL',
|
|
);
|
|
|
|
if (!array_key_exists($name, $initialCoreDimensionVersions)) {
|
|
return false;
|
|
}
|
|
|
|
return strtolower($initialCoreDimensionVersions[$name]) === strtolower($version);
|
|
}
|
|
|
|
public function onNoUpdateAvailable($versionsThatWereChecked)
|
|
{
|
|
if (!empty($versionsThatWereChecked)) {
|
|
// invalidate cache only if there were actually file changes before, otherwise we write the cache on each
|
|
// request. There were versions checked only if there was a file change but no update, meaning we can
|
|
// set the cache and declare this state as "no update available".
|
|
self::cacheCurrentDimensionFileChanges();
|
|
}
|
|
}
|
|
|
|
private static function getCurrentDimensionFileChanges()
|
|
{
|
|
$times = array();
|
|
foreach (Manager::getPluginsDirectories() as $pluginsDir) {
|
|
$files = Filesystem::globr($pluginsDir . '*/Columns', '*.php');
|
|
|
|
foreach ($files as $file) {
|
|
$times[$file] = filemtime($file);
|
|
}
|
|
}
|
|
|
|
return $times;
|
|
}
|
|
|
|
private static function cacheCurrentDimensionFileChanges()
|
|
{
|
|
$changes = self::getCurrentDimensionFileChanges();
|
|
|
|
$cache = self::buildCache();
|
|
$cache->save(self::$cacheId, $changes);
|
|
}
|
|
|
|
private static function buildCache()
|
|
{
|
|
return PiwikCache::getEagerCache();
|
|
}
|
|
|
|
private static function getCachedDimensionFileChanges()
|
|
{
|
|
$cache = self::buildCache();
|
|
|
|
if ($cache->contains(self::$cacheId)) {
|
|
return $cache->fetch(self::$cacheId);
|
|
}
|
|
|
|
return array();
|
|
}
|
|
}
|