diff --git a/core/Config.php b/core/Config.php index 572291b259bbc7af3e3b3c215d0517f8edba9c50..749c585647c96bf65b1bd90d328fc22a5071f380 100644 --- a/core/Config.php +++ b/core/Config.php @@ -459,6 +459,7 @@ class Config extends Singleton $user['bridge'] = 1; return $user; } + } catch (Exception $e) {} return array(); diff --git a/core/CronArchive.php b/core/CronArchive.php index bbb4a7a6e421fd1dda1593af8c892f99ffdd9a71..47fbd7d3dfaa839b53d8c3c6730b46c5db061a9c 100644 --- a/core/CronArchive.php +++ b/core/CronArchive.php @@ -966,11 +966,14 @@ class CronArchive private function initTokenAuth() { - $superUser = Db::get()->fetchRow("SELECT login, token_auth - FROM " . Common::prefixTable("user") . " - WHERE superuser_access = 1 - ORDER BY date_registered ASC"); - $this->token_auth = $superUser['token_auth']; + $token = ''; + + /** + * @ignore + */ + Piwik::postEvent('CronArchive.getTokenAuth', array(&$token)); + + $this->token_auth = $token; } private function initPiwikHost($piwikUrl = false) diff --git a/core/DataAccess/ArchivePurger.php b/core/DataAccess/ArchivePurger.php index 2c1379115dff26c5c32563e68a5763542c262d58..d795ee6fba16c7c4986597ae699ff8162b676dfc 100644 --- a/core/DataAccess/ArchivePurger.php +++ b/core/DataAccess/ArchivePurger.php @@ -31,16 +31,7 @@ class ArchivePurger * Select the archives that have already been invalidated and have been since re-processed. * It purges records for each distinct { archive name (includes segment hash) , idsite, date, period } tuple. */ - $query = ' - SELECT t1.idarchive FROM `' . $archiveTable . '` t1 - INNER JOIN `' . $archiveTable . '` t2 - ON t1.name = t2.name AND t1.idsite=t2.idsite - AND t1.date1=t2.date1 AND t1.date2=t2.date2 AND t1.period=t2.period - WHERE t1.value = ' . ArchiveWriter::DONE_INVALIDATED . ' - AND t2.value IN(' . ArchiveWriter::DONE_OK . ', ' . ArchiveWriter::DONE_OK_TEMPORARY . ') - AND t1.ts_archived < t2.ts_archived AND t1.name LIKE \'done%\''; - - $result = Db::fetchAll($query); + $result = self::getModel()->purgeInvalidatedArchiveTable($archiveTable); if (count($result) > 0) { $archiveIds = array_map( @@ -59,35 +50,39 @@ class ArchivePurger } } + private static function getModel() + { + return new Model(); + } public static function purgeOutdatedArchives(Date $dateStart) { $purgeArchivesOlderThan = Rules::shouldPurgeOutdatedArchives($dateStart); + if (!$purgeArchivesOlderThan) { return; } $idArchivesToDelete = self::getTemporaryArchiveIdsOlderThan($dateStart, $purgeArchivesOlderThan); + if (!empty($idArchivesToDelete)) { self::deleteArchiveIds($dateStart, $idArchivesToDelete); } + self::deleteArchivesWithPeriodRange($dateStart); Log::debug("Purging temporary archives: done [ purged archives older than %s in %s ] [Deleted IDs: %s]", - $purgeArchivesOlderThan, - $dateStart->toString("Y-m"), - implode(',', $idArchivesToDelete)); + $purgeArchivesOlderThan, + $dateStart->toString("Y-m"), + implode(',', $idArchivesToDelete)); } protected static function getTemporaryArchiveIdsOlderThan(Date $date, $purgeArchivesOlderThan) { - $query = "SELECT idarchive FROM " . ArchiveTableCreator::getNumericTable($date) . " - WHERE name LIKE 'done%' - AND (( value = " . ArchiveWriter::DONE_OK_TEMPORARY . " - AND ts_archived < ?) - OR value = " . ArchiveWriter::DONE_ERROR . ")"; + $archiveTable = ArchiveTableCreator::getNumericTable($date); + + $result = self::getModel()->getTemporaryArchivesOlderThan($archiveTable, $purgeArchivesOlderThan); - $result = Db::fetchAll($query, array($purgeArchivesOlderThan)); $idArchivesToDelete = array(); if (!empty($result)) { foreach ($result as $row) { @@ -103,36 +98,25 @@ class ArchivePurger */ protected static function deleteArchivesWithPeriodRange(Date $date) { - $query = "DELETE FROM %s WHERE period = ? AND ts_archived < ?"; - - $yesterday = Date::factory('yesterday')->getDateTime(); - $bind = array(Piwik::$idPeriods['range'], $yesterday); $numericTable = ArchiveTableCreator::getNumericTable($date); - Db::query(sprintf($query, $numericTable), $bind); + $blobTable = ArchiveTableCreator::getBlobTable($date); + $yesterday = Date::factory('yesterday')->getDateTime(); + Log::debug("Purging Custom Range archives: done [ purged archives older than %s from %s / blob ]", - $yesterday, - $numericTable); - try { - Db::query(sprintf($query, ArchiveTableCreator::getBlobTable($date)), $bind); - } catch (Exception $e) { - // Individual blob tables could be missing - } + $yesterday, $numericTable); + + self::getModel()->deleteArchivesWithPeriodRange($numericTable, $blobTable, Piwik::$idPeriods['range'], $yesterday); } protected static function deleteArchiveIds(Date $date, $idArchivesToDelete) { - $batches = array_chunk($idArchivesToDelete, 1000); - foreach ($batches as $idsToDelete) { - $query = "DELETE FROM %s WHERE idarchive IN (" . implode(',', $idsToDelete) . ")"; + $batches = array_chunk($idArchivesToDelete, 1000); + $numericTable = ArchiveTableCreator::getNumericTable($date); + $blobTable = ArchiveTableCreator::getBlobTable($date); - Db::query(sprintf($query, ArchiveTableCreator::getNumericTable($date))); - try { - Db::query(sprintf($query, ArchiveTableCreator::getBlobTable($date))); - } catch (Exception $e) { - // Individual blob tables could be missing - } + foreach ($batches as $idsToDelete) { + self::getModel()->deleteArchiveIds($numericTable, $blobTable, $idsToDelete); } - } } diff --git a/core/DataAccess/ArchiveSelector.php b/core/DataAccess/ArchiveSelector.php index 594889d6dd615163385346a7874e85184efb4657..a342257eca4eb1714643489357890a2e2efc58ad 100644 --- a/core/DataAccess/ArchiveSelector.php +++ b/core/DataAccess/ArchiveSelector.php @@ -38,40 +38,36 @@ class ArchiveSelector const NB_VISITS_CONVERTED_RECORD_LOOKED_UP = "nb_visits_converted"; + private static function getModel() + { + return new Model(); + } + public static function getArchiveIdAndVisits(ArchiveProcessor\Parameters $params, $minDatetimeArchiveProcessedUTC) { - $dateStart = $params->getPeriod()->getDateStart(); - $bindSQL = array($params->getSite()->getId(), - $dateStart->toString('Y-m-d'), - $params->getPeriod()->getDateEnd()->toString('Y-m-d'), - $params->getPeriod()->getId(), - ); - - $timeStampWhere = ''; + $idSite = $params->getSite()->getId(); + $period = $params->getPeriod()->getId(); + $dateStart = $params->getPeriod()->getDateStart(); + $dateStartIso = $dateStart->toString('Y-m-d'); + $dateEndIso = $params->getPeriod()->getDateEnd()->toString('Y-m-d'); + + $numericTable = ArchiveTableCreator::getNumericTable($dateStart); + + $minDatetimeIsoArchiveProcessedUTC = null; if ($minDatetimeArchiveProcessedUTC) { - $timeStampWhere = " AND ts_archived >= ? "; - $bindSQL[] = Date::factory($minDatetimeArchiveProcessedUTC)->getDatetime(); + $minDatetimeIsoArchiveProcessedUTC = Date::factory($minDatetimeArchiveProcessedUTC)->getDatetime(); } $requestedPlugin = $params->getRequestedPlugin(); - $segment = $params->getSegment(); + $segment = $params->getSegment(); $isSkipAggregationOfSubTables = $params->isSkipAggregationOfSubTables(); - $plugins = array("VisitsSummary", $requestedPlugin); - $sqlWhereArchiveName = self::getNameCondition($plugins, $segment, $isSkipAggregationOfSubTables); - - $sqlQuery = " SELECT idarchive, value, name, date1 as startDate - FROM " . ArchiveTableCreator::getNumericTable($dateStart) . "`` - WHERE idsite = ? - AND date1 = ? - AND date2 = ? - AND period = ? - AND ( ($sqlWhereArchiveName) - OR name = '" . self::NB_VISITS_RECORD_LOOKED_UP . "' - OR name = '" . self::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "') - $timeStampWhere - ORDER BY idarchive DESC"; - $results = Db::fetchAll($sqlQuery, $bindSQL); + + $doneFlags = self::getDoneFlags($plugins, $segment, $isSkipAggregationOfSubTables); + $possibleValues = self::getPossibleValues(); + + $results = self::getModel()->getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC, $doneFlags, $possibleValues); + if (empty($results)) { return false; } @@ -81,9 +77,8 @@ class ArchiveSelector list($visits, $visitsConverted) = self::getVisitsMetricsFromResults($idArchive, $idArchiveVisitsSummary, $results); - if ($visits === false - && $idArchive === false - ) { + if (false === $visits && false === $idArchive) { + return false; } @@ -94,9 +89,11 @@ class ArchiveSelector { $visits = $visitsConverted = false; $archiveWithVisitsMetricsWasFound = ($idArchiveVisitsSummary !== false); + if ($archiveWithVisitsMetricsWasFound) { $visits = $visitsConverted = 0; } + foreach ($results as $result) { if (in_array($result['idarchive'], array($idArchive, $idArchiveVisitsSummary))) { $value = (int)$result['value']; @@ -112,6 +109,7 @@ class ArchiveSelector } } } + return array($visits, $visitsConverted); } @@ -119,6 +117,7 @@ class ArchiveSelector { $idArchive = false; $namesRequestedPlugin = Rules::getDoneFlags(array($requestedPlugin), $segment, $isSkipAggregationOfSubTables); + foreach ($results as $result) { if ($idArchive === false && in_array($result['name'], $namesRequestedPlugin) @@ -127,6 +126,7 @@ class ArchiveSelector break; } } + return $idArchive; } @@ -198,8 +198,10 @@ class ArchiveSelector $sql = sprintf($getArchiveIdsSql, $table, $dateCondition); + $archiveIds = Db::fetchAll($sql, $bind); + // get the archive IDs - foreach (Db::fetchAll($sql, $bind) as $row) { + foreach ($archiveIds as $row) { $archiveName = $row['name']; //FIXMEA duplicate with Archive.php @@ -249,18 +251,23 @@ class ArchiveSelector // get data from every table we're querying $rows = array(); foreach ($archiveIds as $period => $ids) { + if (empty($ids)) { throw new Exception("Unexpected: id archive not found for period '$period' '"); } + // $period = "2009-01-04,2009-01-04", $date = Date::factory(substr($period, 0, 10)); + if ($archiveDataType == 'numeric') { $table = ArchiveTableCreator::getNumericTable($date); } else { $table = ArchiveTableCreator::getBlobTable($date); } - $sql = sprintf($getValuesSql, $table, implode(',', $ids)); + + $sql = sprintf($getValuesSql, $table, implode(',', $ids)); $dataRows = Db::fetchAll($sql, $bind); + foreach ($dataRows as $row) { $rows[] = $row; } @@ -279,13 +286,44 @@ class ArchiveSelector * @return string */ private static function getNameCondition(array $plugins, Segment $segment, $isSkipAggregationOfSubTables) + { + // the flags used to tell how the archiving process for a specific archive was completed, + // if it was completed + $doneFlags = self::getDoneFlags($plugins, $segment, $isSkipAggregationOfSubTables); + $allDoneFlags = "'" . implode("','", $doneFlags) . "'"; + + $possibleValues = self::getPossibleValues(); + + // create the SQL to find archives that are DONE + return "((name IN ($allDoneFlags)) AND (value IN (" . implode(',', $possibleValues) . ")))"; + } + + /** + * Returns the SQL condition used to find successfully completed archives that + * this instance is querying for. + * + * @param array $plugins + * @param Segment $segment + * @param bool $isSkipAggregationOfSubTables + * @return string + */ + private static function getDoneFlags(array $plugins, Segment $segment, $isSkipAggregationOfSubTables) { // the flags used to tell how the archiving process for a specific archive was completed, // if it was completed $doneFlags = Rules::getDoneFlags($plugins, $segment, $isSkipAggregationOfSubTables); - $allDoneFlags = "'" . implode("','", $doneFlags) . "'"; + return $doneFlags; + } + /** + * Returns the SQL condition used to find successfully completed archives that + * this instance is querying for. + * + * @return string + */ + private static function getPossibleValues() + { $possibleValues = array(ArchiveWriter::DONE_OK, ArchiveWriter::DONE_OK_TEMPORARY); if (!Rules::isRequestAuthorizedToArchive()) { @@ -293,9 +331,7 @@ class ArchiveSelector $possibleValues[] = ArchiveWriter::DONE_INVALIDATED; } - // create the SQL to find archives that are DONE - return "((name IN ($allDoneFlags)) AND " . - " (value IN (" . implode(',', $possibleValues) . ")))"; + return $possibleValues; } } diff --git a/core/DataAccess/ArchiveTableCreator.php b/core/DataAccess/ArchiveTableCreator.php index 67abd2b1237eca4fa39c749d5795f81a5a0f47fa..230c7da05a08a9ac7862448300fb8ed3936cb5fb 100644 --- a/core/DataAccess/ArchiveTableCreator.php +++ b/core/DataAccess/ArchiveTableCreator.php @@ -9,7 +9,6 @@ namespace Piwik\DataAccess; -use Exception; use Piwik\Common; use Piwik\Date; use Piwik\Db; @@ -18,8 +17,7 @@ use Piwik\DbHelper; class ArchiveTableCreator { const NUMERIC_TABLE = "numeric"; - - const BLOB_TABLE = "blob"; + const BLOB_TABLE = "blob"; public static $tablesAlreadyInstalled = null; @@ -38,7 +36,9 @@ class ArchiveTableCreator $tableNamePrefix = "archive_" . $type; $tableName = $tableNamePrefix . "_" . $date->toString('Y_m'); $tableName = Common::prefixTable($tableName); + self::createArchiveTablesIfAbsent($tableName, $tableNamePrefix); + return $tableName; } @@ -49,24 +49,16 @@ class ArchiveTableCreator } if (!in_array($tableName, self::$tablesAlreadyInstalled)) { - $db = Db::get(); - $sql = DbHelper::getTableCreateSql($tableNamePrefix); - - // replace table name template by real name - $tableNamePrefix = Common::prefixTable($tableNamePrefix); - $sql = str_replace($tableNamePrefix, $tableName, $sql); - try { - $db->query($sql); - } catch (Exception $e) { - // accept mysql error 1050: table already exists, throw otherwise - if (!$db->isErrNo($e, '1050')) { - throw $e; - } - } + self::getModel()->createArchiveTable($tableName, $tableNamePrefix); self::$tablesAlreadyInstalled[] = $tableName; } } + private static function getModel() + { + return new Model(); + } + public static function clear() { self::$tablesAlreadyInstalled = null; @@ -89,6 +81,7 @@ class ArchiveTableCreator } $archiveTables = array(); + foreach (self::$tablesAlreadyInstalled as $table) { if (strpos($table, 'archive_numeric_') !== false || strpos($table, 'archive_blob_') !== false @@ -96,13 +89,15 @@ class ArchiveTableCreator $archiveTables[] = $table; } } + return $archiveTables; } public static function getDateFromTableName($tableName) { $tableName = Common::unprefixTable($tableName); - $date = str_replace(array('archive_numeric_', 'archive_blob_'), '', $tableName); + $date = str_replace(array('archive_numeric_', 'archive_blob_'), '', $tableName); + return $date; } @@ -111,9 +106,11 @@ class ArchiveTableCreator if (strpos($tableName, 'archive_numeric_') !== false) { return self::NUMERIC_TABLE; } + if (strpos($tableName, 'archive_blob_') !== false) { return self::BLOB_TABLE; } + return false; } } diff --git a/core/DataAccess/ArchiveWriter.php b/core/DataAccess/ArchiveWriter.php index 70f2159b67dd5a4e058ec0e47c218e5fe9c969e2..1ea6b0d08c26a5622eaa4a29f62f75f28de7441c 100644 --- a/core/DataAccess/ArchiveWriter.php +++ b/core/DataAccess/ArchiveWriter.php @@ -23,7 +23,6 @@ use Piwik\Period; */ class ArchiveWriter { - const PREFIX_SQL_LOCK = "locked_"; /** * Flag stored at the end of the archiving * @@ -64,9 +63,10 @@ class ArchiveWriter public function __construct(ArchiveProcessor\Parameters $params, $isArchiveTemporary) { $this->idArchive = false; - $this->idSite = $params->getSite()->getId(); - $this->segment = $params->getSegment(); - $this->period = $params->getPeriod(); + $this->idSite = $params->getSite()->getId(); + $this->segment = $params->getSegment(); + $this->period = $params->getPeriod(); + $idSites = array($this->idSite); $this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin(), $params->isSkipAggregationOfSubTables()); $this->isArchiveTemporary = $isArchiveTemporary; @@ -92,7 +92,7 @@ class ArchiveWriter $newName = $name . '_' . $id; } - $value = $this->compress($value); + $value = $this->compress($value); $clean[] = array($newName, $value); } $this->insertBulkRecords($clean); @@ -108,6 +108,7 @@ class ArchiveWriter if ($this->idArchive === false) { throw new Exception("Must call allocateNewArchiveId() first"); } + return $this->idArchive; } @@ -119,7 +120,11 @@ class ArchiveWriter public function finalizeArchive() { - $this->deletePreviousArchiveStatus(); + $numericTable = $this->getTableNumeric(); + $idArchive = $this->getIdArchive(); + + $this->getModel()->deletePreviousArchiveStatus($numericTable, $idArchive, $this->doneFlag); + $this->logArchiveStatusAsFinal(); } @@ -128,28 +133,8 @@ class ArchiveWriter if (Db::get()->hasBlobDataType()) { return gzcompress($data); } - return $data; - } - - protected function getArchiveLockName() - { - $numericTable = $this->getTableNumeric(); - $dbLockName = "allocateNewArchiveId.$numericTable"; - return $dbLockName; - } - - protected function acquireArchiveTableLock() - { - $dbLockName = $this->getArchiveLockName(); - if (Db::getDbLock($dbLockName, $maxRetries = 30) === false) { - throw new Exception("allocateNewArchiveId: Cannot get named lock $dbLockName."); - } - } - protected function releaseArchiveTableLock() - { - $dbLockName = $this->getArchiveLockName(); - Db::releaseDbLock($dbLockName); + return $data; } protected function allocateNewArchiveId() @@ -171,56 +156,31 @@ class ArchiveWriter { $numericTable = $this->getTableNumeric(); $idSite = $this->idSite; + $date = date("Y-m-d H:i:s"); - $this->acquireArchiveTableLock(); + $id = $this->getModel()->insertNewArchiveId($numericTable, $idSite, $date); - $locked = self::PREFIX_SQL_LOCK . Common::generateUniqId(); - $date = date("Y-m-d H:i:s"); - $insertSql = "INSERT INTO $numericTable " - . " SELECT IFNULL( MAX(idarchive), 0 ) + 1, - '" . $locked . "', - " . (int)$idSite . ", - '" . $date . "', - '" . $date . "', - 0, - '" . $date . "', - 0 " - . " FROM $numericTable as tb1"; - Db::get()->exec($insertSql); - - $this->releaseArchiveTableLock(); - - $selectIdSql = "SELECT idarchive FROM $numericTable WHERE name = ? LIMIT 1"; - $id = Db::get()->fetchOne($selectIdSql, $locked); return $id; } - protected function logArchiveStatusAsIncomplete() + private function getModel() { - $statusWhileProcessing = self::DONE_ERROR; - $this->insertRecord($this->doneFlag, $statusWhileProcessing); + return new Model(); } - protected function deletePreviousArchiveStatus() + protected function logArchiveStatusAsIncomplete() { - // without advisory lock here, the DELETE would acquire Exclusive Lock - $this->acquireArchiveTableLock(); - - Db::query("DELETE FROM " . $this->getTableNumeric() . " - WHERE idarchive = ? AND (name = '" . $this->doneFlag - . "' OR name LIKE '" . self::PREFIX_SQL_LOCK . "%')", - array($this->getIdArchive()) - ); - - $this->releaseArchiveTableLock(); + $this->insertRecord($this->doneFlag, self::DONE_ERROR); } protected function logArchiveStatusAsFinal() { $status = self::DONE_OK; + if ($this->isArchiveTemporary) { $status = self::DONE_OK_TEMPORARY; } + $this->insertRecord($this->doneFlag, $status); } @@ -233,27 +193,37 @@ class ArchiveWriter foreach ($records as $record) { $this->insertRecord($record[0], $record[1]); } + return true; } + $bindSql = $this->getInsertRecordBind(); - $values = array(); + $values = array(); $valueSeen = false; foreach ($records as $record) { // don't record zero - if (empty($record[1])) continue; + if (empty($record[1])) { + continue; + } - $bind = $bindSql; - $bind[] = $record[0]; // name - $bind[] = $record[1]; // value + $bind = $bindSql; + $bind[] = $record[0]; // name + $bind[] = $record[1]; // value $values[] = $bind; $valueSeen = $record[1]; } - if (empty($values)) return true; + + if (empty($values)) { + return true; + } $tableName = $this->getTableNameToInsert($valueSeen); - BatchInsert::tableInsertBatch($tableName, $this->getInsertFields(), $values); + $fields = $this->getInsertFields(); + + BatchInsert::tableInsertBatch($tableName, $fields, $values); + return true; } @@ -272,15 +242,11 @@ class ArchiveWriter } $tableName = $this->getTableNameToInsert($value); + $fields = $this->getInsertFields(); + $record = $this->getInsertRecordBind(); + + $this->getModel()->insertRecord($tableName, $fields, $record, $name, $value); - // duplicate idarchives are Ignored, see https://github.com/piwik/piwik/issues/987 - $query = "INSERT IGNORE INTO " . $tableName . " - (" . implode(", ", $this->getInsertFields()) . ") - VALUES (?,?,?,?,?,?,?,?)"; - $bindSql = $this->getInsertRecordBind(); - $bindSql[] = $name; - $bindSql[] = $value; - Db::query($query, $bindSql); return true; } @@ -299,6 +265,7 @@ class ArchiveWriter if (is_numeric($value)) { return $this->getTableNumeric(); } + return ArchiveTableCreator::getBlobTable($this->dateStart); } diff --git a/core/DataAccess/LogAggregator.php b/core/DataAccess/LogAggregator.php index 79db3148b7fee911d4398aa95c8129f28b813564..83946b1f6fad37a58d7424a799d00b065cd32aad 100644 --- a/core/DataAccess/LogAggregator.php +++ b/core/DataAccess/LogAggregator.php @@ -292,52 +292,61 @@ class LogAggregator $tableName = self::LOG_VISIT_TABLE; $availableMetrics = $this->getVisitsMetricFields(); - $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics); - $from = array($tableName); - $where = $this->getWhereStatement($tableName, self::VISIT_DATETIME_FIELD, $where); + $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics); + $from = array($tableName); + $where = $this->getWhereStatement($tableName, self::VISIT_DATETIME_FIELD, $where); $groupBy = $this->getGroupByStatement($dimensions, $tableName); $orderBy = false; if ($rankingQuery) { $orderBy = '`' . Metrics::INDEX_NB_VISITS . '` DESC'; } + $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy); if ($rankingQuery) { unset($availableMetrics[Metrics::INDEX_MAX_ACTIONS]); $sumColumns = array_keys($availableMetrics); + if ($metrics) { $sumColumns = array_intersect($sumColumns, $metrics); } + $rankingQuery->addColumn($sumColumns, 'sum'); if ($this->isMetricRequested(Metrics::INDEX_MAX_ACTIONS, $metrics)) { $rankingQuery->addColumn(Metrics::INDEX_MAX_ACTIONS, 'max'); } + return $rankingQuery->execute($query['sql'], $query['bind']); } + return $this->getDb()->query($query['sql'], $query['bind']); } protected function getSelectsMetrics($metricsAvailable, $metricsRequested = false) { $selects = array(); + foreach ($metricsAvailable as $metricId => $statement) { if ($this->isMetricRequested($metricId, $metricsRequested)) { - $aliasAs = $this->getSelectAliasAs($metricId); + $aliasAs = $this->getSelectAliasAs($metricId); $selects[] = $statement . $aliasAs; } } + return $selects; } protected function getSelectStatement($dimensions, $tableName, $additionalSelects, array $availableMetrics, $requestedMetrics = false) { $dimensionsToSelect = $this->getDimensionsToSelect($dimensions, $additionalSelects); + $selects = array_merge( $this->getSelectDimensions($dimensionsToSelect, $tableName), $this->getSelectsMetrics($availableMetrics, $requestedMetrics), !empty($additionalSelects) ? $additionalSelects : array() ); + $select = implode(self::FIELDS_SEPARATOR, $selects); return $select; } @@ -354,6 +363,7 @@ class LogAggregator if (empty($additionalSelects)) { return $dimensions; } + $dimensionsToSelect = array(); foreach ($dimensions as $selectAs => $dimension) { $asAlias = $this->getSelectAliasAs($dimension); @@ -363,6 +373,7 @@ class LogAggregator } } } + $dimensionsToSelect = array_unique($dimensionsToSelect); return $dimensionsToSelect; } @@ -381,6 +392,7 @@ class LogAggregator { foreach ($dimensions as $selectAs => &$field) { $selectAsString = $field; + if (!is_numeric($selectAs)) { $selectAsString = $selectAs; } else { @@ -389,16 +401,18 @@ class LogAggregator $selectAsString = $appendSelectAs = false; } } + $isKnownField = !in_array($field, array('referrer_data')); - if ($selectAsString == $field - && $isKnownField - ) { + + if ($selectAsString == $field && $isKnownField) { $field = $this->prefixColumn($field, $tableName); } + if ($appendSelectAs && $selectAsString) { $field = $this->prefixColumn($field, $tableName) . $this->getSelectAliasAs($selectAsString); } } + return $dimensions; } @@ -421,7 +435,7 @@ class LogAggregator protected function isFieldFunctionOrComplexExpression($field) { return strpos($field, "(") !== false - || strpos($field, "CASE") !== false; + || strpos($field, "CASE") !== false; } protected function getSelectAliasAs($metricId) @@ -432,7 +446,7 @@ class LogAggregator protected function isMetricRequested($metricId, $metricsRequested) { return $metricsRequested === false - || in_array($metricId, $metricsRequested); + || in_array($metricId, $metricsRequested); } protected function getWhereStatement($tableName, $datetimeField, $extraWhere = false) @@ -440,17 +454,20 @@ class LogAggregator $where = "$tableName.$datetimeField >= ? AND $tableName.$datetimeField <= ? AND $tableName.idsite IN (". Common::getSqlStringFieldsArray($this->sites) . ")"; + if (!empty($extraWhere)) { $extraWhere = sprintf($extraWhere, $tableName, $tableName); - $where .= ' AND ' . $extraWhere; + $where .= ' AND ' . $extraWhere; } + return $where; } protected function getGroupByStatement($dimensions, $tableName) { $dimensions = $this->getSelectDimensions($dimensions, $tableName, $appendSelectAs = false); - $groupBy = implode(", ", $dimensions); + $groupBy = implode(", ", $dimensions); + return $groupBy; } @@ -464,6 +481,7 @@ class LogAggregator { $bind = array($this->dateStart->getDateStartUTC(), $this->dateEnd->getDateEndUTC()); $bind = array_merge($bind, $this->sites); + return $bind; } @@ -624,9 +642,9 @@ class LogAggregator $tableName = self::LOG_ACTIONS_TABLE; $availableMetrics = $this->getActionsMetricFields(); - $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics); - $from = array($tableName); - $where = $this->getWhereStatement($tableName, self::ACTION_DATETIME_FIELD, $where); + $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics, $metrics); + $from = array($tableName); + $where = $this->getWhereStatement($tableName, self::ACTION_DATETIME_FIELD, $where); $groupBy = $this->getGroupByStatement($dimensions, $tableName); $orderBy = false; @@ -638,12 +656,14 @@ class LogAggregator foreach ($joinLogActionOnColumn as $i => $joinColumn) { $tableAlias = 'log_action' . ($multiJoin ? $i + 1 : ''); + if (strpos($joinColumn, ' ') === false) { $joinOn = $tableAlias . '.idaction = ' . $tableName . '.' . $joinColumn; } else { // more complex join column like if (...) $joinOn = $tableAlias . '.idaction = ' . $joinColumn; } + $from[] = array( 'table' => 'log_action', 'tableAlias' => $tableAlias, @@ -663,7 +683,9 @@ class LogAggregator if ($metrics) { $sumColumns = array_intersect($sumColumns, $metrics); } + $rankingQuery->addColumn($sumColumns, 'sum'); + return $rankingQuery->execute($query['sql'], $query['bind']); } @@ -738,8 +760,8 @@ class LogAggregator public function queryConversionsByDimension($dimensions = array(), $where = false, $additionalSelects = array()) { $dimensions = array_merge(array(self::IDGOAL_FIELD), $dimensions); + $tableName = self::LOG_CONVERSION_TABLE; $availableMetrics = $this->getConversionsMetricFields(); - $tableName = self::LOG_CONVERSION_TABLE; $select = $this->getSelectStatement($dimensions, $tableName, $additionalSelects, $availableMetrics); @@ -748,6 +770,7 @@ class LogAggregator $groupBy = $this->getGroupByStatement($dimensions, $tableName); $orderBy = false; $query = $this->generateQuery($select, $from, $where, $groupBy, $orderBy); + return $this->getDb()->query($query['sql'], $query['bind']); } @@ -824,14 +847,16 @@ class LogAggregator { $selects = array(); $extraCondition = ''; + if ($restrictToReturningVisitors) { // extra condition for the SQL SELECT that makes sure only returning visits are counted // when creating the 'days since last visit' report $extraCondition = 'and log_visit.visitor_returning = 1'; - $extraSelect = "sum(case when log_visit.visitor_returning = 0 then 1 else 0 end) " - . " as `" . $selectColumnPrefix . 'General_NewVisits' . "`"; + $extraSelect = "sum(case when log_visit.visitor_returning = 0 then 1 else 0 end) " + . " as `" . $selectColumnPrefix . 'General_NewVisits' . "`"; $selects[] = $extraSelect; } + foreach ($ranges as $gap) { if (count($gap) == 2) { $lowerBound = $gap[0]; @@ -840,12 +865,11 @@ class LogAggregator $selectAs = "$selectColumnPrefix$lowerBound-$upperBound"; $selects[] = "sum(case when $table.$column between $lowerBound and $upperBound $extraCondition" . - " then 1 else 0 end) as `$selectAs`"; + " then 1 else 0 end) as `$selectAs`"; } else { $lowerBound = $gap[0]; - $selectAs = $selectColumnPrefix . ($lowerBound + 1) . urlencode('+'); - + $selectAs = $selectColumnPrefix . ($lowerBound + 1) . urlencode('+'); $selects[] = "sum(case when $table.$column > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`"; } } @@ -869,6 +893,7 @@ class LogAggregator public static function makeArrayOneColumn($row, $columnName, $lookForThisPrefix = false) { $cleanRow = array(); + foreach ($row as $label => $count) { if (empty($lookForThisPrefix) || strpos($label, $lookForThisPrefix) === 0 @@ -877,6 +902,7 @@ class LogAggregator $cleanRow[$cleanLabel] = array($columnName => $count); } } + return $cleanRow; } diff --git a/core/DataAccess/Model.php b/core/DataAccess/Model.php new file mode 100644 index 0000000000000000000000000000000000000000..facaa5dd9741ab9d4b18f821a5966e6d8f95575c --- /dev/null +++ b/core/DataAccess/Model.php @@ -0,0 +1,229 @@ +<?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\DataAccess; + +use Exception; +use Piwik\Common; +use Piwik\Db; +use Piwik\DbHelper; + +/** + * Cleans up outdated archives + * + * @package Piwik\DataAccess + */ +class Model +{ + const PREFIX_SQL_LOCK = "locked_"; + + public function purgeInvalidatedArchiveTable($archiveTable) + { + /** + * Select the archives that have already been invalidated and have been since re-processed. + * It purges records for each distinct { archive name (includes segment hash) , idsite, date, period } tuple. + */ + $query = 'SELECT t1.idarchive FROM `' . $archiveTable . '` t1 + INNER JOIN `' . $archiveTable . '` t2 + ON t1.name = t2.name AND t1.idsite=t2.idsite + AND t1.date1=t2.date1 AND t1.date2=t2.date2 AND t1.period=t2.period + WHERE t1.value = ' . ArchiveWriter::DONE_INVALIDATED . ' + AND t2.value IN(' . ArchiveWriter::DONE_OK . ', ' . ArchiveWriter::DONE_OK_TEMPORARY . ') + AND t1.ts_archived < t2.ts_archived AND t1.name LIKE \'done%\''; + + $result = Db::fetchAll($query); + + return $result; + } + + public function getTemporaryArchivesOlderThan($archiveTable, $purgeArchivesOlderThan) + { + $query = "SELECT idarchive FROM " . $archiveTable . " + WHERE name LIKE 'done%' + AND (( value = " . ArchiveWriter::DONE_OK_TEMPORARY . " + AND ts_archived < ?) + OR value = " . ArchiveWriter::DONE_ERROR . ")"; + + return Db::fetchAll($query, array($purgeArchivesOlderThan)); + } + + /* + * Deleting "Custom Date Range" reports, since they can be re-processed and would take up un-necessary space + */ + public function deleteArchivesWithPeriodRange($numericTable, $blobTable, $range, $date) + { + $query = "DELETE FROM %s WHERE period = ? AND ts_archived < ?"; + $bind = array($range, $date); + + Db::query(sprintf($query, $numericTable), $bind); + + try { + Db::query(sprintf($query, $blobTable), $bind); + } catch (Exception $e) { + // Individual blob tables could be missing + } + } + + public function deleteArchiveIds($numericTable, $blobTable, $idsToDelete) + { + $query = "DELETE FROM %s WHERE idarchive IN (" . implode(',', $idsToDelete) . ")"; + + Db::query(sprintf($query, $numericTable)); + + try { + Db::query(sprintf($query, $blobTable)); + } catch (Exception $e) { + // Individual blob tables could be missing + } + } + + public function getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC, $doneFlags, $possibleValues) + { + $bindSQL = array($idSite, + $dateStartIso, + $dateEndIso, + $period, + ); + + $timeStampWhere = ''; + if ($minDatetimeIsoArchiveProcessedUTC) { + $timeStampWhere = " AND ts_archived >= ? "; + $bindSQL[] = $minDatetimeIsoArchiveProcessedUTC; + } + + $sqlWhereArchiveName = self::getNameCondition($doneFlags, $possibleValues); + + $sqlQuery = "SELECT idarchive, value, name, date1 as startDate FROM $numericTable + WHERE idsite = ? + AND date1 = ? + AND date2 = ? + AND period = ? + AND ( ($sqlWhereArchiveName) + OR name = '" . ArchiveSelector::NB_VISITS_RECORD_LOOKED_UP . "' + OR name = '" . ArchiveSelector::NB_VISITS_CONVERTED_RECORD_LOOKED_UP . "') + $timeStampWhere + ORDER BY idarchive DESC"; + $results = Db::fetchAll($sqlQuery, $bindSQL); + + return $results; + } + + public function createArchiveTable($tableName, $tableNamePrefix) + { + $db = Db::get(); + $sql = DbHelper::getTableCreateSql($tableNamePrefix); + + // replace table name template by real name + $tableNamePrefix = Common::prefixTable($tableNamePrefix); + $sql = str_replace($tableNamePrefix, $tableName, $sql); + + try { + $db->query($sql); + } catch (Exception $e) { + // accept mysql error 1050: table already exists, throw otherwise + if (!$db->isErrNo($e, '1050')) { + throw $e; + } + } + } + + /** + * Locks the archive table to generate a new archive ID. + * + * We lock to make sure that + * if several archiving processes are running at the same time (for different websites and/or periods) + * then they will each use a unique archive ID. + * + * @return int + */ + public function insertNewArchiveId($numericTable, $idSite, $date) + { + $this->acquireArchiveTableLock($numericTable); + + $locked = self::PREFIX_SQL_LOCK . Common::generateUniqId(); + + $insertSql = "INSERT INTO $numericTable " + . " SELECT IFNULL( MAX(idarchive), 0 ) + 1, + '" . $locked . "', + " . (int)$idSite . ", + '" . $date . "', + '" . $date . "', + 0, + '" . $date . "', + 0 " + . " FROM $numericTable as tb1"; + Db::get()->exec($insertSql); + + $this->releaseArchiveTableLock($numericTable); + + $selectIdSql = "SELECT idarchive FROM $numericTable WHERE name = ? LIMIT 1"; + $id = Db::get()->fetchOne($selectIdSql, $locked); + return $id; + } + + public function deletePreviousArchiveStatus($numericTable, $archiveId, $doneFlag) + { + // without advisory lock here, the DELETE would acquire Exclusive Lock + $this->acquireArchiveTableLock($numericTable); + + Db::query("DELETE FROM $numericTable WHERE idarchive = ? AND (name = '" . $doneFlag + . "' OR name LIKE '" . self::PREFIX_SQL_LOCK . "%')", + array($archiveId) + ); + + $this->releaseArchiveTableLock($numericTable); + } + + public function insertRecord($tableName, $fields, $record, $name, $value) + { + // duplicate idarchives are Ignored, see https://github.com/piwik/piwik/issues/987 + $query = "INSERT IGNORE INTO " . $tableName . " (" . implode(", ", $fields) . ") + VALUES (?,?,?,?,?,?,?,?)"; + + $bindSql = $record; + $bindSql[] = $name; + $bindSql[] = $value; + + Db::query($query, $bindSql); + + return true; + } + + /** + * Returns the SQL condition used to find successfully completed archives that + * this instance is querying for. + */ + private static function getNameCondition($doneFlags, $possibleValues) + { + $allDoneFlags = "'" . implode("','", $doneFlags) . "'"; + + // create the SQL to find archives that are DONE + return "((name IN ($allDoneFlags)) AND (value IN (" . implode(',', $possibleValues) . ")))"; + } + + protected function acquireArchiveTableLock($numericTable) + { + $dbLockName = $this->getArchiveLockName($numericTable); + + if (Db::getDbLock($dbLockName, $maxRetries = 30) === false) { + throw new Exception("allocateNewArchiveId: Cannot get named lock $dbLockName."); + } + } + + protected function releaseArchiveTableLock($numericTable) + { + $dbLockName = $this->getArchiveLockName($numericTable); + Db::releaseDbLock($dbLockName); + } + + protected function getArchiveLockName($numericTable) + { + return "allocateNewArchiveId.$numericTable"; + } + +} diff --git a/core/Db.php b/core/Db.php index 4ad62684f6d3649866f9687180d8ee25f65b22c6..52c454e3333ed84dadc287bafb7da4811d9c3fe7 100644 --- a/core/Db.php +++ b/core/Db.php @@ -141,6 +141,7 @@ class Db } $profiler->queryEnd($q); + return $return; } @@ -279,13 +280,13 @@ class Db public static function deleteAllRows($table, $where, $orderBy, $maxRowsPerQuery = 100000, $parameters = array()) { $orderByClause = $orderBy ? "ORDER BY $orderBy" : ""; - $sql = "DELETE FROM $table - $where - $orderByClause + + $sql = "DELETE FROM $table $where $orderByClause LIMIT " . (int)$maxRowsPerQuery; // delete rows w/ a limit $totalRowsDeleted = 0; + do { $rowsDeleted = self::query($sql, $parameters)->rowCount(); @@ -308,6 +309,7 @@ class Db public static function optimizeTables($tables) { $optimize = Config::getInstance()->General['enable_sql_optimize_queries']; + if (empty($optimize)) { return; } @@ -315,13 +317,14 @@ class Db if (empty($tables)) { return false; } + if (!is_array($tables)) { $tables = array($tables); } // filter out all InnoDB tables $myisamDbTables = array(); - foreach (Db::fetchAll("SHOW TABLE STATUS") as $row) { + foreach (self::getTableStatus() as $row) { if (strtolower($row['Engine']) == 'myisam' && in_array($row['Name'], $tables) ) { @@ -337,6 +340,11 @@ class Db return self::query("OPTIMIZE TABLE " . implode(',', $myisamDbTables)); } + private static function getTableStatus() + { + return Db::fetchAll("SHOW TABLE STATUS"); + } + /** * Drops the supplied table or tables. * @@ -397,6 +405,7 @@ class Db if (!is_array($tablesToRead)) { $tablesToRead = array($tablesToRead); } + if (!is_array($tablesToWrite)) { $tablesToWrite = array($tablesToWrite); } @@ -405,6 +414,7 @@ class Db foreach ($tablesToWrite as $table) { $lockExprs[] = $table . " WRITE"; } + foreach ($tablesToRead as $table) { $lockExprs[] = $table . " READ"; } @@ -466,6 +476,7 @@ class Db public static function segmentedFetchFirst($sql, $first, $last, $step, $params = array()) { $result = false; + if ($step > 0) { for ($i = $first; $result === false && $i <= $last; $i += $step) { $result = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); @@ -475,6 +486,7 @@ class Db $result = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); } } + return $result; } @@ -502,6 +514,7 @@ class Db public static function segmentedFetchOne($sql, $first, $last, $step, $params = array()) { $result = array(); + if ($step > 0) { for ($i = $first; $i <= $last; $i += $step) { $result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); @@ -511,6 +524,7 @@ class Db $result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); } } + return $result; } @@ -539,17 +553,19 @@ class Db public static function segmentedFetchAll($sql, $first, $last, $step, $params = array()) { $result = array(); + if ($step > 0) { for ($i = $first; $i <= $last; $i += $step) { $currentParams = array_merge($params, array($i, $i + $step)); - $result = array_merge($result, self::fetchAll($sql, $currentParams)); + $result = array_merge($result, self::fetchAll($sql, $currentParams)); } } else { for ($i = $first; $i >= $last; $i += $step) { $currentParams = array_merge($params, array($i, $i + $step)); - $result = array_merge($result, self::fetchAll($sql, $currentParams)); + $result = array_merge($result, self::fetchAll($sql, $currentParams)); } } + return $result; } @@ -623,6 +639,7 @@ class Db } $maxRetries--; } + return false; } diff --git a/core/Db/Adapter.php b/core/Db/Adapter.php index 342a320d723c38d58b29d7940521d9e92e7af7a2..5ddd529caa00d8d18a8c3672541ec91ba6135111 100644 --- a/core/Db/Adapter.php +++ b/core/Db/Adapter.php @@ -38,7 +38,7 @@ class Adapter } $className = self::getAdapterClassName($adapterName); - $adapter = new $className($dbInfos); + $adapter = new $className($dbInfos); if ($connect) { $adapter->getConnection(); diff --git a/core/Db/BatchInsert.php b/core/Db/BatchInsert.php index 1e818c207309a1220ecacf625e1c16a9eaa850b6..f6e7a223c78c9f5d001a096a35db250871715a77 100644 --- a/core/Db/BatchInsert.php +++ b/core/Db/BatchInsert.php @@ -33,13 +33,12 @@ class BatchInsert public static function tableInsertBatchIterate($tableName, $fields, $values, $ignoreWhenDuplicate = true) { $fieldList = '(' . join(',', $fields) . ')'; - $ignore = $ignoreWhenDuplicate ? 'IGNORE' : ''; + $ignore = $ignoreWhenDuplicate ? 'IGNORE' : ''; foreach ($values as $row) { - $query = "INSERT $ignore - INTO " . $tableName . " - $fieldList - VALUES (" . Common::getSqlStringFieldsArray($row) . ")"; + $query = "INSERT $ignore INTO " . $tableName . " + $fieldList + VALUES (" . Common::getSqlStringFieldsArray($row) . ")"; Db::query($query, $row); } } @@ -172,7 +171,8 @@ class BatchInsert * @see http://bugs.php.net/bug.php?id=54158 */ $openBaseDir = ini_get('open_basedir'); - $safeMode = ini_get('safe_mode'); + $safeMode = ini_get('safe_mode'); + if (empty($openBaseDir) && empty($safeMode)) { // php 5.x - LOAD DATA LOCAL INFILE is disabled if open_basedir restrictions or safe_mode enabled $keywords[] = 'LOCAL '; @@ -199,9 +199,11 @@ class BatchInsert $exceptions[] = "\n Try #" . (count($exceptions) + 1) . ': ' . $queryStart . ": " . $message; } } + if (count($exceptions)) { throw new Exception(implode(",", $exceptions)); } + return false; } @@ -218,8 +220,8 @@ class BatchInsert // Set up CSV delimiters, quotes, etc $delim = $fileSpec['delim']; $quote = $fileSpec['quote']; - $eol = $fileSpec['eol']; - $null = $fileSpec['null']; + $eol = $fileSpec['eol']; + $null = $fileSpec['null']; $escapespecial_cb = $fileSpec['escapespecial_cb']; $fp = @fopen($filePath, 'wb'); @@ -246,6 +248,7 @@ class BatchInsert throw new Exception('Error writing to the tmp file ' . $filePath); } } + fclose($fp); @chmod($filePath, 0777); diff --git a/core/Db/Schema.php b/core/Db/Schema.php index c7823cf70a0a8f23b5ba674686f40c55bf15f084..796e71dca5b384175572ac445afb75e421779a32 100644 --- a/core/Db/Schema.php +++ b/core/Db/Schema.php @@ -116,11 +116,11 @@ class Schema extends Singleton */ private function loadSchema() { - $config = Config::getInstance(); - $dbInfos = $config->database; + $config = Config::getInstance(); + $dbInfos = $config->database; $schemaName = trim($dbInfos['schema']); - $className = self::getSchemaClassName($schemaName); + $className = self::getSchemaClassName($schemaName); $this->schema = new $className(); } @@ -134,6 +134,7 @@ class Schema extends Singleton if ($this->schema === null) { $this->loadSchema(); } + return $this->schema; } diff --git a/core/Db/Schema/Mysql.php b/core/Db/Schema/Mysql.php index 45ff75b68517fbe78264487b3b9a0fa8fbeade90..eb8c1f71f13cc82b720ff43a7e5241f47e3de8fe 100644 --- a/core/Db/Schema/Mysql.php +++ b/core/Db/Schema/Mysql.php @@ -20,6 +20,8 @@ use Piwik\DbHelper; */ class Mysql implements SchemaInterface { + private $tablesInstalled = null; + /** * Is this MySQL storage engine available? * @@ -58,216 +60,213 @@ class Mysql implements SchemaInterface $prefixTables = $this->getTablePrefix(); $tables = array( - 'user' => "CREATE TABLE {$prefixTables}user ( - login VARCHAR(100) NOT NULL, - password CHAR(32) NOT NULL, - alias VARCHAR(45) NOT NULL, - email VARCHAR(100) NOT NULL, - token_auth CHAR(32) NOT NULL, - superuser_access TINYINT(2) unsigned NOT NULL DEFAULT '0', - date_registered TIMESTAMP NULL, - PRIMARY KEY(login), - UNIQUE KEY uniq_keytoken(token_auth) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'access' => "CREATE TABLE {$prefixTables}access ( - login VARCHAR(100) NOT NULL, - idsite INTEGER UNSIGNED NOT NULL, - access VARCHAR(10) NULL, - PRIMARY KEY(login, idsite) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'site' => "CREATE TABLE {$prefixTables}site ( - idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - name VARCHAR(90) NOT NULL, - main_url VARCHAR(255) NOT NULL, - ts_created TIMESTAMP NULL, - ecommerce TINYINT DEFAULT 0, - sitesearch TINYINT DEFAULT 1, - sitesearch_keyword_parameters TEXT NOT NULL, - sitesearch_category_parameters TEXT NOT NULL, - timezone VARCHAR( 50 ) NOT NULL, - currency CHAR( 3 ) NOT NULL, - excluded_ips TEXT NOT NULL, - excluded_parameters TEXT NOT NULL, - excluded_user_agents TEXT NOT NULL, - `group` VARCHAR(250) NOT NULL, - `type` VARCHAR(255) NOT NULL, - keep_url_fragment TINYINT NOT NULL DEFAULT 0, - PRIMARY KEY(idsite) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'site_url' => "CREATE TABLE {$prefixTables}site_url ( - idsite INTEGER(10) UNSIGNED NOT NULL, - url VARCHAR(255) NOT NULL, - PRIMARY KEY(idsite, url) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'goal' => " CREATE TABLE `{$prefixTables}goal` ( - `idsite` int(11) NOT NULL, - `idgoal` int(11) NOT NULL, - `name` varchar(50) NOT NULL, - `match_attribute` varchar(20) NOT NULL, - `pattern` varchar(255) NOT NULL, - `pattern_type` varchar(10) NOT NULL, - `case_sensitive` tinyint(4) NOT NULL, - `allow_multiple` tinyint(4) NOT NULL, - `revenue` float NOT NULL, - `deleted` tinyint(4) NOT NULL default '0', - PRIMARY KEY (`idsite`,`idgoal`) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'logger_message' => "CREATE TABLE {$prefixTables}logger_message ( - idlogger_message INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + 'user' => "CREATE TABLE {$prefixTables}user ( + login VARCHAR(100) NOT NULL, + password CHAR(32) NOT NULL, + alias VARCHAR(45) NOT NULL, + email VARCHAR(100) NOT NULL, + token_auth CHAR(32) NOT NULL, + superuser_access TINYINT(2) unsigned NOT NULL DEFAULT '0', + date_registered TIMESTAMP NULL, + PRIMARY KEY(login), + UNIQUE KEY uniq_keytoken(token_auth) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'access' => "CREATE TABLE {$prefixTables}access ( + login VARCHAR(100) NOT NULL, + idsite INTEGER UNSIGNED NOT NULL, + access VARCHAR(10) NULL, + PRIMARY KEY(login, idsite) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'site' => "CREATE TABLE {$prefixTables}site ( + idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + name VARCHAR(90) NOT NULL, + main_url VARCHAR(255) NOT NULL, + ts_created TIMESTAMP NULL, + ecommerce TINYINT DEFAULT 0, + sitesearch TINYINT DEFAULT 1, + sitesearch_keyword_parameters TEXT NOT NULL, + sitesearch_category_parameters TEXT NOT NULL, + timezone VARCHAR( 50 ) NOT NULL, + currency CHAR( 3 ) NOT NULL, + excluded_ips TEXT NOT NULL, + excluded_parameters TEXT NOT NULL, + excluded_user_agents TEXT NOT NULL, + `group` VARCHAR(250) NOT NULL, + `type` VARCHAR(255) NOT NULL, + keep_url_fragment TINYINT NOT NULL DEFAULT 0, + PRIMARY KEY(idsite) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'site_url' => "CREATE TABLE {$prefixTables}site_url ( + idsite INTEGER(10) UNSIGNED NOT NULL, + url VARCHAR(255) NOT NULL, + PRIMARY KEY(idsite, url) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'goal' => "CREATE TABLE `{$prefixTables}goal` ( + `idsite` int(11) NOT NULL, + `idgoal` int(11) NOT NULL, + `name` varchar(50) NOT NULL, + `match_attribute` varchar(20) NOT NULL, + `pattern` varchar(255) NOT NULL, + `pattern_type` varchar(10) NOT NULL, + `case_sensitive` tinyint(4) NOT NULL, + `allow_multiple` tinyint(4) NOT NULL, + `revenue` float NOT NULL, + `deleted` tinyint(4) NOT NULL default '0', + PRIMARY KEY (`idsite`,`idgoal`) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'logger_message' => "CREATE TABLE {$prefixTables}logger_message ( + idlogger_message INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, tag VARCHAR(50) NULL, - timestamp TIMESTAMP NULL, + timestamp TIMESTAMP NULL, level VARCHAR(16) NULL, - message TEXT NULL, - PRIMARY KEY(idlogger_message) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'log_action' => "CREATE TABLE {$prefixTables}log_action ( - idaction INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - name TEXT, - hash INTEGER(10) UNSIGNED NOT NULL, - type TINYINT UNSIGNED NULL, - url_prefix TINYINT(2) NULL, - PRIMARY KEY(idaction), - INDEX index_type_hash (type, hash) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'log_visit' => "CREATE TABLE {$prefixTables}log_visit ( - idvisit INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - idsite INTEGER(10) UNSIGNED NOT NULL, - idvisitor BINARY(8) NOT NULL, - visit_last_action_time DATETIME NOT NULL, - config_id BINARY(8) NOT NULL, - location_ip VARBINARY(16) NOT NULL, - PRIMARY KEY(idvisit), - INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time), - INDEX index_idsite_datetime (idsite, visit_last_action_time), - INDEX index_idsite_idvisitor (idsite, idvisitor) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + message TEXT NULL, + PRIMARY KEY(idlogger_message) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'log_action' => "CREATE TABLE {$prefixTables}log_action ( + idaction INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + name TEXT, + hash INTEGER(10) UNSIGNED NOT NULL, + type TINYINT UNSIGNED NULL, + url_prefix TINYINT(2) NULL, + PRIMARY KEY(idaction), + INDEX index_type_hash (type, hash) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'log_visit' => "CREATE TABLE {$prefixTables}log_visit ( + idvisit INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + idsite INTEGER(10) UNSIGNED NOT NULL, + idvisitor BINARY(8) NOT NULL, + visit_last_action_time DATETIME NOT NULL, + config_id BINARY(8) NOT NULL, + location_ip VARBINARY(16) NOT NULL, + PRIMARY KEY(idvisit), + INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time), + INDEX index_idsite_datetime (idsite, visit_last_action_time), + INDEX index_idsite_idvisitor (idsite, idvisitor) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", 'log_conversion_item' => "CREATE TABLE `{$prefixTables}log_conversion_item` ( - idsite int(10) UNSIGNED NOT NULL, - idvisitor BINARY(8) NOT NULL, - server_time DATETIME NOT NULL, - idvisit INTEGER(10) UNSIGNED NOT NULL, - idorder varchar(100) NOT NULL, - - idaction_sku INTEGER(10) UNSIGNED NOT NULL, - idaction_name INTEGER(10) UNSIGNED NOT NULL, - idaction_category INTEGER(10) UNSIGNED NOT NULL, - idaction_category2 INTEGER(10) UNSIGNED NOT NULL, - idaction_category3 INTEGER(10) UNSIGNED NOT NULL, - idaction_category4 INTEGER(10) UNSIGNED NOT NULL, - idaction_category5 INTEGER(10) UNSIGNED NOT NULL, - price FLOAT NOT NULL, - quantity INTEGER(10) UNSIGNED NOT NULL, - deleted TINYINT(1) UNSIGNED NOT NULL, - - PRIMARY KEY(idvisit, idorder, idaction_sku), - INDEX index_idsite_servertime ( idsite, server_time ) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'log_conversion' => "CREATE TABLE `{$prefixTables}log_conversion` ( - idvisit int(10) unsigned NOT NULL, - idsite int(10) unsigned NOT NULL, - idvisitor BINARY(8) NOT NULL, - server_time datetime NOT NULL, - idaction_url int(11) default NULL, - idlink_va int(11) default NULL, - idgoal int(10) NOT NULL, - buster int unsigned NOT NULL, - idorder varchar(100) default NULL, - items SMALLINT UNSIGNED DEFAULT NULL, - url text NOT NULL, - - PRIMARY KEY (idvisit, idgoal, buster), - UNIQUE KEY unique_idsite_idorder (idsite, idorder), - INDEX index_idsite_datetime ( idsite, server_time ) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + idsite int(10) UNSIGNED NOT NULL, + idvisitor BINARY(8) NOT NULL, + server_time DATETIME NOT NULL, + idvisit INTEGER(10) UNSIGNED NOT NULL, + idorder varchar(100) NOT NULL, + idaction_sku INTEGER(10) UNSIGNED NOT NULL, + idaction_name INTEGER(10) UNSIGNED NOT NULL, + idaction_category INTEGER(10) UNSIGNED NOT NULL, + idaction_category2 INTEGER(10) UNSIGNED NOT NULL, + idaction_category3 INTEGER(10) UNSIGNED NOT NULL, + idaction_category4 INTEGER(10) UNSIGNED NOT NULL, + idaction_category5 INTEGER(10) UNSIGNED NOT NULL, + price FLOAT NOT NULL, + quantity INTEGER(10) UNSIGNED NOT NULL, + deleted TINYINT(1) UNSIGNED NOT NULL, + PRIMARY KEY(idvisit, idorder, idaction_sku), + INDEX index_idsite_servertime ( idsite, server_time ) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'log_conversion' => "CREATE TABLE `{$prefixTables}log_conversion` ( + idvisit int(10) unsigned NOT NULL, + idsite int(10) unsigned NOT NULL, + idvisitor BINARY(8) NOT NULL, + server_time datetime NOT NULL, + idaction_url int(11) default NULL, + idlink_va int(11) default NULL, + idgoal int(10) NOT NULL, + buster int unsigned NOT NULL, + idorder varchar(100) default NULL, + items SMALLINT UNSIGNED DEFAULT NULL, + url text NOT NULL, + PRIMARY KEY (idvisit, idgoal, buster), + UNIQUE KEY unique_idsite_idorder (idsite, idorder), + INDEX index_idsite_datetime ( idsite, server_time ) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", 'log_link_visit_action' => "CREATE TABLE {$prefixTables}log_link_visit_action ( - idlink_va INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT, - idsite int(10) UNSIGNED NOT NULL, - idvisitor BINARY(8) NOT NULL, - idvisit INTEGER(10) UNSIGNED NOT NULL, - idaction_url_ref INTEGER(10) UNSIGNED NULL DEFAULT 0, - idaction_name_ref INTEGER(10) UNSIGNED NOT NULL, - - custom_float FLOAT NULL DEFAULT NULL, - PRIMARY KEY(idlink_va), - INDEX index_idvisit(idvisit) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'log_profiling' => "CREATE TABLE {$prefixTables}log_profiling ( - query TEXT NOT NULL, - count INTEGER UNSIGNED NULL, - sum_time_ms FLOAT NULL, - UNIQUE KEY query(query(100)) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'option' => "CREATE TABLE `{$prefixTables}option` ( - option_name VARCHAR( 255 ) NOT NULL, - option_value LONGTEXT NOT NULL, - autoload TINYINT NOT NULL DEFAULT '1', - PRIMARY KEY ( option_name ), - INDEX autoload( autoload ) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'session' => "CREATE TABLE {$prefixTables}session ( - id VARCHAR( 255 ) NOT NULL, - modified INTEGER, - lifetime INTEGER, - data TEXT, - PRIMARY KEY ( id ) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'archive_numeric' => "CREATE TABLE {$prefixTables}archive_numeric ( - idarchive INTEGER UNSIGNED NOT NULL, - name VARCHAR(255) NOT NULL, - idsite INTEGER UNSIGNED NULL, - date1 DATE NULL, - date2 DATE NULL, - period TINYINT UNSIGNED NULL, - ts_archived DATETIME NULL, - value DOUBLE NULL, - PRIMARY KEY(idarchive, name), - INDEX index_idsite_dates_period(idsite, date1, date2, period, ts_archived), - INDEX index_period_archived(period, ts_archived) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", - - 'archive_blob' => "CREATE TABLE {$prefixTables}archive_blob ( - idarchive INTEGER UNSIGNED NOT NULL, - name VARCHAR(255) NOT NULL, - idsite INTEGER UNSIGNED NULL, - date1 DATE NULL, - date2 DATE NULL, - period TINYINT UNSIGNED NULL, - ts_archived DATETIME NULL, - value MEDIUMBLOB NULL, - PRIMARY KEY(idarchive, name), - INDEX index_period_archived(period, ts_archived) - ) ENGINE=$engine DEFAULT CHARSET=utf8 - ", + idlink_va INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT, + idsite int(10) UNSIGNED NOT NULL, + idvisitor BINARY(8) NOT NULL, + idvisit INTEGER(10) UNSIGNED NOT NULL, + idaction_url_ref INTEGER(10) UNSIGNED NULL DEFAULT 0, + idaction_name_ref INTEGER(10) UNSIGNED NOT NULL, + custom_float FLOAT NULL DEFAULT NULL, + PRIMARY KEY(idlink_va), + INDEX index_idvisit(idvisit) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'log_profiling' => "CREATE TABLE {$prefixTables}log_profiling ( + query TEXT NOT NULL, + count INTEGER UNSIGNED NULL, + sum_time_ms FLOAT NULL, + UNIQUE KEY query(query(100)) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'option' => "CREATE TABLE `{$prefixTables}option` ( + option_name VARCHAR( 255 ) NOT NULL, + option_value LONGTEXT NOT NULL, + autoload TINYINT NOT NULL DEFAULT '1', + PRIMARY KEY ( option_name ), + INDEX autoload( autoload ) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'session' => "CREATE TABLE {$prefixTables}session ( + id VARCHAR( 255 ) NOT NULL, + modified INTEGER, + lifetime INTEGER, + data TEXT, + PRIMARY KEY ( id ) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'archive_numeric' => "CREATE TABLE {$prefixTables}archive_numeric ( + idarchive INTEGER UNSIGNED NOT NULL, + name VARCHAR(255) NOT NULL, + idsite INTEGER UNSIGNED NULL, + date1 DATE NULL, + date2 DATE NULL, + period TINYINT UNSIGNED NULL, + ts_archived DATETIME NULL, + value DOUBLE NULL, + PRIMARY KEY(idarchive, name), + INDEX index_idsite_dates_period(idsite, date1, date2, period, ts_archived), + INDEX index_period_archived(period, ts_archived) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", + + 'archive_blob' => "CREATE TABLE {$prefixTables}archive_blob ( + idarchive INTEGER UNSIGNED NOT NULL, + name VARCHAR(255) NOT NULL, + idsite INTEGER UNSIGNED NULL, + date1 DATE NULL, + date2 DATE NULL, + period TINYINT UNSIGNED NULL, + ts_archived DATETIME NULL, + value MEDIUMBLOB NULL, + PRIMARY KEY(idarchive, name), + INDEX index_period_archived(period, ts_archived) + ) ENGINE=$engine DEFAULT CHARSET=utf8 + ", ); + return $tables; } @@ -297,17 +296,17 @@ class Mysql implements SchemaInterface */ public function getTablesNames() { - $aTables = array_keys($this->getTablesCreateSql()); + $aTables = array_keys($this->getTablesCreateSql()); $prefixTables = $this->getTablePrefix(); + $return = array(); foreach ($aTables as $table) { $return[] = $prefixTables . $table; } + return $return; } - private $tablesInstalled = null; - /** * Get list of installed columns in a table * @@ -317,7 +316,7 @@ class Mysql implements SchemaInterface */ public function getTableColumns($tableName) { - $db = Db::get(); + $db = $this->getDb(); $allColumns = $db->fetchAll("SHOW COLUMNS FROM . $tableName"); @@ -340,7 +339,8 @@ class Mysql implements SchemaInterface if (is_null($this->tablesInstalled) || $forceReload === true ) { - $db = Db::get(); + + $db = $this->getDb(); $prefixTables = $this->getTablePrefixEscaped(); $allTables = $this->getAllExistingTables($prefixTables); @@ -353,12 +353,13 @@ class Mysql implements SchemaInterface // at this point we have the static list of core tables, but let's add the monthly archive tables $allArchiveNumeric = $db->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "archive_numeric%'"); - $allArchiveBlob = $db->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "archive_blob%'"); + $allArchiveBlob = $db->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "archive_blob%'"); $allTablesReallyInstalled = array_merge($tablesInstalled, $allArchiveNumeric, $allArchiveBlob); $this->tablesInstalled = $allTablesReallyInstalled; } + return $this->tablesInstalled; } @@ -382,6 +383,7 @@ class Mysql implements SchemaInterface if (is_null($dbName)) { $dbName = $this->getDbName(); } + Db::exec("CREATE DATABASE IF NOT EXISTS " . $dbName . " DEFAULT CHARACTER SET utf8"); } @@ -405,7 +407,7 @@ class Mysql implements SchemaInterface } catch (Exception $e) { // mysql code error 1050:table already exists // see bug #153 https://github.com/piwik/piwik/issues/153 - if (!Db::get()->isErrNo($e, '1050')) { + if (!$this->getDb()->isErrNo($e, '1050')) { throw $e; } } @@ -425,7 +427,7 @@ class Mysql implements SchemaInterface */ public function createTables() { - $db = Db::get(); + $db = $this->getDb(); $prefixTables = $this->getTablePrefix(); $tablesAlreadyInstalled = $this->getTablesInstalled(); @@ -448,9 +450,9 @@ class Mysql implements SchemaInterface { // The anonymous user is the user that is assigned by default // note that the token_auth value is anonymous, which is assigned by default as well in the Login plugin - $db = Db::get(); + $db = $this->getDb(); $db->query("INSERT IGNORE INTO " . Common::prefixTable("user") . " - VALUES ( 'anonymous', '', 'anonymous', 'anonymous@example.org', 'anonymous', 0, '" . Date::factory('now')->getDatetime() . "' );"); + VALUES ( 'anonymous', '', 'anonymous', 'anonymous@example.org', 'anonymous', 0, '" . Date::factory('now')->getDatetime() . "' );"); } /** @@ -466,7 +468,7 @@ class Mysql implements SchemaInterface private function getTablePrefix() { - $dbInfos = Db::getDatabaseConfig(); + $dbInfos = Db::getDatabaseConfig(); $prefixTables = $dbInfos['tables_prefix']; return $prefixTables; @@ -475,10 +477,15 @@ class Mysql implements SchemaInterface private function getTableEngine() { $dbInfos = Db::getDatabaseConfig(); - $engine = $dbInfos['type']; + $engine = $dbInfos['type']; + return $engine; } + private function getDb(){ + return Db::get(); + } + private function getDbName() { $dbInfos = Db::getDatabaseConfig(); diff --git a/core/Segment.php b/core/Segment.php index a0952f3bd700b885be4eb00fdebbdb45028a088b..420946794da028e8890c7906058a82399dd74d75 100644 --- a/core/Segment.php +++ b/core/Segment.php @@ -100,7 +100,7 @@ class Segment // As a preventive measure, we restrict the filter size to a safe limit $string = substr($string, 0, self::SEGMENT_TRUNCATE_LIMIT); - $this->string = $string; + $this->string = $string; $this->idSites = $idSites; $segment = new SegmentExpression($string); $this->segment = $segment; @@ -118,6 +118,7 @@ class Segment $expression[SegmentExpression::INDEX_OPERAND] = $cleanedExpression; $cleanedExpressions[] = $expression; } + $segment->setSubExpressionsAfterCleanup($cleanedExpressions); } diff --git a/core/SegmentExpression.php b/core/SegmentExpression.php index 682aa201d0189d7dcb47938de796c4a9d8fac38f..cbd2a3eb9e1ff2e8acdf8a7e24c49380e91b5204 100644 --- a/core/SegmentExpression.php +++ b/core/SegmentExpression.php @@ -84,7 +84,7 @@ class SegmentExpression } $leftMember = $matches[1]; - $operation = $matches[2]; + $operation = $matches[2]; $valueRightMember = urldecode($matches[3]); // is null / is not null @@ -138,6 +138,7 @@ class SegmentExpression if ($operand[1] !== null) { $this->valuesBind[] = $operand[1]; } + $operand = $operand[0]; $sqlSubExpressions[] = array( self::INDEX_BOOL_OPERATOR => $operator, @@ -161,9 +162,9 @@ class SegmentExpression */ protected function getSqlMatchFromDefinition($def, &$availableTables) { - $field = $def[0]; + $field = $def[0]; $matchType = $def[1]; - $value = $def[2]; + $value = $def[2]; $alsoMatchNULLValues = false; switch ($matchType) { @@ -188,22 +189,22 @@ class SegmentExpression break; case self::MATCH_CONTAINS: $sqlMatch = 'LIKE'; - $value = '%' . $this->escapeLikeString($value) . '%'; + $value = '%' . $this->escapeLikeString($value) . '%'; break; case self::MATCH_DOES_NOT_CONTAIN: $sqlMatch = 'NOT LIKE'; - $value = '%' . $this->escapeLikeString($value) . '%'; + $value = '%' . $this->escapeLikeString($value) . '%'; $alsoMatchNULLValues = true; break; case self::MATCH_IS_NOT_NULL_NOR_EMPTY: $sqlMatch = 'IS NOT NULL AND (' . $field . ' <> \'\' OR ' . $field . ' = 0)'; - $value = null; + $value = null; break; case self::MATCH_IS_NULL_OR_EMPTY: $sqlMatch = 'IS NULL OR ' . $field . ' = \'\' '; - $value = null; + $value = null; break; case self::MATCH_ACTIONS_CONTAINS: @@ -212,7 +213,7 @@ class SegmentExpression // it can be used internally to inject sub-expressions into the query. // see Segment::getCleanedExpression() $sqlMatch = 'IN (' . $value['SQL'] . ')'; - $value = $this->escapeLikeString($value['bind']); + $value = $this->escapeLikeString($value['bind']); break; default: throw new Exception("Filter contains the match type '" . $matchType . "' which is not supported"); diff --git a/core/Tracker/Action.php b/core/Tracker/Action.php index 5fba5a2862e3bdf128e2eaf790213fc9b219b19e..6df6f64cfcc3f0c570488686e4e147afc05b6a4b 100644 --- a/core/Tracker/Action.php +++ b/core/Tracker/Action.php @@ -351,9 +351,8 @@ abstract class Action $visitAction = array_merge($visitAction, $customVariables); - $this->recordAction($visitAction); + $this->idLinkVisitAction = $this->getModel()->createAction($visitAction); - $this->idLinkVisitAction = Tracker::getDatabase()->lastInsertId(); $visitAction['idlink_va'] = $this->idLinkVisitAction; Common::printDebug("Inserted new action:"); @@ -384,14 +383,9 @@ abstract class Action return true; } - private function recordAction($visitAction) + private function getModel() { - $fields = implode(", ", array_keys($visitAction)); - $bind = array_values($visitAction); - $values = Common::getSqlStringFieldsArray($visitAction); - - $sql = "INSERT INTO " . Common::prefixTable('log_link_visit_action') . " ($fields) VALUES ($values)"; - Tracker::getDatabase()->query($sql, $bind); + return new Model(); } /** diff --git a/core/Tracker/GoalManager.php b/core/Tracker/GoalManager.php index 7823b88cc2270d38ed183471078bb766483ee73b..69639fd1c63b7b42ac875dc19633ab8aefe58631 100644 --- a/core/Tracker/GoalManager.php +++ b/core/Tracker/GoalManager.php @@ -330,12 +330,7 @@ class GoalManager $conversion['items'] = $itemsCount; if ($this->isThereExistingCartInVisit) { - $updateWhere = array( - 'idvisit' => $visitInformation['idvisit'], - 'idgoal' => self::IDGOAL_CART, - 'buster' => 0, - ); - $recorded = $this->updateExistingConversion($conversion, $updateWhere); + $recorded = $this->getModel()->updateConversion($visitInformation['idvisit'], self::IDGOAL_CART, $conversion); } else { $recorded = $this->insertNewConversion($conversion, $visitInformation); } @@ -398,20 +393,8 @@ class GoalManager $itemInCartBySku[$item[0]] = $item; } - // Select all items currently in the Cart if any - $sql = "SELECT idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder as idorder_original_value - FROM " . Common::prefixTable('log_conversion_item') . " - WHERE idvisit = ? AND (idorder = ? OR idorder = ?)"; - - $bind = array($goal['idvisit'], - isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, - self::ITEM_IDORDER_ABANDONED_CART - ); - - $itemsInDb = Tracker::getDatabase()->fetchAll($sql, $bind); + $itemsInDb = $this->getModel()->getAllItemsCurrentlyInTheCart($goal, self::ITEM_IDORDER_ABANDONED_CART); - Common::printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind, true)); - Common::printDebug($itemsInDb); // Look at which items need to be deleted, which need to be added or updated, based on the SKU $skuFoundInDb = $itemsToUpdate = array(); @@ -601,20 +584,16 @@ class GoalManager foreach ($itemsToUpdate as $item) { $newRow = $this->getItemRowEnriched($goal, $item); Common::printDebug($newRow); - $updateParts = $sqlBind = array(); - foreach ($newRow as $name => $value) { - $updateParts[] = $name . " = ?"; - $sqlBind[] = $value; - } - $sql = 'UPDATE ' . Common::prefixTable('log_conversion_item') . " SET " . implode($updateParts, ', ') . " - WHERE idvisit = ? AND idorder = ? AND idaction_sku = ?"; - $sqlBind[] = $newRow['idvisit']; - $sqlBind[] = $item['idorder_original_value']; - $sqlBind[] = $newRow['idaction_sku']; - Tracker::getDatabase()->query($sql, $sqlBind); + + $this->getModel()->updateEcommerceItem($item['idorder_original_value'], $newRow); } } + private function getModel() + { + return new Model(); + } + /** * Inserts in the cart in the DB the new items * that were not previously in the cart @@ -633,26 +612,13 @@ class GoalManager Common::printDebug("Ecommerce items that are added to the cart/order"); Common::printDebug($itemsToInsert); - $sql = "INSERT INTO " . Common::prefixTable('log_conversion_item') . " - (idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, - idorder, idsite, idvisitor, server_time, idvisit) - VALUES "; - $i = 0; - $bind = array(); + $items = array(); foreach ($itemsToInsert as $item) { - if ($i > 0) { - $sql .= ','; - } - $newRow = array_values($this->getItemRowEnriched($goal, $item)); - $sql .= " ( " . Common::getSqlStringFieldsArray($newRow) . " ) "; - $i++; - $bind = array_merge($bind, $newRow); + $items[] = $this->getItemRowEnriched($goal, $item); } - Tracker::getDatabase()->query($sql, $bind); - Common::printDebug($sql); - Common::printDebug($bind); + $this->getModel()->createEcommerceItems($items); } protected function getItemRowEnriched($goal, $item) @@ -757,15 +723,9 @@ class GoalManager $newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']); Common::printDebug($newGoalDebug); - $fields = implode(", ", array_keys($conversion)); - $bindFields = Common::getSqlStringFieldsArray($conversion); + $wasInserted = $this->getModel()->createConversion($conversion); - $sql = 'INSERT IGNORE INTO ' . Common::prefixTable('log_conversion') . " ($fields) VALUES ($bindFields) "; - $bind = array_values($conversion); - $result = Tracker::getDatabase()->query($sql, $bind); - - // If a record was inserted, we return true - return Tracker::getDatabase()->rowCount($result) > 0; + return $wasInserted; } /** @@ -788,35 +748,6 @@ class GoalManager ); } - protected function updateExistingConversion($newGoal, $updateWhere) - { - $updateParts = $sqlBind = $updateWhereParts = array(); - - foreach ($newGoal as $name => $value) { - $updateParts[] = $name . " = ?"; - $sqlBind[] = $value; - } - - foreach ($updateWhere as $name => $value) { - $updateWhereParts[] = $name . " = ?"; - $sqlBind[] = $value; - } - - $table = Common::prefixTable('log_conversion'); - $parts = implode($updateParts, ', '); - $sql = 'UPDATE ' . $table . " SET " . $parts . " WHERE " . implode($updateWhereParts, ' AND '); - - try { - Tracker::getDatabase()->query($sql, $sqlBind); - } catch(Exception $e){ - Common::printDebug("There was an error while updating the Conversion: " . $e->getMessage()); - - return false; - } - - return true; - } - /** * @param $goal * @param $pattern_type diff --git a/core/Tracker/Model.php b/core/Tracker/Model.php new file mode 100644 index 0000000000000000000000000000000000000000..090cd8be596384c162eae09d14d8d03802928697 --- /dev/null +++ b/core/Tracker/Model.php @@ -0,0 +1,383 @@ +<?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\Tracker; + +use Exception; +use PDOStatement; +use Piwik\Common; +use Piwik\Tracker; +use Piwik\Tracker\Db\DbException; + +class Model +{ + + public function createAction($visitAction) + { + $fields = implode(", ", array_keys($visitAction)); + $values = Common::getSqlStringFieldsArray($visitAction); + $table = Common::prefixTable('log_link_visit_action'); + + $sql = "INSERT INTO $table ($fields) VALUES ($values)"; + $bind = array_values($visitAction); + + $db = $this->getDb(); + $db->query($sql, $bind); + + $id = $db->lastInsertId(); + + return $id; + } + + public function createConversion($conversion) + { + $fields = implode(", ", array_keys($conversion)); + $bindFields = Common::getSqlStringFieldsArray($conversion); + $table = Common::prefixTable('log_conversion'); + + $sql = "INSERT IGNORE INTO $table ($fields) VALUES ($bindFields) "; + $bind = array_values($conversion); + + $db = $this->getDb(); + $result = $db->query($sql, $bind); + + // If a record was inserted, we return true + return $db->rowCount($result) > 0; + } + + public function updateConversion($idVisit, $idGoal, $newConversion) + { + $updateWhere = array( + 'idvisit' => $idVisit, + 'idgoal' => $idGoal, + 'buster' => 0, + ); + + $updateParts = $sqlBind = $updateWhereParts = array(); + + foreach ($newConversion as $name => $value) { + $updateParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + + foreach ($updateWhere as $name => $value) { + $updateWhereParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + + $parts = implode($updateParts, ', '); + $table = Common::prefixTable('log_conversion'); + + $sql = "UPDATE $table SET $parts WHERE " . implode($updateWhereParts, ' AND '); + + try { + $this->getDb()->query($sql, $sqlBind); + } catch(Exception $e){ + Common::printDebug("There was an error while updating the Conversion: " . $e->getMessage()); + + return false; + } + + return true; + } + + + /** + * Loads the Ecommerce items from the request and records them in the DB + * + * @param array $goal + * @param int $defaultIdOrder + * @throws Exception + * @return array + */ + public function getAllItemsCurrentlyInTheCart($goal, $defaultIdOrder) + { + $sql = "SELECT idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder as idorder_original_value + FROM " . Common::prefixTable('log_conversion_item') . " + WHERE idvisit = ? AND (idorder = ? OR idorder = ?)"; + + $bind = array( + $goal['idvisit'], + isset($goal['idorder']) ? $goal['idorder'] : $defaultIdOrder, + $defaultIdOrder + ); + + $itemsInDb = $this->getDb()->fetchAll($sql, $bind); + + Common::printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind, true)); + Common::printDebug($itemsInDb); + + return $itemsInDb; + } + + public function createEcommerceItems($ecommerceItems) + { + $sql = "INSERT INTO " . Common::prefixTable('log_conversion_item'); + $i = 0; + $bind = array(); + + foreach ($ecommerceItems as $item) { + + if ($i === 0) { + $fields = implode(', ', array_keys($item)); + $sql .= ' (' . $fields . ') VALUES '; + } elseif ($i > 0) { + $sql .= ','; + } + + $newRow = array_values($item); + $sql .= " ( " . Common::getSqlStringFieldsArray($newRow) . " ) "; + $bind = array_merge($bind, $newRow); + $i++; + } + + $this->getDb()->query($sql, $bind); + + Common::printDebug($sql); + Common::printDebug($bind); + } + + public function createNewIdAction($name, $type, $urlPrefix) + { + $table = Common::prefixTable('log_action'); + $sql = "INSERT INTO $table (name, hash, type, url_prefix) VALUES (?,CRC32(?),?,?)"; + + $db = $this->getDb(); + $db->query($sql, array($name, $name, $type, $urlPrefix)); + + $actionId = $db->lastInsertId(); + + return $actionId; + } + + private function getSqlSelectActionId() + { + $sql = "SELECT idaction, type, name FROM " . Common::prefixTable('log_action') + . " WHERE ( hash = CRC32(?) AND name = ? AND type = ? ) "; + + return $sql; + } + + public function getIdActionMatchingNameAndType($name, $type) + { + $sql = $this->getSqlSelectActionId(); + $bind = array($name, $name, $type); + + $idAction = $this->getDb()->fetchOne($sql, $bind); + + return $idAction; + } + + public function getIdsAction($actionsNameAndType) + { + $sql = $this->getSqlSelectActionId(); + $bind = array(); + + $i = 0; + foreach ($actionsNameAndType as $actionNameType) { + $name = $actionNameType['name']; + + if (empty($name)) { + continue; + } + + if ($i > 0) { + $sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) "; + } + + $bind[] = $name; + $bind[] = $name; + $bind[] = $actionNameType['type']; + $i++; + } + + // Case URL & Title are empty + if (empty($bind)) { + return false; + } + + $actionIds = $this->getDb()->fetchAll($sql, $bind); + + return $actionIds; + } + + public function updateEcommerceItem($originalIdOrder, $newItem) + { + $updateParts = $sqlBind = array(); + foreach ($newItem as $name => $value) { + $updateParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + + $parts = implode($updateParts, ', '); + $table = Common::prefixTable('log_conversion_item'); + + $sql = "UPDATE $table SET $parts WHERE idvisit = ? AND idorder = ? AND idaction_sku = ?"; + + $sqlBind[] = $newItem['idvisit']; + $sqlBind[] = $originalIdOrder; + $sqlBind[] = $newItem['idaction_sku']; + + $this->getDb()->query($sql, $sqlBind); + } + + public function createVisit($visit) + { + $fields = array_keys($visit); + $fields = implode(", ", $fields); + $values = Common::getSqlStringFieldsArray($visit); + $table = Common::prefixTable('log_visit'); + + $sql = "INSERT INTO $table ($fields) VALUES ($values)"; + $bind = array_values($visit); + + $db = $this->getDb(); + $db->query($sql, $bind); + + return $db->lastInsertId(); + } + + public function updateVisit($idSite, $idVisit, $valuesToUpdate) + { + list($updateParts, $sqlBind) = $this->visitFieldsToQuery($valuesToUpdate); + + $parts = implode($updateParts, ', '); + $table = Common::prefixTable('log_visit'); + + $sqlQuery = "UPDATE $table SET $parts WHERE idsite = ? AND idvisit = ?"; + + $sqlBind[] = $idSite; + $sqlBind[] = $idVisit; + + $db = $this->getDb(); + $result = $db->query($sqlQuery, $sqlBind); + $wasInserted = $db->rowCount($result) != 0; + + if (!$wasInserted) { + Common::printDebug("Visitor with this idvisit wasn't found in the DB."); + Common::printDebug("$sqlQuery --- "); + Common::printDebug($sqlBind); + } + + return $wasInserted; + } + + public function findVisitor($idSite, $configId, $idVisitor, $fieldsToRead, $numCustomVarsToRead, $shouldMatchOneFieldOnly, $isVisitorIdToLookup, $timeLookBack, $timeLookAhead) + { + $selectCustomVariables = ''; + + if ($numCustomVarsToRead) { + for ($index = 1; $index <= $numCustomVarsToRead; $index++) { + $selectCustomVariables .= ', custom_var_k' . $index . ', custom_var_v' . $index; + } + } + + $selectFields = implode(', ', $fieldsToRead); + + $select = "SELECT $selectFields $selectCustomVariables "; + $from = "FROM " . Common::prefixTable('log_visit'); + + // Two use cases: + // 1) there is no visitor ID so we try to match only on config_id (heuristics) + // Possible causes of no visitor ID: no browser cookie support, direct Tracking API request without visitor ID passed, + // importing server access logs with import_logs.py, etc. + // In this case we use config_id heuristics to try find the visitor in tahhhe past. There is a risk to assign + // this page view to the wrong visitor, but this is better than creating artificial visits. + // 2) there is a visitor ID and we trust it (config setting trust_visitors_cookies, OR it was set using &cid= in tracking API), + // and in these cases, we force to look up this visitor id + $whereCommon = "visit_last_action_time >= ? AND visit_last_action_time <= ? AND idsite = ?"; + $bindSql = array( + $timeLookBack, + $timeLookAhead, + $idSite + ); + + if ($shouldMatchOneFieldOnly) { + if ($isVisitorIdToLookup) { + $whereCommon .= ' AND idvisitor = ?'; + $bindSql[] = $idVisitor; + } else { + $whereCommon .= ' AND config_id = ?'; + $bindSql[] = $configId; + } + + $sql = "$select $from + WHERE " . $whereCommon . " + ORDER BY visit_last_action_time DESC + LIMIT 1"; + } // We have a config_id AND a visitor_id. We match on either of these. + // Why do we also match on config_id? + // we do not trust the visitor ID only. Indeed, some browsers, or browser addons, + // cause the visitor id from the 1st party cookie to be different on each page view! + // It is not acceptable to create a new visit every time such browser does a page view, + // so we also backup by searching for matching config_id. + // We use a UNION here so that each sql query uses its own INDEX + else { + // will use INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time) + $where = ' AND config_id = ? AND user_id IS NULL '; + $bindSql[] = $configId; + $sqlConfigId = "$select , + 0 as priority + $from + WHERE $whereCommon $where + ORDER BY visit_last_action_time DESC + LIMIT 1 + "; + // will use INDEX index_idsite_idvisitor (idsite, idvisitor) + $bindSql[] = $timeLookBack; + $bindSql[] = $timeLookAhead; + $bindSql[] = $idSite; + $where = ' AND idvisitor = ?'; + $bindSql[] = $idVisitor; + $sqlVisitorId = "$select , + 1 as priority + $from + WHERE $whereCommon $where + ORDER BY visit_last_action_time DESC + LIMIT 1 + "; + + // We join both queries and favor the one matching the visitor_id if it did match + $sql = " ( $sqlConfigId ) + UNION + ( $sqlVisitorId ) + ORDER BY priority DESC + LIMIT 1"; + } + + $visitRow = $this->getDb()->fetch($sql, $bindSql); + + return $visitRow; + } + + private function visitFieldsToQuery($valuesToUpdate) + { + $updateParts = array(); + $sqlBind = array(); + + foreach ($valuesToUpdate as $name => $value) { + // Case where bind parameters don't work + if ($value === $name . ' + 1') { + //$name = 'visit_total_events' + //$value = 'visit_total_events + 1'; + $updateParts[] = " $name = $value "; + } else { + $updateParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + } + + return array($updateParts, $sqlBind); + } + + private function getDb() + { + return Tracker::getDatabase(); + } + +} diff --git a/core/Tracker/TableLogAction.php b/core/Tracker/TableLogAction.php index 7ae99a535ededb1ef375c586eb3350b35f5281e7..4f4849a3526baff23bfd4a1d6981c0061da3a230 100644 --- a/core/Tracker/TableLogAction.php +++ b/core/Tracker/TableLogAction.php @@ -51,20 +51,6 @@ class TableLogAction return $queriedIds; } - /** - * @param $name - * @param $type - * @return string - */ - private static function getIdActionMatchingNameAndType($name, $type) - { - $sql = TableLogAction::getSqlSelectActionId(); - $bind = array($name, $name, $type); - $idAction = \Piwik\Db::fetchOne($sql, $bind); - - return $idAction; - } - /** * @param $matchType * @param $actionType @@ -95,62 +81,39 @@ class TableLogAction return $sql; } - private static function getSqlSelectActionId() - { - $sql = "SELECT idaction, type, name - FROM " . Common::prefixTable('log_action') - . " WHERE " - . " ( hash = CRC32(?) AND name = ? AND type = ? ) "; - - return $sql; - } - private static function insertNewIdsAction($actionsNameAndType, $fieldNamesToInsert) { - $sql = "INSERT INTO " . Common::prefixTable('log_action') . - "( name, hash, type, url_prefix ) VALUES (?,CRC32(?),?,?)"; // Then, we insert all new actions in the lookup table $inserted = array(); foreach ($fieldNamesToInsert as $fieldName) { list($name, $type, $urlPrefix) = $actionsNameAndType[$fieldName]; - Tracker::getDatabase()->query($sql, array($name, $name, $type, $urlPrefix)); - $actionId = Tracker::getDatabase()->lastInsertId(); - - $inserted[$fieldName] = $actionId; + $actionId = self::getModel()->createNewIdAction($name, $type, $urlPrefix); Common::printDebug("Recorded a new action (" . Action::getTypeAsString($type) . ") in the lookup table: " . $name . " (idaction = " . $actionId . ")"); + + $inserted[$fieldName] = $actionId; } return $inserted; } - private static function queryIdsAction($actionsNameAndType) + private static function getModel() { - $sql = TableLogAction::getSqlSelectActionId(); - $bind = array(); + return new Model(); + } - $i = 0; + private static function queryIdsAction($actionsNameAndType) + { + $toQuery = array(); foreach ($actionsNameAndType as &$actionNameType) { list($name, $type, $urlPrefix) = $actionNameType; - if (empty($name)) { - continue; - } - if ($i > 0) { - $sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) "; - } - $bind[] = $name; - $bind[] = $name; - $bind[] = $type; - $i++; - } - // Case URL & Title are empty - if (empty($bind)) { - return false; + $toQuery[] = array('name' => $name, 'type' => $type); } - $actionIds = Tracker::getDatabase()->fetchAll($sql, $bind); + $actionIds = self::getModel()->getIdsAction($toQuery); + return $actionIds; } @@ -214,7 +177,7 @@ class TableLogAction if ($matchType == SegmentExpression::MATCH_EQUAL || $matchType == SegmentExpression::MATCH_NOT_EQUAL ) { - $idAction = self::getIdActionMatchingNameAndType($valueToMatch, $actionType); + $idAction = self::getModel()->getIdActionMatchingNameAndType($valueToMatch, $actionType); // if the action is not found, we hack -100 to ensure it tries to match against an integer // otherwise binding idaction_name to "false" returns some rows for some reasons (in case &segment=pageTitle==Větrnásssssss) if (empty($idAction)) { diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php index f01f34bc199539eb135dfb666099db41e33074ed..0ce45c3659de613e5fa195c4b579123dd5287a5d 100644 --- a/core/Tracker/Visit.php +++ b/core/Tracker/Visit.php @@ -327,13 +327,18 @@ class Visit implements VisitInterface $this->printVisitorInformation(); - $idVisit = $this->insertNewVisit( $this->visitorInfo ); + $idVisit = $this->insertNewVisit($this->visitorInfo); $this->setVisitorColumn($visitor, 'idvisit', $idVisit); $this->setVisitorColumn($visitor, 'visit_first_action_time', $this->request->getCurrentTimestamp()); $this->setVisitorColumn($visitor, 'visit_last_action_time', $this->request->getCurrentTimestamp()); } + private function getModel() + { + return new Model(); + } + /** * Returns visitor cookie * @@ -426,43 +431,25 @@ class Visit implements VisitInterface return str_replace('www.', '', $hostLower); } - /** - * @return mixed - */ - protected function insertNewVisit($visit) - { - $fields = array_keys($visit); - $fields = implode(", ", $fields); - $values = Common::getSqlStringFieldsArray($visit); - - $sql = "INSERT INTO " . Common::prefixTable('log_visit') . " ($fields) VALUES ($values)"; - $bind = array_values($visit); - Tracker::getDatabase()->query($sql, $bind); - - return Tracker::getDatabase()->lastInsertId(); - } - /** * @param $valuesToUpdate * @throws VisitorNotFoundInDb */ protected function updateExistingVisit($valuesToUpdate) { - list($sqlQuery, $sqlBind) = $this->getUpdateExistingVisitQuery($valuesToUpdate); + $idSite = $this->request->getIdSite(); + $idVisit = (int) $this->visitorInfo['idvisit']; - $result = Tracker::getDatabase()->query($sqlQuery, $sqlBind); + $wasInserted = $this->getModel()->updateVisit($idSite, $idVisit, $valuesToUpdate); // Debug output if (isset($valuesToUpdate['idvisitor'])) { $valuesToUpdate['idvisitor'] = bin2hex($valuesToUpdate['idvisitor']); } - Common::printDebug('Updating existing visit: ' . var_export($valuesToUpdate, true)); - - if (Tracker::getDatabase()->rowCount($result) == 0) { - Common::printDebug("Visitor with this idvisit wasn't found in the DB."); - Common::printDebug("$sqlQuery --- "); - Common::printDebug($sqlBind); + if ($wasInserted) { + Common::printDebug('Updated existing visit: ' . var_export($valuesToUpdate, true)); + } else { throw new VisitorNotFoundInDb( "The visitor with idvisitor=" . bin2hex($this->visitorInfo['idvisitor']) . " and idvisit=" . $this->visitorInfo['idvisit'] . " wasn't found in the DB, we fallback to a new visitor"); @@ -569,42 +556,6 @@ class Visit implements VisitInterface return $dimensions; } - private function fieldsToQuery($valuesToUpdate) - { - $updateParts = array(); - $sqlBind = array(); - - foreach ($valuesToUpdate as $name => $value) { - // Case where bind parameters don't work - if ($value === $name . ' + 1') { - //$name = 'visit_total_events' - //$value = 'visit_total_events + 1'; - $updateParts[] = " $name = $value "; - } else { - $updateParts[] = $name . " = ?"; - $sqlBind[] = $value; - } - } - - return array($updateParts, $sqlBind); - } - - private function getUpdateExistingVisitQuery($valuesToUpdate) - { - $sqlQuery = "UPDATE " . Common::prefixTable('log_visit') . " SET %s WHERE idsite = ? AND idvisit = ?"; - - // build sql query - list($updateParts, $sqlBind) = $this->fieldsToQuery($valuesToUpdate); - - $idSite = $this->request->getIdSite(); - $idVisit = (int) $this->visitorInfo['idvisit']; - - $sqlQuery = sprintf($sqlQuery, implode($updateParts, ', ')); - array_push($sqlBind, $idSite, $idVisit); - - return array($sqlQuery, $sqlBind); - } - private function getVisitStandardLength() { return Config::getInstance()->Tracker['visit_standard_length']; @@ -634,4 +585,9 @@ class Visit implements VisitInterface } return $valuesToUpdate; } + + protected function insertNewVisit($visit) + { + return $this->getModel()->createVisit($visit); + } } diff --git a/core/Tracker/Visitor.php b/core/Tracker/Visitor.php index ec5dd6d2796fe86c79e7bb7f5d2551045a966942..afec61849e74f42c0d445f151c5e368291d9ac19 100644 --- a/core/Tracker/Visitor.php +++ b/core/Tracker/Visitor.php @@ -25,9 +25,9 @@ class Visitor public function __construct(Request $request, $configId, $visitorInfo = array(), $customVariables = null) { $this->request = $request; + $this->configId = $configId; $this->visitorInfo = $visitorInfo; $this->customVariables = $customVariables; - $this->configId = $configId; } /** @@ -41,120 +41,35 @@ class Visitor { $this->setIsVisitorKnown(false); - $configId = $this->configId; - + $configId = $this->configId; + $idSite = $this->request->getIdSite(); $idVisitor = $this->request->getVisitorId(); + $isVisitorIdToLookup = !empty($idVisitor); if ($isVisitorIdToLookup) { $this->visitorInfo['idvisitor'] = $idVisitor; - Common::printDebug("Matching visitors with: visitorId=" . bin2hex($this->visitorInfo['idvisitor']) . " OR configId=" . bin2hex($configId)); + Common::printDebug("Matching visitors with: visitorId=" . bin2hex($idVisitor) . " OR configId=" . bin2hex($configId)); } else { Common::printDebug("Visitor doesn't have the piwik cookie..."); } - $selectCustomVariables = ''; - // No custom var were found in the request, so let's copy the previous one in a potential conversion later + $numCustomVarsToRead = 0; if (!$this->customVariables) { - $maxCustomVariables = CustomVariables::getMaxCustomVariables(); - - for ($index = 1; $index <= $maxCustomVariables; $index++) { - $selectCustomVariables .= ', custom_var_k' . $index . ', custom_var_v' . $index; - } + // No custom var were found in the request, so let's copy the previous one in a potential conversion later + $numCustomVarsToRead = CustomVariables::getMaxCustomVariables(); } - $persistedVisitAttributes = self::getVisitFieldsPersist(); - array_unshift($persistedVisitAttributes, 'visit_first_action_time'); - array_unshift($persistedVisitAttributes, 'visit_last_action_time'); - $persistedVisitAttributes = array_unique($persistedVisitAttributes); - - $selectFields = implode(", ", $persistedVisitAttributes); - - $select = "SELECT - $selectFields - $selectCustomVariables - "; - $from = "FROM " . Common::prefixTable('log_visit'); - + $persistedVisitAttributes = $this->getVisitFieldsPersist(); + $shouldMatchOneFieldOnly = $this->shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup); list($timeLookBack, $timeLookAhead) = $this->getWindowLookupThisVisit(); - $shouldMatchOneFieldOnly = $this->shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup); - - // Two use cases: - // 1) there is no visitor ID so we try to match only on config_id (heuristics) - // Possible causes of no visitor ID: no browser cookie support, direct Tracking API request without visitor ID passed, - // importing server access logs with import_logs.py, etc. - // In this case we use config_id heuristics to try find the visitor in tahhhe past. There is a risk to assign - // this page view to the wrong visitor, but this is better than creating artificial visits. - // 2) there is a visitor ID and we trust it (config setting trust_visitors_cookies, OR it was set using &cid= in tracking API), - // and in these cases, we force to look up this visitor id - $whereCommon = "visit_last_action_time >= ? AND visit_last_action_time <= ? AND idsite = ?"; - $bindSql = array( - $timeLookBack, - $timeLookAhead, - $this->request->getIdSite() - ); - - if ($shouldMatchOneFieldOnly) { - if ($isVisitorIdToLookup) { - $whereCommon .= ' AND idvisitor = ?'; - $bindSql[] = $this->visitorInfo['idvisitor']; - } else { - $whereCommon .= ' AND config_id = ?'; - $bindSql[] = $configId; - } - - $sql = "$select - $from - WHERE " . $whereCommon . " - ORDER BY visit_last_action_time DESC - LIMIT 1"; - } // We have a config_id AND a visitor_id. We match on either of these. - // Why do we also match on config_id? - // we do not trust the visitor ID only. Indeed, some browsers, or browser addons, - // cause the visitor id from the 1st party cookie to be different on each page view! - // It is not acceptable to create a new visit every time such browser does a page view, - // so we also backup by searching for matching config_id. - // We use a UNION here so that each sql query uses its own INDEX - else { - // will use INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time) - $where = ' AND config_id = ? - AND user_id IS NULL '; - $bindSql[] = $configId; - $sqlConfigId = "$select , - 0 as priority - $from - WHERE $whereCommon $where - ORDER BY visit_last_action_time DESC - LIMIT 1 - "; - // will use INDEX index_idsite_idvisitor (idsite, idvisitor) - $bindSql[] = $timeLookBack; - $bindSql[] = $timeLookAhead; - $bindSql[] = $this->request->getIdSite(); - $where = ' AND idvisitor = ?'; - $bindSql[] = $this->visitorInfo['idvisitor']; - $sqlVisitorId = "$select , - 1 as priority - $from - WHERE $whereCommon $where - ORDER BY visit_last_action_time DESC - LIMIT 1 - "; - - // We join both queries and favor the one matching the visitor_id if it did match - $sql = " ( $sqlConfigId ) - UNION - ( $sqlVisitorId ) - ORDER BY priority DESC - LIMIT 1"; - } - - $visitRow = Tracker::getDatabase()->fetch($sql, $bindSql); + $model = $this->getModel(); + $visitRow = $model->findVisitor($idSite, $configId, $idVisitor, $persistedVisitAttributes, $numCustomVarsToRead, $shouldMatchOneFieldOnly, $isVisitorIdToLookup, $timeLookBack, $timeLookAhead); $isNewVisitForced = $this->request->getParam('new_visit'); $isNewVisitForced = !empty($isNewVisitForced); - $enforceNewVisit = $isNewVisitForced || Config::getInstance()->Debug['tracker_always_new_visitor']; + $enforceNewVisit = $isNewVisitForced || Config::getInstance()->Debug['tracker_always_new_visitor']; if (!$enforceNewVisit && $visitRow @@ -162,17 +77,16 @@ class Visitor ) { // These values will be used throughout the request - foreach($persistedVisitAttributes as $field) { + foreach ($persistedVisitAttributes as $field) { $this->visitorInfo[$field] = $visitRow[$field]; } - $this->visitorInfo['visit_last_action_time'] = strtotime($visitRow['visit_last_action_time']); + $this->visitorInfo['visit_last_action_time'] = strtotime($visitRow['visit_last_action_time']); $this->visitorInfo['visit_first_action_time'] = strtotime($visitRow['visit_first_action_time']); // Custom Variables copied from Visit in potential later conversion - if (!empty($selectCustomVariables)) { - $maxCustomVariables = CustomVariables::getMaxCustomVariables(); - for ($i = 1; $i <= $maxCustomVariables; $i++) { + if (!empty($numCustomVarsToRead)) { + for ($i = 1; $i <= $numCustomVarsToRead; $i++) { if (isset($visitRow['custom_var_k' . $i]) && strlen($visitRow['custom_var_k' . $i]) ) { @@ -193,7 +107,6 @@ class Visitor last action = " . date("r", $this->visitorInfo['visit_last_action_time']) . ", first action = " . date("r", $this->visitorInfo['visit_first_action_time']) . ", visit_goal_buyer' = " . $this->visitorInfo['visit_goal_buyer'] . ")"); - //Common::printDebug($this->visitorInfo); } else { Common::printDebug("The visitor was not matched with an existing visitor..."); } @@ -228,7 +141,8 @@ class Visitor protected function shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup) { $isForcedUserIdMustMatch = (false !== $this->request->getForcedUserId()); - if($isForcedUserIdMustMatch) { + + if ($isForcedUserIdMustMatch) { // if &iud was set, we must try and match both idvisitor and config_id return false; } @@ -236,18 +150,18 @@ class Visitor // This setting would be enabled for Intranet websites, to ensure that visitors using all the same computer config, same IP // are not counted as 1 visitor. In this case, we want to enforce and trust the visitor ID from the cookie. $trustCookiesOnly = Config::getInstance()->Tracker['trust_visitors_cookies']; - if($isVisitorIdToLookup && $trustCookiesOnly) { + if ($isVisitorIdToLookup && $trustCookiesOnly) { return true; } // If a &cid= was set, we force to select this visitor (or create a new one) $isForcedVisitorIdMustMatch = ($this->request->getForcedVisitorId() != null); - if($isForcedVisitorIdMustMatch) { + if ($isForcedVisitorIdMustMatch) { return true; } - if( !$isVisitorIdToLookup ) { + if (!$isVisitorIdToLookup ) { return true; } @@ -257,7 +171,7 @@ class Visitor /** * @return array */ - public static function getVisitFieldsPersist() + private function getVisitFieldsPersist() { $fields = array( 'idvisitor', @@ -307,6 +221,10 @@ class Visitor */ Piwik::postEvent('Tracker.getVisitFieldsToPersist', array(&$fields)); + array_unshift($fields, 'visit_first_action_time'); + array_unshift($fields, 'visit_last_action_time'); + $fields = array_unique($fields); + return $fields; } @@ -343,4 +261,9 @@ class Visitor { return $this->visitorKnown = $isVisitorKnown; } + + private function getModel() + { + return new Model(); + } } diff --git a/plugins/Goals/API.php b/plugins/Goals/API.php index ffc5bfe46777697132a7bab79eef9e8112344b7d..c76054aab9974eb1f9a694496b7c62bb35d389d7 100644 --- a/plugins/Goals/API.php +++ b/plugins/Goals/API.php @@ -190,8 +190,8 @@ class API extends \Piwik\Plugin\API Piwik::checkUserHasAdminAccess($idSite); $this->getModel()->deleteGoal($idSite, $idGoal); + $this->getModel()->deleteGoalConversions($idSite, $idGoal); - Db::deleteAllRows(Common::prefixTable("log_conversion"), "WHERE idgoal = ? AND idsite = ?", "idvisit", 100000, array($idGoal, $idSite)); Cache::regenerateCacheWebsiteAttributes($idSite); } diff --git a/plugins/Goals/Model.php b/plugins/Goals/Model.php index 93a9e9fd409d603c53f2a3dba75d0a45639a3fee..2b35f7947d0d40abc4956847baffe8088a945864 100644 --- a/plugins/Goals/Model.php +++ b/plugins/Goals/Model.php @@ -56,6 +56,14 @@ class Model $db->update($this->table, $goal, "idsite = '$idSite' AND idgoal = '$idGoal'"); } + // actually this should be in a log_conversion model + public function deleteGoalConversions($idSite, $idGoal) + { + $table = Common::prefixTable("log_conversion"); + + Db::deleteAllRows($table, "WHERE idgoal = ? AND idsite = ?", "idvisit", 100000, array($idGoal, $idSite)); + } + public function getActiveGoals($idSite) { $idSite = array_map('intval', $idSite); diff --git a/plugins/SitesManager/API.php b/plugins/SitesManager/API.php index 0d8a85c11c8b7e600a8383f8aa108cb90367541e..1bb4d4b304b6e8769fc9a4d433f6c0508ce88717 100644 --- a/plugins/SitesManager/API.php +++ b/plugins/SitesManager/API.php @@ -517,8 +517,6 @@ class API extends \Piwik\Plugin\API } $this->checkValidCurrency($currency); - $db = Db::get(); - $url = $urls[0]; $urls = array_slice($urls, 1); @@ -528,31 +526,31 @@ class API extends \Piwik\Plugin\API ); $bind['excluded_ips'] = $this->checkAndReturnExcludedIps($excludedIps); - $bind['excluded_parameters'] = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters); + $bind['excluded_parameters'] = $this->checkAndReturnCommaSeparatedStringList($excludedQueryParameters); $bind['excluded_user_agents'] = $this->checkAndReturnCommaSeparatedStringList($excludedUserAgents); - $bind['keep_url_fragment'] = $keepURLFragments; - $bind['timezone'] = $timezone; - $bind['currency'] = $currency; - $bind['ecommerce'] = (int)$ecommerce; + $bind['keep_url_fragment'] = $keepURLFragments; + $bind['timezone'] = $timezone; + $bind['currency'] = $currency; + $bind['ecommerce'] = (int)$ecommerce; $bind['sitesearch'] = $siteSearch; - $bind['sitesearch_keyword_parameters'] = $searchKeywordParameters; + $bind['sitesearch_keyword_parameters'] = $searchKeywordParameters; $bind['sitesearch_category_parameters'] = $searchCategoryParameters; - $bind['ts_created'] = !is_null($startDate) - ? Date::factory($startDate)->getDatetime() - : Date::now()->getDatetime(); + + if (is_null($startDate)) { + $bind['ts_created'] = Date::now()->getDatetime(); + } else { + $bind['ts_created'] = Date::factory($startDate)->getDatetime(); + } + $bind['type'] = $this->checkAndReturnType($type); - if (!empty($group) - && Piwik::hasUserSuperUserAccess() - ) { + if (!empty($group) && Piwik::hasUserSuperUserAccess()) { $bind['group'] = trim($group); } else { $bind['group'] = ""; } - $db->insert(Common::prefixTable("site"), $bind); - - $idSite = $db->lastInsertId(); + $idSite = $this->getModel()->createSite($bind); $this->insertSiteUrls($idSite, $urls); @@ -567,7 +565,7 @@ class API extends \Piwik\Plugin\API */ Piwik::postEvent('SitesManager.addSite.end', array($idSite)); - return (int)$idSite; + return (int) $idSite; } private function postUpdateWebsite($idSite) diff --git a/plugins/SitesManager/Model.php b/plugins/SitesManager/Model.php index 7aa779b530a6a90cb60464aa3c26075101ae85bf..971844c40b0a634d69ca87dafcf4850802101ef2 100644 --- a/plugins/SitesManager/Model.php +++ b/plugins/SitesManager/Model.php @@ -24,6 +24,16 @@ class Model $this->table = Common::prefixTable(self::$rawPrefix); } + public function createSite($site) + { + $db = $this->getDb(); + $db->insert($this->table, $site); + + $idSite = $db->lastInsertId(); + + return $idSite; + } + /** * Returns all websites belonging to the specified group * @param string $group Group name @@ -202,8 +212,8 @@ class Model */ public function getSiteFromId($idSite) { - $site = Db::get()->fetchRow("SELECT * FROM " . $this->table . " - WHERE idsite = ?", $idSite); + $site = $this->getDb()->fetchRow("SELECT * FROM " . $this->table . " + WHERE idsite = ?", $idSite); return $site; } diff --git a/plugins/UsersManager/Model.php b/plugins/UsersManager/Model.php index 3c39516cb5b817d0733d538fd9dc39a6c3250dbd..3d62ca61fd8e43265503e4331c279e81b0a493d1 100644 --- a/plugins/UsersManager/Model.php +++ b/plugins/UsersManager/Model.php @@ -50,9 +50,9 @@ class Model $bind = $userLogins; } - $users = Db::get()->fetchAll("SELECT * FROM " . $this->table . " - $where - ORDER BY login ASC", $bind); + $users = $this->getDb()->fetchAll("SELECT * FROM " . $this->table . " + $where + ORDER BY login ASC", $bind); return $users; } @@ -64,7 +64,7 @@ class Model */ public function getUsersLogin() { - $users = Db::get()->fetchAll("SELECT login FROM " . $this->table . " ORDER BY login ASC"); + $users = $this->getDb()->fetchAll("SELECT login FROM " . $this->table . " ORDER BY login ASC"); $return = array(); foreach ($users as $login) { diff --git a/plugins/UsersManager/UsersManager.php b/plugins/UsersManager/UsersManager.php index b2c4b2e8f3e2e8bb7a39d953f0366a186bf730d9..cdc383d041711750d509e03087c8299459ae5c84 100644 --- a/plugins/UsersManager/UsersManager.php +++ b/plugins/UsersManager/UsersManager.php @@ -34,7 +34,8 @@ class UsersManager extends \Piwik\Plugin 'SitesManager.deleteSite.end' => 'deleteSite', 'Tracker.Cache.getSiteAttributes' => 'recordAdminUsersInCache', 'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys', - 'Platform.initialized' => 'onPlatformInitialized' + 'Platform.initialized' => 'onPlatformInitialized', + 'CronArchive.getTokenAuth' => 'getCronArchiveTokenAuth' ); } @@ -62,9 +63,22 @@ class UsersManager extends \Piwik\Plugin foreach ($users as $user) { $tokens[] = $user['token_auth']; } + $attributes['admin_token_auth'] = $tokens; } + public function getCronArchiveTokenAuth(&$token) + { + $model = new Model(); + $superUsers = $model->getUsersHavingSuperUserAccess(); + + if (!empty($superUsers)) { + $superUser = array_shift($superUsers); + + $token = $superUser['token_auth']; + } + } + /** * Delete user preferences associated with a particular site */ @@ -105,7 +119,9 @@ class UsersManager extends \Piwik\Plugin ) { return true; } + $l = strlen($input); + return $l >= self::PASSWORD_MIN_LENGTH && $l <= self::PASSWORD_MAX_LENGTH; }