diff options
author | matt <matt@59fd770c-687e-43c8-a1e3-f5a4ff64c105> | 2010-06-23 07:45:36 +0400 |
---|---|---|
committer | matt <matt@59fd770c-687e-43c8-a1e3-f5a4ff64c105> | 2010-06-23 07:45:36 +0400 |
commit | 7e1b5d6b762340cbff1bb928d15815980c7649a7 (patch) | |
tree | e07da179b9e1372866d2349777bd1cc6b4c9e8cf /core | |
parent | 999f46479294713104c962bfe7469e9b6e7a4bbf (diff) | |
parent | c98ea06f2cccec81c6ccce49162a583494e44d91 (diff) |
Diffstat (limited to 'core')
135 files changed, 6450 insertions, 3184 deletions
diff --git a/core/API/DataTableGenericFilter.php b/core/API/DataTableGenericFilter.php index d339a080e7..c8843059c6 100644 --- a/core/API/DataTableGenericFilter.php +++ b/core/API/DataTableGenericFilter.php @@ -58,7 +58,8 @@ class Piwik_API_DataTableGenericFilter 'filter_add_columns_when_show_all_columns' => array('integer') ), 'UpdateColumnsWhenShowAllGoals' => array( - 'filter_update_columns_when_show_all_goals' => array('integer') + 'filter_update_columns_when_show_all_goals' => array('integer'), + 'filter_only_display_idgoal' => array('integer', Piwik_DataTable_Filter_UpdateColumnsWhenShowAllGoals::GOALS_OVERVIEW), ), 'Sort' => array( 'filter_sort_column' => array('string'), @@ -84,15 +85,23 @@ class Piwik_API_DataTableGenericFilter if($datatable instanceof Piwik_DataTable_Array ) { $tables = $datatable->getArray(); + $filterWasApplied = false; foreach($tables as $table) { - $this->applyGenericFilters($table); + $filterWasApplied = $this->applyGenericFilters($table); + // if no generic filter was applied to the first table, we can return + // as no filter would be applied to any other dataTable + if(!$filterWasApplied) + { + return; + } } return; } $genericFilters = self::getGenericFiltersInformation(); + $filterApplied = false; foreach($genericFilters as $filterName => $parameters) { $filterParameters = array(); @@ -123,18 +132,14 @@ class Piwik_API_DataTableGenericFilter if(!$exceptionRaised) { - // a generic filter class name must follow this pattern - $class = "Piwik_DataTable_Filter_".$filterName; if($filterName == 'Limit') { $datatable->setRowsCountBeforeLimitFilter(); } - // build the set of parameters for the filter - $filterParameters = array_merge(array($datatable), $filterParameters); - // use Reflection to create a new instance of the filter, given parameters $filterParameters - $reflectionObj = new ReflectionClass($class); - $filter = $reflectionObj->newInstanceArgs($filterParameters); + $datatable->filter($filterName, $filterParameters); + $filterApplied = true; } } + return $filterApplied; } } diff --git a/core/API/DocumentationGenerator.php b/core/API/DocumentationGenerator.php index 8a1d71de9f..0d959171fb 100644 --- a/core/API/DocumentationGenerator.php +++ b/core/API/DocumentationGenerator.php @@ -96,7 +96,7 @@ class Piwik_API_DocumentationGenerator $str .= "</span>"; } $str .= '</small>'; - $str .= "\n<br>"; + $str .= "\n<br />"; } } return $str; diff --git a/core/API/Proxy.php b/core/API/Proxy.php index 26c11288c9..9bd12e25b4 100644 --- a/core/API/Proxy.php +++ b/core/API/Proxy.php @@ -12,6 +12,9 @@ /** * To differentiate between "no value" and default value of null + * + * @package Piwik + * @subpackage Piwik_API */ class Piwik_API_Proxy_NoDefaultValue {} @@ -215,8 +218,8 @@ class Piwik_API_Proxy } } } catch(Exception $e) { - throw new Exception("The required variable '$name' is not correct or has not been found in the API Request. Add the parameter '&$name=' (with a value) in the URL."); - } + throw new Exception(Piwik_TranslateException('General_ExceptionVariableNotFound', array($name))); + } $finalParameters[] = $requestValue; } return $finalParameters; @@ -232,7 +235,7 @@ class Piwik_API_Proxy $module = self::getModuleNameFromClassName($fileName); $path = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/API.php'; - if(Zend_Loader::isReadable($path)) + if(is_readable($path)) { require_once $path; // prefixed by PIWIK_INCLUDE_PATH } @@ -282,7 +285,7 @@ class Piwik_API_Proxy { if(!$this->isMethodAvailable($className, $methodName)) { - throw new Exception("The method '$methodName' does not exist or is not available in the module '".$className."'."); + throw new Exception(Piwik_TranslateException('General_ExceptionMethodNotFound', array($methodName,$className))); } } diff --git a/core/API/Request.php b/core/API/Request.php index b6d5f09f2a..c0ad14d5fc 100644 --- a/core/API/Request.php +++ b/core/API/Request.php @@ -87,7 +87,7 @@ class Piwik_API_Request $outputFormat = strtolower(Piwik_Common::getRequestVar('format', 'xml', 'string', $this->request)); // create the response - $response = new Piwik_API_ResponseBuilder($this->request, $outputFormat); + $response = new Piwik_API_ResponseBuilder($outputFormat, $this->request); try { // read parameters @@ -101,13 +101,7 @@ class Piwik_API_Request } $module = "Piwik_" . $module . "_API"; - // if a token_auth is specified in the API request, we load the right permissions - $token_auth = Piwik_Common::getRequestVar('token_auth', '', 'string', $this->request); - if($token_auth) - { - Piwik_PostEvent('API.Request.authenticate', $token_auth); - Zend_Registry::get('access')->reloadAccess(); - } + self::reloadAuthUsingTokenAuth($this->request); // call the method $returnedValue = Piwik_API_Proxy::getInstance()->call($module, $method, $this->request); @@ -120,6 +114,25 @@ class Piwik_API_Request } /** + * If the token_auth is found in the $request parameter, + * the current session will be authenticated using this token_auth. + * It will overwrite the previous Auth object. + * + * @param $request If null, uses the default request ($_GET) + * @return void + */ + static public function reloadAuthUsingTokenAuth($request = null) + { + // if a token_auth is specified in the API request, we load the right permissions + $token_auth = Piwik_Common::getRequestVar('token_auth', '', 'string', $request); + if($token_auth) + { + Piwik_PostEvent('API.Request.authenticate', $token_auth); + Zend_Registry::get('access')->reloadAccess(); + } + } + + /** * Returns array( $class, $method) from the given string $class.$method * * @return array diff --git a/core/API/ResponseBuilder.php b/core/API/ResponseBuilder.php index a632163108..1d94dc9888 100644 --- a/core/API/ResponseBuilder.php +++ b/core/API/ResponseBuilder.php @@ -19,7 +19,7 @@ class Piwik_API_ResponseBuilder private $request = null; private $outputFormat = null; - public function __construct($request, $outputFormat) + public function __construct($outputFormat, $request = array()) { $this->request = $request; $this->outputFormat = $outputFormat; @@ -49,11 +49,17 @@ class Piwik_API_ResponseBuilder * * @throws Exception If an object/resource is returned, if any of conversion fails, etc. * - * @param mixed The initial returned value, before post process + * @param mixed The initial returned value, before post process. If set to null, success response is returned. * @return mixed Usually a string, but can still be a PHP data structure if the format requested is 'original' */ - public function getResponse($value) + public function getResponse($value = null) { + // when null or void is returned from the api call, we handle it as a successful operation + if(!isset($value)) + { + return $this->handleSuccess(); + } + // If the returned value is an object DataTable we // apply the set of generic filters if asked in the URL // and we render the DataTable according to the format specified in the URL @@ -73,12 +79,6 @@ class Piwik_API_ResponseBuilder return $this->handleArray($value); } - // when null or void is returned from the api call, we handle it as a successful operation - if(!isset($value)) - { - return $this->handleSuccess(); - } - // original data structure requested, we return without process if( $this->outputFormat == 'original' ) { @@ -104,41 +104,30 @@ class Piwik_API_ResponseBuilder */ public function getResponseException(Exception $e) { - $message = htmlentities($e->getMessage(), ENT_COMPAT, "UTF-8"); - switch($this->outputFormat) + $format = strtolower($this->outputFormat); + + if( $format == 'original' ) { - case 'original': - throw $e; - break; - case 'xml': - @header("Content-Type: text/xml;charset=utf-8"); - $return = - "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" . - "<result>\n". - "\t<error message=\"".$message."\" />\n". - "</result>"; - break; - case 'json': - @header( "Content-type: application/json" ); - // we remove the \n from the resulting string as this is not allowed in json - $message = str_replace("\n","",$message); - $return = '{"result":"error", "message":"'.$message.'"}'; - break; - case 'php': - $return = array('result' => 'error', 'message' => $message); - if($this->caseRendererPHPSerialize()) - { - $return = serialize($return); - } - break; - case 'html': - $return = nl2br($message); - break; - default: - $return = 'Error: '.$message; - break; + throw $e; } - return $return; + + try + { + $renderer = Piwik_DataTable_Renderer::factory($format); + + } catch (Exception $e) { + + return "Error: " . $e->getMessage(); + } + + $renderer->setException($e); + + if($format == 'php') + { + $renderer->setSerialize($this->caseRendererPHPSerialize()); + } + + return $renderer->renderException(); } /** @@ -202,6 +191,10 @@ class Piwik_API_ResponseBuilder { $renderer->setTableId($this->request['method']); } + else if($format == 'csv') + { + $renderer->setConvertToUnicode( Piwik_Common::getRequestVar('convertToUnicode', true, 'int') ); + } return $renderer->render(); } @@ -226,7 +219,7 @@ class Piwik_API_ResponseBuilder "</result>"; break; case 'json': - @header( "Content-type: application/json" ); + @header( "Content-Type: application/json" ); $return = '{"result":"success", "message":"'.$message.'"}'; break; case 'php': @@ -238,7 +231,7 @@ class Piwik_API_ResponseBuilder break; case 'csv': - header("Content-type: application/vnd.ms-excel"); + header("Content-Type: application/vnd.ms-excel"); header("Content-Disposition: attachment; filename=piwik-report-export.csv"); $return = "message\n".$message; break; @@ -296,5 +289,5 @@ class Piwik_API_ResponseBuilder $dataTable->addRowsFromSimpleArray($array); return $this->getRenderedDataTable($dataTable); } - } + } } diff --git a/core/Access.php b/core/Access.php index 2485a7658b..4d08347b44 100644 --- a/core/Access.php +++ b/core/Access.php @@ -146,10 +146,7 @@ class Piwik_Access // we join with site in case there are rows in access for an idsite that doesn't exist anymore // (backward compatibility ; before we deleted the site without deleting rows in _access table) - $accessRaw = Piwik_FetchAll("SELECT access, t2.idsite - FROM ".Piwik::prefixTable('access'). " as t1 - JOIN ".Piwik::prefixTable('site')." as t2 USING (idsite) ". - " WHERE login = ?", $this->login); + $accessRaw = Piwik_FetchAll(self::getSqlAccessSite("access, t2.idsite"), $this->login); foreach($accessRaw as $access) { $this->idsitesByAccess[$access['access']][] = $access['idsite']; @@ -158,6 +155,20 @@ class Piwik_Access } /** + * Returns the SQL query joining sites and access table for a given login + * + * @param $select eg. "MIN(ts_created)" + * @return string SQL query + */ + static public function getSqlAccessSite($select) + { + return "SELECT ". $select ." + FROM ".Piwik_Common::prefixTable('access'). " as t1 + JOIN ".Piwik_Common::prefixTable('site')." as t2 USING (idsite) ". + " WHERE login = ?"; + } + + /** * Reload super user access * * @return bool @@ -165,7 +176,7 @@ class Piwik_Access protected function reloadAccessSuperUser() { $this->isSuperUser = true; - $this->idsitesByAccess['superuser'] = Piwik_SitesManager_API::getAllSitesId(); + $this->idsitesByAccess['superuser'] = Piwik_SitesManager_API::getInstance()->getAllSitesId(); return true; } @@ -173,9 +184,17 @@ class Piwik_Access * We bypass the normal auth method and give the current user Super User rights. * This should be very carefully used. */ - public function setSuperUser() + public function setSuperUser($bool = true) { - $this->reloadAccessSuperUser(); + if($bool) + { + $this->reloadAccessSuperUser(); + } + else + { + $this->isSuperUser = false; + $this->idsitesByAccess['superuser'] = array(); + } } /** @@ -223,8 +242,7 @@ class Piwik_Access $this->idsitesByAccess['superuser']) ); } - - + /** * Returns an array of ID sites for which the user has an ADMIN access. * @@ -261,7 +279,7 @@ class Piwik_Access { if($this->isSuperUser === false) { - throw new Piwik_Access_NoAccessException("You can't access this resource as it requires a 'superuser' access."); + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilege', array("'superuser'"))); } } @@ -272,10 +290,14 @@ class Piwik_Access */ public function checkUserHasSomeAdminAccess() { + if($this->isSuperUser()) + { + return; + } $idSitesAccessible = $this->getSitesIdWithAdminAccess(); if(count($idSitesAccessible) == 0) { - throw new Piwik_Access_NoAccessException("You can't access this resource as it requires an 'admin' access for at least one website."); + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin'))); } } @@ -289,7 +311,7 @@ class Piwik_Access $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); if(count($idSitesAccessible) == 0) { - throw new Piwik_Access_NoAccessException("You can't access this resource as it requires a 'view' access for at least one website."); + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAtLeastOneWebsite', array('view'))); } } @@ -315,7 +337,7 @@ class Piwik_Access { if(!in_array($idsite, $idSitesAccessible)) { - throw new Piwik_Access_NoAccessException("You can't access this resource as it requires an 'admin' access for the website id = $idsite."); + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'admin'", $idsite))); } } } @@ -344,7 +366,7 @@ class Piwik_Access { if(!in_array($idsite, $idSitesAccessible)) { - throw new Piwik_Access_NoAccessException("You can't access this resource as it requires a 'view' access for the website id = $idsite."); + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idsite))); } } } diff --git a/core/Archive.php b/core/Archive.php index bcc86237a3..17900f51a5 100644 --- a/core/Archive.php +++ b/core/Archive.php @@ -120,7 +120,7 @@ abstract class Piwik_Archive { if($idSite === 'all') { - $sites = Piwik_SitesManager_API::getSitesIdWithAtLeastViewAccess(); + $sites = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess(); } else { diff --git a/core/Archive/Array/IndexedByDate.php b/core/Archive/Array/IndexedByDate.php index 53a0369153..8629ef9672 100644 --- a/core/Archive/Array/IndexedByDate.php +++ b/core/Archive/Array/IndexedByDate.php @@ -25,7 +25,7 @@ class Piwik_Archive_Array_IndexedByDate extends Piwik_Archive_Array */ function __construct(Piwik_Site $oSite, $strPeriod, $strDate) { - $rangePeriod = new Piwik_Period_Range($strPeriod, $strDate); + $rangePeriod = new Piwik_Period_Range($strPeriod, $strDate, $oSite->getTimezone()); foreach($rangePeriod->getSubperiods() as $subPeriod) { $startDate = $subPeriod->getDateStart(); @@ -42,6 +42,13 @@ class Piwik_Archive_Array_IndexedByDate extends Piwik_Archive_Array return 'date'; } + /** + * Adds metadata information to the Piwik_DataTable_Array + * using the information given by the Archive + * + * @param Piwik_DataTable_Array $table + * @param Piwik_Archive $archive + */ protected function loadMetadata(Piwik_DataTable_Array $table, $archive) { $table->metadata[$archive->getPrettyDate()] = array( @@ -91,21 +98,22 @@ class Piwik_Archive_Array_IndexedByDate extends Piwik_Archive_Array $arrayValues = array(); foreach($queries as $table => $aIds) { - $inIds = implode(', ', $aIds); + $inIds = implode(', ', array_filter($aIds)); if(empty($inIds)) { // Probable timezone configuration error, i.e., mismatch between PHP and MySQL server. continue; } - $sql = "SELECT value, name, UNIX_TIMESTAMP(date1) as timestamp + $sql = "SELECT value, name, date1 as startDate FROM $table WHERE idarchive IN ( $inIds ) AND name IN ( $inNames )"; $values = $db->fetchAll($sql); foreach($values as $value) { - $arrayValues[$value['timestamp']][$value['name']] = (float)$value['value']; + $timestamp = Piwik_Date::factory($value['startDate'])->getTimestamp(); + $arrayValues[$timestamp][$value['name']] = (float)$value['value']; } } diff --git a/core/Archive/Array/IndexedBySite.php b/core/Archive/Array/IndexedBySite.php index 22cbd88dbd..a5d4cf6163 100644 --- a/core/Archive/Array/IndexedBySite.php +++ b/core/Archive/Array/IndexedBySite.php @@ -112,7 +112,7 @@ class Piwik_Archive_Array_IndexedBySite extends Piwik_Archive_Array } $archiveIds[] = $archive->getIdArchive(); } - return implode(', ', $archiveIds); + return implode(', ', array_filter($archiveIds)); } private function getNumericTableName() diff --git a/core/Archive/Single.php b/core/Archive/Single.php index 464ecf7f31..991ecd7a67 100644 --- a/core/Archive/Single.php +++ b/core/Archive/Single.php @@ -134,9 +134,12 @@ class Piwik_Archive_Single extends Piwik_Archive { if(!is_null($this->archiveProcessing)) { - return $this->archiveProcessing->getTimestampStartDate(); + $timestamp = $this->archiveProcessing->getTimestampStartDate(); + if(!empty($timestamp)) + { + return $timestamp; + } } - return $this->period->getDateStart()->getTimestamp(); } @@ -152,18 +155,20 @@ class Piwik_Archive_Single extends Piwik_Archive { $this->isThereSomeVisits = false; $this->alreadyChecked = true; - + $logMessage = "Preparing archive: "; // if the END of the period is BEFORE the website creation date // we already know there are no stats for this period // we add one day to make sure we don't miss the day of the website creation if( $this->period->getDateEnd()->addDay(2)->isEarlier( $this->site->getCreationDate() ) ) { - return; + Piwik::log("$logMessage skipped, archive is before the website was created."); + return; } // if the starting date is in the future we know there is no visit - if( $this->period->getDateStart()->subDay(1)->isLater( Piwik_Date::today() ) ) + if( $this->period->getDateStart()->subDay(2)->isLater( Piwik_Date::today() ) ) { + Piwik::log("$logMessage skipped, archive is after today."); return; } @@ -173,12 +178,17 @@ class Piwik_Archive_Single extends Piwik_Archive $archiveProcessing->setSite($this->site); $archiveProcessing->setPeriod($this->period); $idArchive = $archiveProcessing->loadArchive(); - if($idArchive === null) + if(empty($idArchive)) { + Piwik::log("$logMessage not archived yet, starting processing..."); $archiveJustProcessed = true; $archiveProcessing->launchArchiving(); $idArchive = $archiveProcessing->getIdArchive(); } + else + { + Piwik::log("$logMessage archive already processed [id = $idArchive]..."); + } $this->isThereSomeVisits = $archiveProcessing->isThereSomeVisits; $this->idArchive = $idArchive; $this->archiveProcessing = $archiveProcessing; @@ -442,7 +452,7 @@ class Piwik_Archive_Single extends Piwik_Archive if($data === false && $idSubTable !== null) { - throw new Exception("You are requesting a precise subTable but there is not such data in the Archive."); + throw new Exception(Piwik_TranslateException('General_ExceptionSubtableNotFoundInArchive')); } return $table; diff --git a/core/ArchiveProcessing.php b/core/ArchiveProcessing.php index 91eed568a4..23dca365cf 100644 --- a/core/ArchiveProcessing.php +++ b/core/ArchiveProcessing.php @@ -45,6 +45,14 @@ abstract class Piwik_ArchiveProcessing * @var int */ const DONE_ERROR = 2; + + /** + * Flag indicates the archive is over a period that is not finished, eg. the current day, current week, etc. + * Archives flagged will be regularly purged from the DB. + * + * @var int + */ + const DONE_OK_TEMPORARY = 3; /** * Idarchive in the DB for the requested archive @@ -96,11 +104,11 @@ abstract class Piwik_ArchiveProcessing protected $tableArchiveBlob; /** - * Maximum timestamp above which a given archive is considered out of date + * Minimum timestamp looked at for processed archives * * @var int */ - protected $maxTimestampArchive; + protected $minDatetimeArchiveProcessedUTC = false; /** * Compress blobs @@ -110,6 +118,13 @@ abstract class Piwik_ArchiveProcessing protected $compressBlob; /** + * Is the current archive temporary. ie. + * - today + * - current week / month / year + */ + protected $temporaryArchive; + + /** * Id of the current site * Can be accessed by plugins (that is why it's public) * @@ -121,7 +136,7 @@ abstract class Piwik_ArchiveProcessing * Period of the current archive * Can be accessed by plugins (that is why it's public) * - * @var Piwik_Period + * @var $period Piwik_Period */ public $period = null; @@ -134,14 +149,14 @@ abstract class Piwik_ArchiveProcessing public $site = null; /** - * Starting date @see Piwik_Date::toString() + * Starting datetime in UTC * * @var string */ - public $strDateStart; + public $startDatetimeUTC; /** - * Ending date @see Piwik_Date::toString() + * Ending date in UTC * * @var string */ @@ -183,6 +198,9 @@ abstract class Piwik_ArchiveProcessing */ public $isThereSomeVisits = false; + protected $startTimestampUTC; + protected $endTimestampUTC; + /** * Constructor */ @@ -218,61 +236,142 @@ abstract class Piwik_ArchiveProcessing return $process; } + const OPTION_TODAY_ARCHIVE_TTL = 'todayArchiveTimeToLive'; + const OPTION_BROWSER_TRIGGER_ARCHIVING = 'enableBrowserTriggerArchiving'; + + static public function setTodayArchiveTimeToLive($timeToLiveSeconds) + { + $timeToLiveSeconds = (int)$timeToLiveSeconds; + if($timeToLiveSeconds <= 0) + { + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidArchiveTimeToLive')); + } + Piwik_SetOption(self::OPTION_TODAY_ARCHIVE_TTL, $timeToLiveSeconds, $autoload = true); + } + + static public function getTodayArchiveTimeToLive() + { + $timeToLive = Piwik_GetOption(self::OPTION_TODAY_ARCHIVE_TTL); + if($timeToLive !== false) + { + return $timeToLive; + } + return Zend_Registry::get('config')->General->time_before_today_archive_considered_outdated; + } + + static public function setBrowserTriggerArchiving($enabled) + { + if(!is_bool($enabled)) + { + throw new Exception('Browser trigger archiving must be set to true or false.'); + } + Piwik_SetOption(self::OPTION_BROWSER_TRIGGER_ARCHIVING, (int)$enabled, $autoload = true); + + } + static public function isBrowserTriggerArchivingEnabled() + { + $browserArchivingEnabled = Piwik_GetOption(self::OPTION_BROWSER_TRIGGER_ARCHIVING); + if($browserArchivingEnabled !== false) + { + return (bool)$browserArchivingEnabled; + } + return (bool)Zend_Registry::get('config')->General->enable_browser_archiving_triggering; + } + public function getIdArchive() { return $this->idArchive; } /** - * Inits the object + * Sets object attributes that will be used throughout the process */ - protected function loadArchiveProperties() - { + public function init() + { $this->idsite = $this->site->getId(); - $this->periodId = $this->period->getId(); - - $this->dateStart = $this->period->getDateStart(); - $this->dateEnd = $this->period->getDateEnd(); + + $dateStartLocalTimezone = $this->period->getDateStart(); + $dateEndLocalTimezone = $this->period->getDateEnd(); $this->tableArchiveNumeric = new Piwik_TablePartitioning_Monthly('archive_numeric'); $this->tableArchiveNumeric->setIdSite($this->idsite); - $this->tableArchiveNumeric->setTimestamp($this->dateStart->get()); + $this->tableArchiveNumeric->setTimestamp($dateStartLocalTimezone->getTimestamp()); $this->tableArchiveBlob = new Piwik_TablePartitioning_Monthly('archive_blob'); $this->tableArchiveBlob->setIdSite($this->idsite); - $this->tableArchiveBlob->setTimestamp($this->dateStart->get()); + $this->tableArchiveBlob->setTimestamp($dateStartLocalTimezone->getTimestamp()); + + $dateStartUTC = $dateStartLocalTimezone->setTimezone($this->site->getTimezone()); + $dateEndUTC = $dateEndLocalTimezone->setTimezone($this->site->getTimezone()); + $this->startDatetimeUTC = $dateStartUTC->getDateStartUTC(); + $this->endDatetimeUTC = $dateEndUTC->getDateEndUTC(); - $this->strDateStart = $this->dateStart->toString(); - $this->strDateEnd = $this->dateEnd->toString(); + $this->startTimestampUTC = $dateStartUTC->getTimestamp(); + $this->endTimestampUTC = strtotime($this->endDatetimeUTC); + $this->minDatetimeArchiveProcessedUTC = $this->getMinTimeArchivedProcessed(); + $db = Zend_Registry::get('db'); + $this->compressBlob = $db->hasBlobDataType(); + } + + public function getStartDatetimeUTC() + { + return $this->startDatetimeUTC; + } + + public function getEndDatetimeUTC() + { + return $this->endDatetimeUTC; + } + + public function isArchiveTemporary() + { + return $this->temporaryArchive; + } + + /** + * Returns the minimum archive processed datetime to look at + * + * @return string Datetime string, or false if must look at any archive available + */ + public function getMinTimeArchivedProcessed() + { + $this->temporaryArchive = false; // if the current archive is a DAY and if it's today, - // we set this maxTimestampArchive that defines the lifetime value of today's archive - $this->maxTimestampArchive = 0; + // we set this minDatetimeArchiveProcessedUTC that defines the lifetime value of today's archive if( $this->period->getNumberOfSubperiods() == 0 - && $this->period->toString() == date("Y-m-d") + && $this->startTimestampUTC <= time() && $this->endTimestampUTC > time() ) { - $this->maxTimestampArchive = time() - Zend_Registry::get('config')->General->time_before_today_archive_considered_outdated; + $this->temporaryArchive = true; + $minDatetimeArchiveProcessedUTC = time() - self::getTodayArchiveTimeToLive(); + // see #1150; if new archives are not triggered from the browser, + // we still want to try and return the latest archive available for today (rather than return nothing) + if($this->isArchivingDisabled()) + { + return false; + } } // either // - if the period we're looking for is finished, we look for a ts_archived that // is greater than the last day of the archive // - if the period we're looking for is not finished, we look for a recent enough archive - // recent enough means maxTimestampArchive = 00:00:01 this morning + // recent enough means minDatetimeArchiveProcessedUTC = 00:00:01 this morning else { - if($this->period->isFinished()) + if($this->endTimestampUTC <= time()) { - $this->maxTimestampArchive = $this->period->getDateEnd()->setTime('00:00:00')->addDay(1)->getTimestamp(); + $minDatetimeArchiveProcessedUTC = $this->endTimestampUTC+1; } else { - $this->maxTimestampArchive = Piwik_Date::today()->getTimestamp(); + $this->temporaryArchive = true; + $minDatetimeArchiveProcessedUTC = Piwik_Date::today() + ->setTimezone($this->site->getTimezone()) + ->getTimestamp(); } } - - $db = Zend_Registry::get('db'); - $this->compressBlob = $db->hasBlobDataType(); + return $minDatetimeArchiveProcessedUTC; } /** @@ -286,7 +385,7 @@ abstract class Piwik_ArchiveProcessing */ public function loadArchive() { - $this->loadArchiveProperties(); + $this->init(); $this->idArchive = $this->isArchived(); if($this->idArchive === false @@ -327,10 +426,19 @@ abstract class Piwik_ArchiveProcessing { $this->loadNextIdarchive(); $this->insertNumericRecord('done', Piwik_ArchiveProcessing::DONE_ERROR); - $this->logTable = Piwik::prefixTable('log_visit'); - $this->logVisitActionTable = Piwik::prefixTable('log_link_visit_action'); - $this->logActionTable = Piwik::prefixTable('log_action'); - $this->logConversionTable = Piwik::prefixTable('log_conversion'); + $this->logTable = Piwik_Common::prefixTable('log_visit'); + $this->logVisitActionTable = Piwik_Common::prefixTable('log_link_visit_action'); + $this->logActionTable = Piwik_Common::prefixTable('log_action'); + $this->logConversionTable = Piwik_Common::prefixTable('log_conversion'); + + $temporary = 'definitive archive'; + if($this->isArchiveTemporary()) + { + $temporary = 'temporary archive'; + } + Piwik::log("Processing archive '" . $this->period->getLabel() . "', + idsite = ". $this->idsite." ($temporary) - + UTC datetime [".$this->startDatetimeUTC." -> ".$this->endDatetimeUTC." ]..."); } /** @@ -343,12 +451,17 @@ abstract class Piwik_ArchiveProcessing { // delete the first done = ERROR Piwik_Query("/* SHARDING_ID_SITE = ".$this->idsite." */ - DELETE FROM ".$this->tableArchiveNumeric->getTableName()." - WHERE idarchive = ? AND name = 'done'", + DELETE FROM ".$this->tableArchiveNumeric->getTableName()." + WHERE idarchive = ? AND name = 'done'", array($this->idArchive) - ); + ); - $this->insertNumericRecord('done', Piwik_ArchiveProcessing::DONE_OK); + $flag = Piwik_ArchiveProcessing::DONE_OK; + if($this->isArchiveTemporary()) + { + $flag = Piwik_ArchiveProcessing::DONE_OK_TEMPORARY; + } + $this->insertNumericRecord('done', $flag); Piwik_DataTable_Manager::getInstance()->deleteAll(); } @@ -400,14 +513,8 @@ abstract class Piwik_ArchiveProcessing */ public function getTimestampStartDate() { - // case when archive processing is in the past or the future, the starting date has not been set or processed yet - if(is_null($this->timestampDateStart)) - { - return Piwik_Date::factory($this->strDateStart)->getTimestamp(); - } return $this->timestampDateStart; } - // exposing the number of visits publicly (number used to compute conversions rates) protected $nb_visits = null; @@ -438,7 +545,9 @@ abstract class Piwik_ArchiveProcessing protected function loadNextIdarchive() { $db = Zend_Registry::get('db'); - $id = $db->fetchOne("/* SHARDING_ID_SITE = ".$this->idsite." */ SELECT max(idarchive) FROM ".$this->tableArchiveNumeric->getTableName()); + $id = $db->fetchOne("/* SHARDING_ID_SITE = ".$this->idsite." */ + SELECT max(idarchive) + FROM ".$this->tableArchiveNumeric->getTableName()); if(empty($id)) { $id = 0; @@ -498,23 +607,31 @@ abstract class Piwik_ArchiveProcessing */ protected function insertRecord($record) { + // table to use to save the data if(is_numeric($record->value)) { + // We choose not to record records with a value of 0 + if($record->value == 0) + { + return; + } $table = $this->tableArchiveNumeric; } else { $table = $this->tableArchiveBlob; } - - $query = "INSERT INTO ".$table->getTableName()." (idarchive, idsite, date1, date2, period, ts_archived, name, value) + + // ignore duplicate idarchive + // @see http://dev.piwik.org/trac/ticket/987 + $query = "INSERT IGNORE INTO ".$table->getTableName()." (idarchive, idsite, date1, date2, period, ts_archived, name, value) VALUES (?,?,?,?,?,?,?,?)"; Piwik_Query($query, array( $this->idArchive, $this->idsite, - $this->strDateStart, - $this->strDateEnd, + $this->period->getDateStart()->toString('Y-m-d'), + $this->period->getDateEnd()->toString('Y-m-d'), $this->periodId, date("Y-m-d H:i:s"), $record->name, @@ -528,7 +645,7 @@ abstract class Piwik_ArchiveProcessing * Returns false if the archive needs to be computed. * * An archive is available if - * - for today, the archive was computed less than maxTimestampArchive seconds ago + * - for today, the archive was computed less than minDatetimeArchiveProcessedUTC seconds ago * - for any other day, if the archive was computed once this day was finished * - for other periods, if the archive was computed once the period was finished * @@ -537,20 +654,27 @@ abstract class Piwik_ArchiveProcessing protected function isArchived() { $bindSQL = array( $this->idsite, - $this->strDateStart, - $this->strDateEnd, - $this->periodId, - ); - $timeStampWhere = " AND UNIX_TIMESTAMP(ts_archived) >= ? "; - $bindSQL[] = $this->maxTimestampArchive; + $this->period->getDateStart()->toString('Y-m-d'), + $this->period->getDateEnd()->toString('Y-m-d'), + $this->periodId, + ); + + $timeStampWhere = ''; - $sqlQuery = " SELECT idarchive, value, name, UNIX_TIMESTAMP(date1) as timestamp + if($this->minDatetimeArchiveProcessedUTC) + { + $timeStampWhere = " AND ts_archived >= ? "; + $bindSQL[] = Piwik_Date::factory($this->minDatetimeArchiveProcessedUTC)->getDatetime(); + } + + $sqlQuery = " SELECT idarchive, value, name, date1 as startDate FROM ".$this->tableArchiveNumeric->getTableName()." WHERE idsite = ? AND date1 = ? AND date2 = ? AND period = ? AND ( (name = 'done' AND value = ".Piwik_ArchiveProcessing::DONE_OK.") + OR (name = 'done' AND value = ".Piwik_ArchiveProcessing::DONE_OK_TEMPORARY.") OR name = 'nb_visits') $timeStampWhere ORDER BY ts_archived DESC"; @@ -567,7 +691,7 @@ abstract class Piwik_ArchiveProcessing if($result['name'] == 'done') { $idarchive = $result['idarchive']; - $this->timestampDateStart = $result['timestamp']; + $this->timestampDateStart = Piwik_Date::factory($result['startDate'])->getTimestamp(); break; } } @@ -579,7 +703,7 @@ abstract class Piwik_ArchiveProcessing return false; } - // we look for the nb_visits result for this more recent archive + // we look for the nb_visits result for this most recent archive foreach($results as $result) { if($result['name'] == 'nb_visits' @@ -599,19 +723,11 @@ abstract class Piwik_ArchiveProcessing */ protected function isArchivingDisabled() { - static $archivingIsDisabled = null; - if(is_null($archivingIsDisabled)) + if(!self::isBrowserTriggerArchivingEnabled() + && !Piwik_Common::isPhpCliMode()) { - $archivingIsDisabled = false; - $enableBrowserArchivingTriggering = (bool)Zend_Registry::get('config')->General->enable_browser_archiving_triggering; - if($enableBrowserArchivingTriggering == false) - { - if( !Piwik_Common::isPhpCliMode()) - { - $archivingIsDisabled = true; - } - } + return true; } - return $archivingIsDisabled; + return false; } } diff --git a/core/ArchiveProcessing/Day.php b/core/ArchiveProcessing/Day.php index df823b978b..5193a39562 100644 --- a/core/ArchiveProcessing/Day.php +++ b/core/ArchiveProcessing/Day.php @@ -44,12 +44,12 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing sum(case visit_total_actions when 1 then 1 else 0 end) as bounce_count, sum(case visit_goal_converted when 1 then 1 else 0 end) as nb_visits_converted FROM ".$this->logTable." - WHERE visit_server_date = ? + WHERE visit_last_action_time >= ? + AND visit_last_action_time <= ? AND idsite = ? - GROUP BY visit_server_date ORDER BY NULL"; - $row = $this->db->fetchRow($query, array($this->strDateStart,$this->idsite ) ); - if($row === false || $row === null) + $row = $this->db->fetchRow($query, array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite ) ); + if($row === false || $row === null || $row['nb_visits'] == 0) { return; } @@ -87,9 +87,10 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing { $query = "SELECT $select FROM ".$this->logTable." - WHERE visit_server_date = ? - AND idsite = ?"; - $data = $this->db->fetchRow($query, array( $this->strDateStart, $this->idsite )); + WHERE visit_last_action_time >= ? + AND visit_last_action_time <= ? + AND idsite = ?"; + $data = $this->db->fetchRow($query, array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite )); foreach($data as $label => &$count) { @@ -152,11 +153,12 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing sum(case visit_total_actions when 1 then 1 else 0 end) as bounce_count, sum(case visit_goal_converted when 1 then 1 else 0 end) as nb_visits_converted FROM ".$this->logTable." - WHERE visit_server_date = ? - AND idsite = ? + WHERE visit_last_action_time >= ? + AND visit_last_action_time <= ? + AND idsite = ? GROUP BY label ORDER BY NULL"; - $query = $this->db->query($query, array( $this->strDateStart, $this->idsite ) ); + $query = $this->db->query($query, array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite ) ); $interest = array(); while($row = $query->fetch()) @@ -327,11 +329,12 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing sum(revenue) as revenue $segments FROM ".$this->logConversionTable." - WHERE visit_server_date = ? - AND idsite = ? + WHERE server_time >= ? + AND server_time <= ? + AND idsite = ? GROUP BY idgoal $segments ORDER BY NULL"; - $query = $this->db->query($query, array( $this->strDateStart, $this->idsite )); + $query = $this->db->query($query, array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite )); return $query; } @@ -342,15 +345,20 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing sum(revenue) as revenue, $segment as label FROM ".$this->logConversionTable." - WHERE visit_server_date = ? - AND idsite = ? + WHERE server_time >= ? + AND server_time <= ? + AND idsite = ? GROUP BY idgoal, label ORDER BY NULL"; - $query = $this->db->query($query, array( $this->strDateStart, $this->idsite )); + $query = $this->db->query($query, array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite )); return $query; } /** + * Given an array of stats, it will process the sum of goal conversions + * and sum of revenue and add it in the stats array in two new fields. + * + * @param $interestByLabel Passed by reference, it will be modified as follows: * Input: * array( * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X, @@ -362,10 +370,12 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] ) * ); * + * * Output: * array( * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X, - * + * Piwik_Archive::INDEX_NB_CONVERSIONS => Y, // sum of all conversions + * Piwik_Archive::INDEX_REVENUE => Z, // sum of all revenue * Piwik_Archive::INDEX_GOALS => array( * idgoal1 => array( [...] ), * idgoal2 => array( [...] ), diff --git a/core/ArchiveProcessing/Period.php b/core/ArchiveProcessing/Period.php index 7f40c744e7..37b65cba70 100644 --- a/core/ArchiveProcessing/Period.php +++ b/core/ArchiveProcessing/Period.php @@ -156,6 +156,11 @@ class Piwik_ArchiveProcessing_Period extends Piwik_ArchiveProcessing * final DataTable (ie. the number of distinct LABEL over the period) (eg. the number of distinct keywords over the last month) * * @param string|array Field name(s) of DataTable to select so we can get the sum + * @param array (current_column_name => new_column_name) for columns that must change names when summed (eg. unique visitors go from nb_uniq_visitors to sum_daily_nb_uniq_visitors) + * @param int Max row count of parent datatable to archive + * @param int Max row count of children datatable(s) to archive + * @param string Column name to sort by, before truncating rows (ie. if there are more rows than the specified max row count) + * * @return array array ( * nameTable1 => number of rows, * nameTable2 => number of rows, @@ -283,10 +288,14 @@ class Piwik_ArchiveProcessing_Period extends Piwik_ArchiveProcessing protected function computeNbUniqVisitors() { - $query = "SELECT count(distinct visitor_idcookie) as nb_uniq_visitors FROM ".$this->logTable." - WHERE visit_server_date >= ? AND visit_server_date <= ? AND idsite = ?"; + $query = " + SELECT count(distinct visitor_idcookie) as nb_uniq_visitors + FROM ".$this->logTable." + WHERE visit_last_action_time >= ? + AND visit_last_action_time <= ? + AND idsite = ?"; - return Zend_Registry::get('db')->fetchOne($query, array( $this->strDateStart, $this->strDateEnd, $this->idsite )); + return Zend_Registry::get('db')->fetchOne($query, array( $this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite )); } /** @@ -297,46 +306,53 @@ class Piwik_ArchiveProcessing_Period extends Piwik_ArchiveProcessing { parent::postCompute(); + foreach($this->archives as $archive) + { + destroy($archive); + } + $this->archives = array(); + $blobTable = $this->tableArchiveBlob->getTableName(); $numericTable = $this->tableArchiveNumeric->getTableName(); - // delete out of date records maximum once per day (DELETE request is costly) $key = 'lastPurge_' . $blobTable; $timestamp = Piwik_GetOption($key); if(!$timestamp - || $timestamp < time() - 86400 ) + || $timestamp < time() - 86400) { - // we delete out of date daily archives from table, maximum once per day - // those for day N that were processed on day N (means the archives are only partial as the day wasn't finished) - $query = "/* SHARDING_ID_SITE = ".$this->idsite." */ DELETE - FROM %s - WHERE period = ? - AND date1 = DATE(ts_archived) - AND DATE(ts_archived) <> CURRENT_DATE() - "; - Piwik_Query(sprintf($query, $blobTable), Piwik::$idPeriods['day']); - Piwik_Query(sprintf($query, $numericTable), Piwik::$idPeriods['day']); - - // we delete out of date Period records (week/month/etc) - // we delete archives that were archived before the end of the period - // and only if they are at least 1 day old (so we don't delete archives computed today that may be stil valid) - $query = " DELETE - FROM %s - WHERE period > ? - AND DATE(ts_archived) <= date2 - AND date(ts_archived) < date_sub(CURRENT_DATE(), INTERVAL 1 DAY) - "; + Piwik_SetOption($key, time()); - Piwik_Query(sprintf($query, $blobTable), Piwik::$idPeriods['day']); - Piwik_Query(sprintf($query, $numericTable), Piwik::$idPeriods['day']); + // we delete out of date daily archives from table, maximum once per day + // we only delete archives processed that are older than 1 day, to not delete archives we just processed + $yesterday = Piwik_Date::factory('yesterday')->getDateTime(); + $result = Piwik_FetchAll(" + SELECT idarchive + FROM $numericTable + WHERE name='done' + AND value = ". Piwik_ArchiveProcessing::DONE_OK_TEMPORARY ." + AND ts_archived < ?", array($yesterday)); - Piwik_SetOption($key, time()); + $idArchivesToDelete = array(); + if(!empty($result)) + { + foreach($result as $row) { + $idArchivesToDelete[] = $row['idarchive']; + } + $query = "/* SHARDING_ID_SITE = ".$this->idsite." */ + DELETE + FROM %s + WHERE idarchive IN (".implode(',',$idArchivesToDelete).") + "; + + Piwik_Query(sprintf($query, $blobTable)); + Piwik_Query(sprintf($query, $numericTable)); + } + Piwik::log("Purging temporary archives: done [ purged archives older than $yesterday from $blobTable and $numericTable ] [Deleted IDs: ". implode(',',$idArchivesToDelete)."]"); } - - foreach($this->archives as $archive) + else { - destroy($archive); + Piwik::log("Purging temporary archives: skipped."); } - $this->archives = array(); + } } diff --git a/core/CacheFile.php b/core/CacheFile.php index 8d877abddc..6a3f95cf9e 100644 --- a/core/CacheFile.php +++ b/core/CacheFile.php @@ -28,7 +28,6 @@ class Piwik_CacheFile function __construct($directory) { $this->cachePath = PIWIK_USER_PATH . '/tmp/cache/' . $directory . '/'; -// echo $this->cachePath;exit; } /** @@ -112,4 +111,12 @@ class Piwik_CacheFile } return false; } + + /** + * A function to delete all cache entries in the directory + */ + function deleteAll() + { + Piwik::unlinkRecursive($this->cachePath, $deleteRootToo = false); + } } diff --git a/core/Common.php b/core/Common.php index b1f152b95c..e972cc3a70 100644 --- a/core/Common.php +++ b/core/Common.php @@ -47,7 +47,7 @@ class Piwik_Common static $prefixTable = null; if(is_null($prefixTable)) { - if(defined('PIWIK_TRACKER_MODE') && PIWIK_TRACKER_MODE) + if(!empty($GLOBALS['PIWIK_TRACKER_MODE'])) { $prefixTable = Piwik_Tracker_Config::getInstance()->database['tables_prefix']; } @@ -84,23 +84,40 @@ class Piwik_Common { return $cacheContent; } - if(defined('PIWIK_TRACKER_MODE') - && PIWIK_TRACKER_MODE) + if(!empty($GLOBALS['PIWIK_TRACKER_MODE'])) { require_once PIWIK_INCLUDE_PATH . '/core/PluginsManager.php'; require_once PIWIK_INCLUDE_PATH . '/core/Translate.php'; require_once PIWIK_INCLUDE_PATH . '/core/Option.php'; - Zend_Registry::set('db', Piwik_Tracker::getDatabase()); - Piwik::createAccessObject(); - Piwik::createConfigObject(); - Piwik::setUserIsSuperUser(); + try { + $access = Zend_Registry::get('access'); + } catch (Exception $e) { + Piwik::createAccessObject(); + } + try { + $config = Zend_Registry::get('config'); + } catch (Exception $e) { + Piwik::createConfigObject(); + } + try { + $db = Zend_Registry::get('db'); + } catch (Exception $e) { + Piwik::createDatabaseObject(); + } + $pluginsManager = Piwik_PluginsManager::getInstance(); - $pluginsManager->setPluginsToLoad( Zend_Registry::get('config')->Plugins->Plugins->toArray() ); + $pluginsManager->loadPlugins( Zend_Registry::get('config')->Plugins->Plugins->toArray() ); } + $isSuperUser = Piwik::isUserIsSuperUser(); + Piwik::setUserIsSuperUser(); $content = array(); Piwik_PostEvent('Common.fetchWebsiteAttributes', $content, $idSite); + + // we remove the temporary Super user privilege + Piwik::setUserIsSuperUser($isSuperUser); + // if nothing is returned from the plugins, we don't save the content // this is not expected: all websites are expected to have at least one URL if(!empty($content)) @@ -140,6 +157,16 @@ class Piwik_Common } /** + * Deletes all Tracker cache files + */ + static public function deleteAllCache() + { + $cache = new Piwik_CacheFile('tracker'); + $cache->deleteAll(); + } + + + /** * Returns the path and query part from a URL. * Eg. http://piwik.org/test/index.php?module=CoreHome will return /test/index.php?module=CoreHome * @@ -190,7 +217,6 @@ class Piwik_Common /** * Returns an URL query string in an array format - * The input query string should be htmlspecialchar'ed * * @param string urlQuery * @return array array( param1=> value1, param2=>value2) @@ -243,6 +269,35 @@ class Piwik_Common } /** + * Builds a URL from the result of parse_url function + * Copied from the PHP comments at http://php.net/parse_url + * @param array + */ + static public function getParseUrlReverse($parsed) + { + if (!is_array($parsed)) + { + return false; + } + + $uri = !empty($parsed['scheme']) ? $parsed['scheme'].':'.((strtolower($parsed['scheme']) == 'mailto') ? '' : '//') : ''; + $uri .= !empty($parsed['user']) ? $parsed['user'].(!empty($parsed['pass']) ? ':'.$parsed['pass'] : '').'@' : ''; + $uri .= !empty($parsed['host']) ? $parsed['host'] : ''; + $uri .= !empty($parsed['port']) ? ':'.$parsed['port'] : ''; + + if (!empty($parsed['path'])) + { + $uri .= (substr($parsed['path'], 0, 1) == '/') + ? $parsed['path'] + : ((!empty($uri) ? '/' : '' ) . $parsed['path']); + } + + $uri .= !empty($parsed['query']) ? '?'.$parsed['query'] : ''; + $uri .= !empty($parsed['fragment']) ? '#'.$parsed['fragment'] : ''; + return $uri; + } + + /** * Create directory if permitted * * @param string $path @@ -272,10 +327,11 @@ class Piwik_Common * Apache-specific; for IIS @see web.config * * @param string $path without trailing slash + * @param string $content */ - static public function createHtAccess( $path ) + static public function createHtAccess( $path, $content = "<Files \"*\">\nDeny from all\n</Files>\n" ) { - @file_put_contents($path . '/.htaccess', 'Deny from all'); + @file_put_contents($path . '/.htaccess', $content); } /** @@ -355,8 +411,8 @@ class Piwik_Common { $value = self::sanitizeInputValue($value); - // Undo the damage caused by magic_quotes -- only before php 5.3 as it is now deprecated - if ( version_compare(phpversion(), '5.3') === -1 + // Undo the damage caused by magic_quotes; deprecated in php 5.3 but not removed until php 6 + if ( version_compare(phpversion(), '6') === -1 && get_magic_quotes_gpc()) { $value = stripslashes($value); @@ -508,6 +564,30 @@ class Piwik_Common } /** + * Unserialize (serialized) array + * + * @param string + * @return array or original string if not unserializable + */ + public static function unserialize_array( $str ) + { + // we set the unserialized version only for arrays as you can have set a serialized string on purpose + if (preg_match('/^a:[0-9]+:{/', $str) + && !preg_match('/(^|;|{|})O:[0-9]+:"/', $str) + && strpos($str, "\0") === false) + { + if( ($arrayValue = @unserialize($str)) !== false + && is_array($arrayValue) ) + { + return $arrayValue; + } + } + + // return original string + return $str; + } + + /** * Returns a 32 characters long uniq ID * * @return string 32 chars @@ -518,13 +598,47 @@ class Piwik_Common } /** + * Get salt from [superuser] section + * + * @return string + */ + static public function getSalt() + { + static $salt = null; + if(is_null($salt)) + { + if(!empty($GLOBALS['PIWIK_TRACKER_MODE'])) + { + $salt = Piwik_Tracker_Config::getInstance()->superuser['salt']; + } + else + { + $config = Zend_Registry::get('config'); + if($config !== false) + { + $salt = $config->superuser->salt; + } + } + } + return $salt; + } + + /** * Convert dotted IP to a stringified integer representation * * @return string ip */ static public function getIp() { - return sprintf("%u", ip2long(self::getIpString())); + $ip = self::getIpString(); + + // accept ipv4-mapped addresses + if(strpos($ip, '::ffff:') === 0) + { + $ip = substr($ip, 7); + } + + return sprintf("%u", ip2long($ip)); } /** @@ -534,38 +648,34 @@ class Piwik_Common */ static public function getIpString() { - if(isset($_SERVER['HTTP_CLIENT_IP']) - && ($ip = self::getFirstIpFromList($_SERVER['HTTP_CLIENT_IP'])) - && strpos($ip, "unknown") === false) - { - return $ip; - } - elseif(isset($_SERVER['HTTP_X_FORWARDED_FOR']) - && $ip = self::getFirstIpFromList($_SERVER['HTTP_X_FORWARDED_FOR']) - && isset($ip) - && !empty($ip) - && strpos($ip, "unknown")===false ) - { - return $ip; - } - elseif( isset($_SERVER['HTTP_CLIENT_IP']) - && strlen( self::getFirstIpFromList($_SERVER['HTTP_CLIENT_IP']) ) != 0 ) - { - return self::getFirstIpFromList($_SERVER['HTTP_CLIENT_IP']); - } - else if( isset($_SERVER['HTTP_X_FORWARDED_FOR']) - && strlen ($ip = self::getFirstIpFromList($_SERVER['HTTP_X_FORWARDED_FOR'])) != 0) + // note: these may be spoofed + static $clientHeaders = array( + // ISP proxy + 'HTTP_CLIENT_IP', + + // de facto standard + 'HTTP_X_FORWARDED_FOR', + ); + + foreach($clientHeaders as $clientHeader) { - return $ip; + if(!empty($_SERVER[$clientHeader])) + { + $ip = self::getFirstIpFromList($_SERVER[$clientHeader]); + if(!empty($ip) && stripos($ip, 'unknown') === false) + { + return $ip; + } + } } - elseif(isset($_SERVER['REMOTE_ADDR'])) + + // default + if(isset($_SERVER['REMOTE_ADDR'])) { return self::getFirstIpFromList($_SERVER['REMOTE_ADDR']); } - else - { - return '0.0.0.0'; - } + + return '0.0.0.0'; } /** @@ -575,7 +685,7 @@ class Piwik_Common * * @return string first element before ',' */ - static private function getFirstIpFromList($ip) + static public function getFirstIpFromList($ip) { $p = strpos($ip, ','); if($p!==false) @@ -657,14 +767,23 @@ class Piwik_Common } /** - * Returns the visitor country based only on the Browser 'accepted language' information + * Returns the visitor country based on the Browser 'accepted language' + * information, but provides a hook for geolocation via IP address. * * @param string $lang browser lang * @param bool If set to true, some assumption will be made and detection guessed more often, but accuracy could be affected + * @param string $ip * @return string 2 letter ISO code */ - static public function getCountry( $lang, $enableLanguageToCountryGuess ) + static public function getCountry( $lang, $enableLanguageToCountryGuess, $ip ) { + $country = null; + Piwik_PostEvent('Common.getCountry', $country, $ip); + if($country) + { + return $country; + } + if(empty($lang) || strlen($lang) < 2) { return 'xx'; @@ -797,13 +916,26 @@ class Piwik_Common { return false; } + // some search engines (eg. Bing Images) use the same domain + // as an existing search engine (eg. Bing), we must also use the url path + $refererPath = ''; + if(isset($refererParsed['path'])) + { + $refererPath = $refererParsed['path']; + } + // no search query if(!isset($refererParsed['query'])) { return false; } require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php'; - if(!array_key_exists($refererHost, $GLOBALS['Piwik_SearchEngines'])) + $refererHostPath = $refererHost . $refererPath; + if(array_key_exists($refererHostPath, $GLOBALS['Piwik_SearchEngines'])) + { + $refererHost = $refererHostPath; + } + elseif(!array_key_exists($refererHost, $GLOBALS['Piwik_SearchEngines'])) { return false; } @@ -824,10 +956,12 @@ class Piwik_Common } $query = $refererParsed['query']; - if($searchEngineName == 'Google Images') + if($searchEngineName == 'Google Images' + || ($searchEngineName == 'Google' && strpos($refererUrl, '/imgres') !== false) ) { $query = urldecode(trim(strtolower(self::getParameterFromQueryString($query, 'prev')))); $query = str_replace('&', '&', strstr($query, '?')); + $searchEngineName = 'Google Images'; } foreach($variableNames as $variableName) @@ -892,7 +1026,8 @@ class Piwik_Common */ static public function isPhpCliMode() { + $remoteAddr = @$_SERVER['REMOTE_ADDR']; return PHP_SAPI == 'cli' || - (substr(PHP_SAPI, 0, 3) == 'cgi' && @$_SERVER['REMOTE_ADDR'] == ''); + (substr(PHP_SAPI, 0, 3) == 'cgi' && empty($remoteAddr)); } } diff --git a/core/Config.php b/core/Config.php index 84851053ea..4801875131 100644 --- a/core/Config.php +++ b/core/Config.php @@ -20,6 +20,7 @@ * will read the value minimumMemoryLimit under the [General] section of the config file * * @package Piwik + * @subpackage Piwik_Config */ class Piwik_Config { @@ -37,7 +38,8 @@ class Piwik_Config protected $pathIniFileDefaultConfig = null; protected $configFileUpdated = false; protected $doWriteFileWhenUpdated = true; - protected $cachedConfigArray = array(); + protected $cachedConfigArray = array(); + protected $isTestEnvironment = false; /** * Storing the correct cwd() because the value is not correct in the destructor @@ -104,12 +106,25 @@ class Piwik_Config public function init() { - $this->defaultConfig = new Zend_Config_Ini($this->pathIniFileDefaultConfig, null, true); - if(!Zend_Loader::isReadable($this->pathIniFileUserConfig)) + if(!is_readable($this->pathIniFileDefaultConfig)) { - throw new Exception("The configuration file {$this->pathIniFileUserConfig} has not been found."); + Piwik_ExitWithMessage(Piwik_TranslateException('General_ExceptionConfigurationFileNotFound', array($this->pathIniFileDefaultConfig))); + } + $this->defaultConfig = new Piwik_Config_Ini($this->pathIniFileDefaultConfig, null, true); + if(is_null($this->defaultConfig) || count($this->defaultConfig->toArray()) == 0) + { + Piwik_ExitWithMessage(Piwik_TranslateException('General_ExceptionUnreadableFileDisabledMethod', array($this->pathIniFileDefaultConfig, "parse_ini_file()"))); + } + + if(!is_readable($this->pathIniFileUserConfig)) + { + throw new Exception(Piwik_TranslateException('General_ExceptionConfigurationFileNotFound', array($this->pathIniFileUserConfig))); + } + $this->userConfig = new Piwik_Config_Ini($this->pathIniFileUserConfig, null, true); + if(is_null($this->userConfig) || count($this->userConfig->toArray()) == 0) + { + Piwik_ExitWithMessage(Piwik_TranslateException('General_ExceptionUnreadableFileDisabledMethod', array($this->pathIniFileUserConfig, "parse_ini_file()"))); } - $this->userConfig = new Zend_Config_Ini($this->pathIniFileUserConfig, null, true); } /** @@ -154,16 +169,35 @@ class Piwik_Config $configFile .= "\n"; } chdir($this->correctCwd); - file_put_contents($this->pathIniFileUserConfig, $configFile ); + @file_put_contents($this->pathIniFileUserConfig, $configFile ); } } + public function isFileWritable() + { + return is_writable($this->pathIniFileUserConfig); + } + /** * If called, we use the database_tests credentials */ public function setTestEnvironment() { + $this->isTestEnvironment = true; $this->database = $this->database_tests->toArray(); + // for unit tests, we set that no plugin is installed. This will force + // the test initialization to create the plugins tables, execute ALTER queries, etc. + $this->PluginsInstalled = array(); + $this->disableSavingConfigurationFileUpdates(); + } + + /** + * Is the config file set to use the test values? + * @return bool + */ + public function isTestEnvironment() + { + return $this->isTestEnvironment; } /** @@ -294,3 +328,47 @@ class Piwik_Config return $this->cachedConfigArray[$name]; } } + +/** + * Subclasses Zend_Config_Ini so we can use our own parse_ini_file() wrapper. + * + * @package Piwik + * @subpackage Piwik_Config + */ +class Piwik_Config_Ini extends Zend_Config_Ini +{ + /** + * Handle any errors from parse_ini_file + * + * @param integer $errno + * @param string $errstr + * @param string $errfile + * @param integer $errline + */ + public function _parseFileErrorHandler($errno, $errstr, $errfile, $errline) + { + $this->_loadFileErrorHandler($errno, $errstr, $errfile, $errline); + } + + /** + * Load ini file configuration + * + * Derived from Zend_Config_Ini->_loadIniFile() and Zend_Config_Ini->_parseIniFile() + * @license New BSD License + * + * @param string $filename + * @return array + */ + protected function _loadIniFile($filename) + { + set_error_handler(array($this, '_parseFileErrorHandler')); + $iniArray = _parse_ini_file($filename, true); + restore_error_handler(); + // Check if there was an error while loading the file + if ($this->_loadFileErrorStr !== null) { + throw new Zend_Config_Exception($this->_loadFileErrorStr); + } + + return $iniArray; + } +} diff --git a/core/Controller.php b/core/Controller.php index 04d950fa80..f3c9fce465 100644 --- a/core/Controller.php +++ b/core/Controller.php @@ -37,6 +37,8 @@ abstract class Piwik_Controller * @var Piwik_Date|null */ protected $date; + protected $idSite; + protected $site = null; /** * Builds the controller object, reads the date from the request, extracts plugin name from @@ -45,11 +47,12 @@ abstract class Piwik_Controller { $aPluginName = explode('_', get_class($this)); $this->pluginName = $aPluginName[1]; - $this->strDate = Piwik_Common::getRequestVar('date', 'yesterday', 'string'); - try{ - // the date looks like YYYY-MM-DD we can build it - $this->date = Piwik_Date::factory($this->strDate); - $this->strDate = $this->date->toString(); + $date = Piwik_Common::getRequestVar('date', 'yesterday', 'string'); + try { + $this->idSite = Piwik_Common::getRequestVar('idSite', false, 'int'); + $this->site = new Piwik_Site($this->idSite); + $date = $this->getDateParameterInTimezone($date, $this->site->getTimezone()); + $this->setDate($date); } catch(Exception $e){ // the date looks like YYYY-MM-DD,YYYY-MM-DD or other format $this->date = null; @@ -57,6 +60,48 @@ abstract class Piwik_Controller } /** + * Helper method to convert "today" or "yesterday" to the default timezone specified. + * If the date is absolute, ie. YYYY-MM-DD, it will not be converted to the timezone + * @param $date today, yesterday, YYYY-MM-DD + * @param $defaultTimezone + * @return Piwik_Date + */ + protected function getDateParameterInTimezone($date, $defaultTimezone ) + { + $timezone = null; + // if the requested date is not YYYY-MM-DD, we need to ensure + // it is relative to the website's timezone + if(in_array($date, array('today', 'yesterday'))) + { + // today is at midnight; we really want to get the time now, so that + // * if the website is UTC+12 and it is 5PM now in UTC, the calendar will allow to select the UTC "tomorrow" + // * if the website is UTC-12 and it is 5AM now in UTC, the calendar will allow to select the UTC "yesterday" + if($date == 'today') + { + $date = 'now'; + } + elseif($date == 'yesterday') + { + $date = 'yesterdaySameTime'; + } + $timezone = $defaultTimezone; + } + return Piwik_Date::factory($date, $timezone); + } + + /** + * Sets the date to be used by all other methods in the controller. + * If the date has to be modified, it should be called just after the controller construct + * @param $date + * @return void + */ + protected function setDate(Piwik_Date $date) + { + $this->date = $date; + $this->strDate = $this->date->toString(); + } + + /** * Returns the name of the default method that will be called * when visiting: index.php?module=PluginName without the action parameter * @@ -181,14 +226,13 @@ abstract class Piwik_Controller { $period = $paramsToSet['period']; } - $last30Relative = new Piwik_Period_Range($period, $range ); + $last30Relative = new Piwik_Period_Range($period, $range, $this->site->getTimezone() ); $last30Relative->setDefaultEndDate(Piwik_Date::factory($endDate)); $paramDate = $last30Relative->getDateStart()->toString() . "," . $last30Relative->getDateEnd()->toString(); $params = array_merge($paramsToSet , array( 'date' => $paramDate ) ); - return $params; } @@ -228,6 +272,37 @@ abstract class Piwik_Controller return $url; } + /** + * Sets the first date available in the calendar + * @param $minDate + * @param $view + * @return void + */ + protected function setMinDateView(Piwik_Date $minDate, $view) + { + $view->minDateYear = $minDate->toString('Y'); + $view->minDateMonth = $minDate->toString('m'); + $view->minDateDay = $minDate->toString('d'); + } + + /** + * Sets "today" in the calendar. Today does not always mean "UTC" today, eg. for websites in UTC+12. + * @param $maxDate + * @param $view + * @return void + */ + protected function setMaxDateView(Piwik_Date $maxDate, $view) + { + $view->maxDateYear = $maxDate->toString('Y'); + $view->maxDateMonth = $maxDate->toString('m'); + $view->maxDateDay = $maxDate->toString('d'); + } + + /** + * Sets general variables to the view that are used by various templates and Javascript + * @param $view + * @return void + */ protected function setGeneralVariablesView($view) { $view->date = $this->strDate; @@ -236,29 +311,35 @@ abstract class Piwik_Controller $this->setPeriodVariablesView($view); $period = Piwik_Period::factory(Piwik_Common::getRequestVar('period'), Piwik_Date::factory($this->strDate)); $view->prettyDate = $period->getLocalizedLongString(); - $idSite = Piwik_Common::getRequestVar('idSite'); - $view->idSite = $idSite; - $site = new Piwik_Site($idSite); - $view->siteName = $site->getName(); - $view->siteMainUrl = $site->getMainUrl(); + $view->idSite = $this->idSite; + if(is_null($this->site)) + { + throw new Exception("invalid website"); + } + $view->siteName = $this->site->getName(); + $view->siteMainUrl = $this->site->getMainUrl(); - $minDate = $site->getCreationDate(); - $view->minDateYear = $minDate->toString('Y'); - $view->minDateMonth = $minDate->toString('m'); - $view->minDateDay = $minDate->toString('d'); + $datetimeMinDate = $this->site->getCreationDate()->getDatetime(); + $minDate = Piwik_Date::factory($datetimeMinDate, $this->site->getTimezone()); + $this->setMinDateView($minDate, $view); - $maxDate = Piwik_Date::factory('today'); - $view->maxDateYear = $maxDate->toString('Y'); - $view->maxDateMonth = $maxDate->toString('m'); - $view->maxDateDay = $maxDate->toString('d'); + $maxDate = Piwik_Date::factory('now', $this->site->getTimezone()); + $this->setMaxDateView($maxDate, $view); + $view->currentAdminMenuName = Piwik_GetCurrentAdminMenuName(); $view->debugTrackVisitsInsidePiwikUI = Zend_Registry::get('config')->Debug->track_visits_inside_piwik_ui; + $view->isSuperUser = Zend_Registry::get('access')->isSuperUser(); } catch(Exception $e) { self::redirectToIndex(Piwik::getModule(), Piwik::getAction()); } } + /** + * Sets general period variables (available periods, current period, period labels) used by templates + * @param $view + * @return void + */ public static function setPeriodVariablesView($view) { if(isset($view->period)) @@ -288,44 +369,137 @@ abstract class Piwik_Controller $view->periodsNames = $periodNames; } - function redirectToIndex($moduleToRedirect, $actionToRedirect) + /** + * Helper method used to redirect the current http request to another module/action + * If specified, will also redirect to a given website, period and /or date + * + * @param $moduleToRedirect eg. "MultiSites" + * @param $actionToRedirect eg. "index" + * @param $websiteId eg. 1 + * @param $defaultPeriod eg. "day" + * @param $defaultDate eg. "today" + * @return issues a http header redirect and exits + */ + function redirectToIndex($moduleToRedirect, $actionToRedirect, $websiteId = null, $defaultPeriod = null, $defaultDate = null) { - $sitesId = Piwik_SitesManager_API::getSitesIdWithAtLeastViewAccess(); - if(!empty($sitesId)) + if(is_null($websiteId)) { - $firstSiteId = $sitesId[0]; - $firstSite = new Piwik_Site($firstSiteId); - if ($firstSite->getCreationDate()->isToday()) - { - $defaultDate = 'today'; - } - else - { - $defaultDate = Zend_Registry::get('config')->General->default_day; - } - $defaultPeriod = Zend_Registry::get('config')->General->default_period; - header("Location:index.php?module=".$moduleToRedirect."&action=".$actionToRedirect."&idSite=$firstSiteId&period=$defaultPeriod&date=$defaultDate"); + $websiteId = $this->getDefaultWebsiteId(); } - else + if(is_null($defaultDate)) { - if(Piwik::isUserIsSuperUser()) - { - Piwik_ExitWithMessage("Error: no website were found in this Piwik installation. - <br>Check the table '". Piwik::prefixTable('site') ."' that should contain your Piwik websites.", false, true); - } - $currentLogin = Piwik::getCurrentUserLogin(); - if(!empty($currentLogin) - && $currentLogin != 'anonymous') - { - $errorMessage = sprintf(Piwik_Translate('CoreHome_NoPrivileges'),$currentLogin); - $errorMessage .= "<br /><br /> <b><a href='index.php?module=". Zend_Registry::get('auth')->getName() ."&action=logout'>› ". Piwik_Translate('General_Logout'). "</a></b><br />"; - Piwik_ExitWithMessage($errorMessage, false, true); - } - else - { - Piwik_FrontController::dispatch('Login', false); - } + $defaultDate = $this->getDefaultDate(); + } + if(is_null($defaultPeriod)) + { + $defaultPeriod = $this->getDefaultPeriod(); } + + if($websiteId) { + header("Location:index.php?module=".$moduleToRedirect + ."&action=".$actionToRedirect + ."&idSite=".$websiteId + ."&period=".$defaultPeriod + ."&date=".$defaultDate); + exit; + } + + if(Piwik::isUserIsSuperUser()) + { + Piwik_ExitWithMessage("Error: no website were found in this Piwik installation. + <br />Check the table '". Piwik_Common::prefixTable('site') ."' that should contain your Piwik websites.", false, true); + } + + $currentLogin = Piwik::getCurrentUserLogin(); + if(!empty($currentLogin) + && $currentLogin != 'anonymous') + { + $errorMessage = sprintf(Piwik_Translate('CoreHome_NoPrivileges'),$currentLogin); + $errorMessage .= "<br /><br /> <b><a href='index.php?module=". Zend_Registry::get('auth')->getName() ."&action=logout'>› ". Piwik_Translate('General_Logout'). "</a></b><br />"; + Piwik_ExitWithMessage($errorMessage, false, true); + } + + Piwik_FrontController::dispatch(Piwik::getLoginPluginName(), false); exit; } + + + /** + * Returns default website that Piwik should load + * @return Piwik_Site + */ + protected function getDefaultWebsiteId() + { + $defaultWebsiteId = false; + + // User preference: default website ID to load + $defaultReport = Piwik_UsersManager_API::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), Piwik_UsersManager_API::PREFERENCE_DEFAULT_REPORT); + if(is_numeric($defaultReport)) + { + $defaultWebsiteId = $defaultReport; + } + + Piwik_PostEvent( 'Controller.getDefaultWebsiteId', $defaultWebsiteId ); + + if($defaultWebsiteId) + { + return $defaultWebsiteId; + } + + $sitesId = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess(); + if(!empty($sitesId)) + { + return $sitesId[0]; + } + return false; + } + + /** + * Returns default date for Piwik reports + * @return string today, 2010-01-01, etc. + */ + protected function getDefaultDate() + { + $userSettingsDate = Piwik_UsersManager_API::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), Piwik_UsersManager_API::PREFERENCE_DEFAULT_REPORT_DATE); + if($userSettingsDate === false) + { + return Zend_Registry::get('config')->General->default_day; + } + if($userSettingsDate == 'yesterday') + { + return $userSettingsDate; + } + return 'today'; + } + + /** + * Returns default date for Piwik reports + * @return string today, 2010-01-01, etc. + */ + protected function getDefaultPeriod() + { + $userSettingsDate = Piwik_UsersManager_API::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), Piwik_UsersManager_API::PREFERENCE_DEFAULT_REPORT_DATE); + if($userSettingsDate === false) + { + return Zend_Registry::get('config')->General->default_period; + } + if(in_array($userSettingsDate, array('today','yesterday'))) + { + return 'day'; + } + return $userSettingsDate; + } + + /** + * Checks that the specified token matches the current logged in user token + * Protection against CSRF + * + * @return throws exception if token doesn't match + */ + protected function checkTokenInUrl() + { + if(Piwik_Common::getRequestVar('token_auth', false) != Piwik::getCurrentUserTokenAuth()) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionInvalidToken')); + } + } } diff --git a/core/Cookie.php b/core/Cookie.php index 3da0fe5cd4..215f985450 100644 --- a/core/Cookie.php +++ b/core/Cookie.php @@ -46,11 +46,12 @@ class Piwik_Cookie * * @param string cookie Name * @param int The timestamp after which the cookie will expire, eg time() + 86400 + * @param string The path on the server in which the cookie will be available on. */ - public function __construct( $cookieName, $expire = null) + public function __construct( $cookieName, $expire = null, $path = null) { $this->name = $cookieName; - + $this->path = $path; $this->expire = $expire; if(is_null($expire) || !is_numeric($expire) @@ -59,6 +60,7 @@ class Piwik_Cookie $this->expire = $this->getDefaultExpire(); } + if($this->isCookieFound()) { $this->loadContentFromCookie(); @@ -139,7 +141,7 @@ class Piwik_Cookie public function save() { $this->setP3PHeader(); - $this->setCookie( $this->name, $this->generateContentString(), $this->expire); + $this->setCookie( $this->name, $this->generateContentString(), $this->expire, $this->path); } /** @@ -164,13 +166,7 @@ class Piwik_Cookie $varValue = base64_decode($varValue); // some of the values may be serialized array so we try to unserialize it - if( ($arrayValue = @unserialize($varValue)) !== false - // we set the unserialized version only for arrays as you can have set a serialized string on purpose - && is_array($arrayValue) - ) - { - $varValue = $arrayValue; - } + $varValue = Piwik_Common::unserialize_array($varValue); } $this->set($varName, $varValue); @@ -245,12 +241,12 @@ class Piwik_Cookie */ public function __toString() { - $str = "<-- Content of the cookie '{$this->name}' <br>\n"; + $str = "<-- Content of the cookie '{$this->name}' <br />\n"; foreach($this->value as $name => $value ) { - $str .= $name . " = " . var_export($this->get($name), true) . "<br>\n"; + $str .= $name . " = " . var_export($this->get($name), true) . "<br />\n"; } - $str .= "--> <br>\n"; + $str .= "--> <br />\n"; return $str; } diff --git a/core/DataFiles/SearchEngines.php b/core/DataFiles/SearchEngines.php index 7239247aea..6d467076df 100644 --- a/core/DataFiles/SearchEngines.php +++ b/core/DataFiles/SearchEngines.php @@ -1,37 +1,37 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later * @version $Id$ - * + * * @category Piwik * @package DataFiles */ /** * Search Engine database - * + * * ====================================== * HOW TO ADD A SEARCH ENGINE TO THE LIST * ====================================== * If you want to add a new entry, please email us the information + icon at hello at piwik.org * * See also: http://piwik.org/faq/general/#faq_39 - * + * * Detail of a line: * Url => array( SearchEngineName, KeywordParameter, [path containing the keyword], [charset used by the search engine]) - * + * * The main search engine URL has to be at the top of the list for the given search Engine. - * You can add new search engines icons by adding the icon in the plugins/Referers/images/SearchEngines directory + * You can add new search engines icons by adding the icon in the plugins/Referers/images/SearchEngines directory * using the format 'mainSearchEngineUrl.png'. Example: www.google.com.png - * To help Piwik link directly the search engine result page for the keyword, specify the third entry in the array + * To help Piwik link directly the search engine result page for the keyword, specify the third entry in the array * using the macro {k} that will automatically be replaced by the keyword. - * + * * A simple example is: * 'www.google.com' => array('Google', 'q', 'search?q={k}'), - * + * * A more complicated example, with an array of possible variable names, and a custom charset: * 'www.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), 's?wd={k}', 'gb2312'), */ @@ -50,10 +50,7 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // 1und1 'portal.1und1.de' => array('1und1', 'search'), - - // 3271 - 'nmsearch.3721.com' => array('3271', 'p'), - 'seek.3721.com' => array('3271', 'p'), + 'search.1und1.de' => array('1und1', 'su', 'search/web/?mc=suche%40web%40home.suche%40web&allparams=&smode=&su={k}&search=Suche&webRb='), // A9 'www.a9.com' => array('A9', ''), @@ -72,9 +69,6 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Acoon 'www.acoon.de' => array('Acoon', 'begriff'), - // Acont - 'acont.de' => array('Acont', 'query'), - // Alexa 'www.alexa.com' => array('Alexa', 'q', 'search?q={k}'), 'alexa.com' => array('Alexa', 'q'), @@ -126,7 +120,6 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'www.aolrecherches.aol.fr' => array('AOL', array('query', 'q')), 'www.aolimages.aol.fr' => array('AOL', array('query', 'q')), 'www.recherche.aol.fr' => array('AOL', array('query', 'q')), - 'aolsearcht.aol.com' => array('AOL', array('query', 'q')), 'find.web.aol.com' => array('AOL', array('query', 'q')), 'recherche.aol.ca' => array('AOL', array('query', 'q')), 'aolsearch.aol.co.uk' => array('AOL', array('query', 'q')), @@ -156,6 +149,7 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Ask 'www.ask.com' => array('Ask', array('ask', 'q'), 'web?q={k}'), 'web.ask.com' => array('Ask', array('ask', 'q')), + 'images.ask.com' => array('Ask', 'q'), 'ask.reference.com' => array('Ask', 'q'), 'www.ask.co.uk' => array('Ask', 'q'), 'uk.ask.com' => array('Ask', 'q'), @@ -198,6 +192,9 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Bing 'www.bing.com' => array('Bing', 'q', 'search?q={k}'), + // Bing Images + 'www.bing.com/images/search'=> array('Bing Images', 'q', 'search?q={k}'), + // Blogdigger 'www.blogdigger.com' => array('Blogdigger', 'q'), @@ -236,9 +233,7 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Comcast 'www.comcast.net' => array('Comcast', 'query'), 'search.comcast.net' => array('Comcast', 'q'), - - // Comet systems - 'search.cometsystems.com' => array('CometSystems', 'q'), + 'search3.comcast.com' => array('Comcast', 'url'), // Compuserve 'suche.compuserve.de' => array('Compuserve.de (Powered by Google)', 'q'), @@ -274,6 +269,9 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Deskfeeds 'www.deskfeeds.com' => array('Deskfeeds', 'sx'), + // Digg + 'digg.com' => array('Digg', 's', 'search?s={k}'), + // Dino 'www.dino-online.de' => array('Dino', 'query'), @@ -290,9 +288,16 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'search.dogpile.com' => array('Dogpile', 'q'), 'nbci.dogpile.com' => array('Dogpile', 'q'), + // DuckDuckGo + 'duckduckgo.com' => array('DuckDuckGo', 'q', '?q={k}'), + // earthlink 'search.earthlink.net' => array('Earthlink', 'q'), + // Ecosia (powered by Bing) + 'ecosia.org' => array('Ecosia', 'q', 'search.php?q={k}'), + 'www.ecosia.org' => array('Ecosia', 'q'), + // Eniro 'www.eniro.se' => array('Eniro', 'q'), @@ -323,6 +328,9 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // eo 'eo.st' => array('eo', 'q'), + // Facebook + 'www.facebook.com' => array('Facebook', 'q', 'search/?q={k}'), + // Feedminer 'www.feedminer.com' => array('Feedminer', 'q'), @@ -335,6 +343,7 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Fireball 'suche.fireball.de' => array('Fireball', 'query'), + 'www.fireball.de' => array('Fireball', 'q'), // Firstfind 'www.firstsfind.com' => array('Firstsfind', 'qry'), @@ -345,6 +354,13 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Flix 'www.flix.de' => array('Flix.de', 'keyword'), + // Forestle + 'de.forestle.org' => array('Forestle', 'q', 'search.php?q={k}'), + 'at.forestle.org' => array('Forestle', 'q', 'search.php?q={k}'), + 'ch.forestle.org' => array('Forestle', 'q', 'search.php?q={k}'), + 'us.forestle.org' => array('Forestle', 'q', 'search.php?q={k}'), + 'fr.forestle.org' => array('Forestle', 'q', 'search.php?q={k}'), + // Free 'search.free.fr' => array('Free', 'q'), 'search1-2.free.fr' => array('Free', 'q'), @@ -365,14 +381,13 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'froogle.google.co.uk' => array('Google (Froogle)', 'q'), // GAIS - 'gais.cs.ccu.edu.tw' => array('GAIS)', 'query'), + 'gais.cs.ccu.edu.tw' => array('GAIS', 'query'), // Gigablast 'www.gigablast.com' => array('Gigablast', 'q'), 'blogs.gigablast.com' => array('Gigablast (Blogs)', 'q'), 'travel.gigablast.com' => array('Gigablast (Travel)', 'q'), 'dir.gigablast.com' => array('Gigablast (Directory)', 'q'), - 'gov.gigablast.com' => array('Gigablast (Gov)', 'q'), // GMX 'suche.gmx.net' => array('GMX', 'su'), @@ -396,7 +411,10 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'www.gogole.com' => array('Google', 'q'), 'www.gppgle.com' => array('Google', 'q'), 'go.google.com' => array('Google', 'q'), + 'www.google.ad' => array('Google', 'q'), 'www.google.ae' => array('Google', 'q'), + 'www.google.am' => array('Google', 'q'), + 'www.google.it.ao' => array('Google', 'q'), 'www.google.as' => array('Google', 'q'), 'www.google.at' => array('Google', 'q'), 'wwwgoogle.at' => array('Google', 'q'), @@ -405,21 +423,28 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'www.google.az' => array('Google', 'q'), 'www.google.ba' => array('Google', 'q'), 'www.google.be' => array('Google', 'q'), + 'www.google.bf' => array('Google', 'q'), 'www.google.bg' => array('Google', 'q'), 'google.bg' => array('Google', 'q'), 'www.google.bi' => array('Google', 'q'), + 'www.google.bj' => array('Google', 'q'), + 'www.google.bs' => array('Google', 'q'), 'www.google.ca' => array('Google', 'q'), 'ww.google.ca' => array('Google', 'q'), 'w.google.ca' => array('Google', 'q'), + 'www.google.cat' => array('Google', 'q'), 'www.google.cc' => array('Google', 'q'), 'www.google.cd' => array('Google', 'q'), + 'google.cf' => array('Google', 'q'), 'www.google.cg' => array('Google', 'q'), 'www.google.ch' => array('Google', 'q'), 'ww.google.ch' => array('Google', 'q'), 'w.google.ch' => array('Google', 'q'), 'www.google.ci' => array('Google', 'q'), + 'google.co.ck' => array('Google', 'q'), 'www.google.cl' => array('Google', 'q'), 'www.google.cn' => array('Google', 'q'), + 'google.cm' => array('Google', 'q'), 'www.google.co' => array('Google', 'q'), 'www.google.cz' => array('Google', 'q'), 'wwwgoogle.cz' => array('Google', 'q'), @@ -427,11 +452,14 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'ww.google.de' => array('Google', 'q'), 'w.google.de' => array('Google', 'q'), 'wwwgoogle.de' => array('Google', 'q'), + 'google.dm' => array('Google', 'q'), + 'google.dz' => array('Google', 'q'), 'www.google.ee' => array('Google', 'q'), 'www.google.dj' => array('Google', 'q'), 'www.google.dk' => array('Google', 'q'), 'www.google.es' => array('Google', 'q'), 'www.google.fi' => array('Google', 'q'), + 'www.googel.fi' => array('Google', 'q'), 'www.google.fm' => array('Google', 'q'), 'gogole.fr' => array('Google', 'q'), 'www.gogole.fr' => array('Google', 'q'), @@ -441,35 +469,53 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'www.google.fr' => array('Google', 'q'), 'www.google.fr.' => array('Google', 'q'), 'google.fr' => array('Google', 'q'), - 'www.google.gg' => array('Google', 'q'), + 'www.google.ga' => array('Google', 'q'), 'google.ge' => array('Google', 'q'), 'w.google.ge' => array('Google', 'q'), 'ww.google.ge' => array('Google', 'q'), 'www.google.ge' => array('Google', 'q'), + 'www.google.gg' => array('Google', 'q'), 'google.gr' => array('Google', 'q'), - 'www.googel.fi' => array('Google', 'q'), 'www.google.gl' => array('Google', 'q'), 'www.google.gm' => array('Google', 'q'), + 'www.google.gp' => array('Google', 'q'), 'www.google.gr' => array('Google', 'q'), + 'www.google.gy' => array('Google', 'q'), 'www.google.hn' => array('Google', 'q'), 'www.google.hr' => array('Google', 'q'), + 'www.google.ht' => array('Google', 'q'), 'www.google.hu' => array('Google', 'q'), 'www.google.ie' => array('Google', 'q'), + 'www.google.im' => array('Google', 'q'), 'www.google.is' => array('Google', 'q'), 'www.google.it' => array('Google', 'q'), + 'www.google.je' => array('Google', 'q'), 'www.google.jo' => array('Google', 'q'), + 'www.google.ki' => array('Google', 'q'), + 'www.google.kg' => array('Google', 'q'), 'www.google.kz' => array('Google', 'q'), + 'www.google.la' => array('Google', 'q'), 'www.google.li' => array('Google', 'q'), 'www.google.lk' => array('Google', 'q'), 'www.google.lt' => array('Google', 'q'), 'www.google.lu' => array('Google', 'q'), 'www.google.lv' => array('Google', 'q'), 'www.google.md' => array('Google', 'q'), + 'www.google.me' => array('Google', 'q'), + 'www.google.mg' => array('Google', 'q'), + 'www.google.mk' => array('Google', 'q'), + 'www.google.ml' => array('Google', 'q'), + 'www.google.mn' => array('Google', 'q'), 'www.google.ms' => array('Google', 'q'), 'www.google.mu' => array('Google', 'q'), + 'www.google.mv' => array('Google', 'q'), 'www.google.mw' => array('Google', 'q'), + 'www.google.ne' => array('Google', 'q'), 'www.google.nl' => array('Google', 'q'), 'www.google.no' => array('Google', 'q'), + 'www.google.nr' => array('Google', 'q'), + 'www.google.nu' => array('Google', 'q'), + 'www.google.ps' => array('Google', 'q'), 'www.google.pl' => array('Google', 'q'), 'www.google.pn' => array('Google', 'q'), 'www.google.pt' => array('Google', 'q'), @@ -477,16 +523,26 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'www.google.rs' => array('Google', 'q'), 'www.google.ru' => array('Google', 'q'), 'www.google.rw' => array('Google', 'q'), + 'www.google.sc' => array('Google', 'q'), 'www.google.se' => array('Google', 'q'), 'www.google.sh' => array('Google', 'q'), 'www.google.si' => array('Google', 'q'), 'www.google.sk' => array('Google', 'q'), 'www.google.sm' => array('Google', 'q'), 'www.google.sn' => array('Google', 'q'), + 'www.google.st' => array('Google', 'q'), 'www.google.td' => array('Google', 'q'), + 'www.google.tg' => array('Google', 'q'), + 'www.google.tk' => array('Google', 'q'), + 'www.google.tl' => array('Google', 'q'), + 'www.google.tm' => array('Google', 'q'), + 'www.google.to' => array('Google', 'q'), 'www.google.tt' => array('Google', 'q'), 'www.google.uz' => array('Google', 'q'), + 'www.google.vu' => array('Google', 'q'), 'www.google.vg' => array('Google', 'q'), + 'www.google.ws' => array('Google', 'q'), + 'www.google.co.bw' => array('Google', 'q'), 'www.google.co.cr' => array('Google', 'q'), 'www.google.co.gg' => array('Google', 'q'), 'www.google.co.hu' => array('Google', 'q'), @@ -499,23 +555,36 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'www.google.co.ke' => array('Google', 'q'), 'www.google.co.kr' => array('Google', 'q'), 'www.google.co.ma' => array('Google', 'q'), + 'www.google.co.mz' => array('Google', 'q'), 'www.google.co.nz' => array('Google', 'q'), 'www.google.co.th' => array('Google', 'q'), + 'www.google.co.tz' => array('Google', 'q'), + 'www.google.co.ug' => array('Google', 'q'), 'www.google.co.uk' => array('Google', 'q'), + 'www.google.co.uz' => array('Google', 'q'), + 'www.google.co.vi' => array('Google', 'q'), 'www.google.co.ve' => array('Google', 'q'), 'www.google.co.za' => array('Google', 'q'), + 'www.google.co.zm' => array('Google', 'q'), 'www.google.co.zw' => array('Google', 'q'), + 'www.google.com.af' => array('Google', 'q'), + 'www.google.com.ag' => array('Google', 'q'), + 'www.google.com.ai' => array('Google', 'q'), 'www.google.com.ar' => array('Google', 'q'), 'www.google.com.au' => array('Google', 'q'), + 'www.google.com.bd' => array('Google', 'q'), 'www.google.com.bh' => array('Google', 'q'), + 'www.google.com.bn' => array('Google', 'q'), 'www.google.com.bo' => array('Google', 'q'), 'www.google.com.br' => array('Google', 'q'), 'www.google.com.by' => array('Google', 'q'), + 'www.google.com.bz' => array('Google', 'q'), 'www.google.com.co' => array('Google', 'q'), 'www.google.com.cu' => array('Google', 'q'), 'www.google.com.do' => array('Google', 'q'), 'www.google.com.ec' => array('Google', 'q'), 'www.google.com.eg' => array('Google', 'q'), + 'www.google.com.et' => array('Google', 'q'), 'www.google.com.fj' => array('Google', 'q'), 'www.google.com.gh' => array('Google', 'q'), 'www.google.com.gi' => array('Google', 'q'), @@ -523,7 +592,9 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'www.google.com.gt' => array('Google', 'q'), 'www.google.com.hk' => array('Google', 'q'), 'www.google.com.jm' => array('Google', 'q'), + 'www.google.com.kh' => array('Google', 'q'), 'www.google.com.kw' => array('Google', 'q'), + 'www.google.com.lb' => array('Google', 'q'), 'www.google.com.ly' => array('Google', 'q'), 'www.google.com.mt' => array('Google', 'q'), 'www.google.com.mx' => array('Google', 'q'), @@ -544,8 +615,11 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'www.google.com.qa' => array('Google', 'q'), 'www.google.com.ru' => array('Google', 'q'), 'www.google.com.sa' => array('Google', 'q'), + 'www.google.com.sb' => array('Google', 'q'), 'www.google.com.sg' => array('Google', 'q'), + 'www.google.com.sl' => array('Google', 'q'), 'www.google.com.sv' => array('Google', 'q'), + 'www.google.com.tj' => array('Google', 'q'), 'www.google.com.tr' => array('Google', 'q'), 'www.google.com.tw' => array('Google', 'q'), 'www.google.com.ua' => array('Google', 'q'), @@ -594,6 +668,9 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'blogsearch.google.co.in' => array('Google Blogsearch', 'q'), 'blogsearch.google.co.uk' => array('Google Blogsearch', 'q'), + // Google Custom Search + 'www.google.com/cse' => array('Google Custom Search', 'q'), + // Google translation 'translate.google.com' => array('Google Translations', 'q'), @@ -690,7 +767,7 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Goyellow.de 'www.goyellow.de' => array('GoYellow.de', 'MDN'), - // Gule Sider: + // Gule Sider 'www.gulesider.no' => array('Gule Sider', 'q'), // HighBeam @@ -726,9 +803,7 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'search.icq.com' => array('ICQ', 'q'), // Ilse - 'spsearch.ilse.nl' => array('Startpagina', 'search_for'), - 'be.ilse.nl' => array('Ilse BE', 'query'), - 'search.ilse.nl' => array('Ilse NL', 'search_for'), + 'www.ilse.nl' => array('Ilse NL', 'search_for', '?search_for={k}'), // Iwon 'search.iwon.com' => array('Iwon', 'searchfor'), @@ -885,23 +960,23 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'ariadna.elmundo.es' => array('El Mundo', 'q'), // MySpace - 'searchservice.myspace.com' => array('MySpace', 'qry'), + 'searchservice.myspace.com' => array('MySpace', 'qry', 'index.cfm?fuseaction=sitesearch.results&type=Web&qry={k}'), - // MyWebSearch - 'kf.mysearch.myway.com' => array('MyWebSearch', 'searchfor'), + // MySearch / MyWay / MyWebSearch (default: powered by Ask.com) + 'www.mysearch.com' => array('MyWebSearch', 'searchfor', 'search/Ajmain.jhtml?searchfor={k}'), 'ms114.mysearch.com' => array('MyWebSearch', 'searchfor'), 'ms146.mysearch.com' => array('MyWebSearch', 'searchfor'), - 'mysearch.myway.com' => array('MyWebSearch', 'searchfor'), - 'searchfr.myway.com' => array('MyWebSearch', 'searchfor'), + 'kf.mysearch.myway.com' => array('MyWebSearch', 'searchfor'), 'ki.mysearch.myway.com' => array('MyWebSearch', 'searchfor'), - 'search.mywebsearch.com' => array('MyWebSearch', 'searchfor'), - 'www.mywebsearch.com' => array('MyWebSearch', 'searchfor'), + 'search.myway.com' => array('MyWebSearch', 'searchfor'), + 'search.mywebsearch.com' => array('MyWebSearch', 'searchfor', 'mywebsearch/Ajmain.jhtml?searchfor={k}'), + // Najdi 'www.najdi.si' => array('Najdi.si', 'q'), // Naver - 'search.naver.com' => array('Naver', 'query'), + 'search.naver.com' => array('Naver', 'query', 'search.naver?query={k}', 'x-windows-949'), // Needtofind 'ko.search.need2find.com' => array('Needtofind', 'searchfor'), @@ -939,7 +1014,7 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Onet 'szukaj.onet.pl' => array('Onet.pl', 'qt'), - // Online.no: + // Online.no 'www.online.no' => array('Online.no', 'q'), 'online.no' => array('Online.no', 'q'), @@ -951,7 +1026,7 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'bbs2.openfind.com.tw' => array('Openfind (BBS)', 'query'), 'news.openfind.com.tw' => array('Openfind (News)', 'query'), - // Opplysningen 1881: + // Opplysningen 1881 'www.1881.no' => array('Opplysningen 1881', 'Query'), // Overture @@ -981,9 +1056,9 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'data.quicksearches.net' => array('QuickSearches', 'q'), // Qualigo - 'www.qualigo.de' => array('Qualigo', 'q'), - 'www.qualigo.ch' => array('Qualigo', 'q'), 'www.qualigo.at' => array('Qualigo', 'q'), + 'www.qualigo.ch' => array('Qualigo', 'q'), + 'www.qualigo.de' => array('Qualigo', 'q'), 'www.qualigo.nl' => array('Qualigo', 'q'), // Rambler @@ -994,6 +1069,11 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Reacteur.com 'www.reacteur.com' => array('Reacteur', 'kw'), + // RPMFind + 'www.rpmfind.net' => array('rpmfind', 'query', 'linux/rpm2html/search.php?query={k}'), + 'rpmfind.net' => array('rpmfind', 'query'), + 'fr2.rpmfind.net' => array('rpmfind', 'query'), + // Sapo 'pesquisa.sapo.pt' => array('Sapo', 'q'), @@ -1003,36 +1083,13 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Search.ch 'www.search.ch' => array('Search.ch', 'q'), - // Search a lot - 'www.searchalot.com' => array('Searchalot', 'query'), + // Searchalot + 'www.searchalot.com' => array('Searchalot', 'q', '?q={k}'), + 'searchalot.com' => array('Searchalot', 'q'), // Seek 'www.seek.fr' => array('Searchalot', 'qry_str'), - // Seekport - 'www.seekport.de' => array('Seekport', 'query'), - 'www.seekport.co.uk' => array('Seekport', 'query'), - 'www.seekport.fr' => array('Seekport', 'query'), - 'www.seekport.at' => array('Seekport', 'query'), - 'www.seekport.es' => array('Seekport', 'query'), - 'www.seekport.it' => array('Seekport', 'query'), - - // Seekport (blogs) - 'blogs.seekport.de' => array('Seekport (Blogs)', 'query'), - 'blogs.seekport.co.uk' => array('Seekport (Blogs)', 'query'), - 'blogs.seekport.fr' => array('Seekport (Blogs)', 'query'), - 'blogs.seekport.at' => array('Seekport (Blogs)', 'query'), - 'blogs.seekport.es' => array('Seekport (Blogs)', 'query'), - 'blogs.seekport.it' => array('Seekport (Blogs)', 'query'), - - // Seekport (news) - 'news.seekport.de' => array('Seekport (News)', 'query'), - 'news.seekport.co.uk' => array('Seekport (News)', 'query'), - 'news.seekport.fr' => array('Seekport (News)', 'query'), - 'news.seekport.at' => array('Seekport (News)', 'query'), - 'news.seekport.es' => array('Seekport (News)', 'query'), - 'news.seekport.it' => array('Seekport (News)', 'query'), - // Searchscout 'www.searchscout.com' => array('Search Scout', 'gt_keywords'), @@ -1046,13 +1103,13 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'bg.setooz.com' => array('Setooz', 'query'), 'el.setooz.com' => array('Setooz', 'query'), 'et.setooz.com' => array('Setooz', 'query'), - 'lv.setooz.com' => array('Setooz', 'query'), - 'lt.setooz.com' => array('Setooz', 'query'), + 'fi.setooz.com' => array('Setooz', 'query'), 'hu.setooz.com' => array('Setooz', 'query'), + 'lt.setooz.com' => array('Setooz', 'query'), + 'lv.setooz.com' => array('Setooz', 'query'), 'no.setooz.com' => array('Setooz', 'query'), 'pl.setooz.com' => array('Setooz', 'query'), 'sk.setooz.com' => array('Setooz', 'query'), - 'fi.setooz.com' => array('Setooz', 'query'), 'sv.setooz.com' => array('Setooz', 'query'), 'tr.setooz.com' => array('Setooz', 'query'), 'uk.setooz.com' => array('Setooz', 'query'), @@ -1069,8 +1126,11 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Skynet 'search.skynet.be' => array('Skynet', 'keywords'), + // Sogou + 'www.sogou.com' => array('Sogou', 'query', 'web?query={k}'), + // soso.com - 'www.soso.com' => array('Soso', 'w'), + 'www.soso.com' => array('Soso', 'w', 'q?w={k}', 'gb2312'), // Sphere 'www.sphere.com' => array('Sphere', 'q'), @@ -1109,8 +1169,12 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'search-dyn.tiscali.de' => array('Tiscali', 'key'), 'hledani.tiscali.cz' => array('Tiscali', 'query', false, 'windows-1250'), + // Tixuma + 'www.tixuma.de' => array('Tixuma', 'sc', 'index.php?mp=search&stp=&sc={k}&tg=0'), + // T-Online 'suche.t-online.de' => array('T-Online', 'q'), + 'navigationshilfe.t-online.de'=> array('T-Online', 'q', 'dtag/dns/results?mode=search_top&q={k}'), // Trouvez.com 'www.trouvez.com' => array('Trouvez.com', 'query'), @@ -1156,9 +1220,31 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'suche.web.de' => array('Web.de (Websuche)', 'su'), 'dir.web.de' => array('Web.de (Directory)', 'su'), + // Web.nl + 'www.web.nl' => array('Web.nl', 'query'), + + // Weborama + 'www.weborama.fr' => array('weborama', 'query'), + + // WebSearch + 'is1.websearch.com' => array('WebSearch', 'qkw'), + 'www.websearch.com' => array('WebSearch', 'qkw'), + // Webtip 'www.webtip.de' => array('Webtip', 'keyword'), + // Wedoo + 'fr.wedoo.com' => array('Wedoo', 'keyword'), + + // Witch + 'www.witch.de' => array('Witch', 'search'), + + // WXS + 'wxsl.nl' => array('Planet Internet', 'q'), + + // WWW + 'search.www.ee' => array('www värav', 'query'), + // X-recherche 'www.x-recherche.com' => array('X-Recherche', 'mots'), @@ -1174,28 +1260,32 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'au.search.yahoo.com' => array('Yahoo!', 'p'), 'br.search.yahoo.com' => array('Yahoo!', 'p'), 'ch.search.yahoo.com' => array('Yahoo!', 'p'), - 'de.search.yahoo.com' => array('Yahoo!', 'p'), 'ca.search.yahoo.com' => array('Yahoo!', 'p'), + 'cade.search.yahoo.com' => array('Yahoo!', 'p'), 'cf.search.yahoo.com' => array('Yahoo!', 'p'), - 'fr.search.yahoo.com' => array('Yahoo!', 'p'), - 'espanol.search.yahoo.com' => array('Yahoo!', 'p'), + 'de.search.yahoo.com' => array('Yahoo!', 'p'), 'es.search.yahoo.com' => array('Yahoo!', 'p'), + 'espanol.search.yahoo.com' => array('Yahoo!', 'p'), + 'fr.search.yahoo.com' => array('Yahoo!', 'p'), + 'hk.search.yahoo.com' => array('Yahoo!', 'p'), 'id.search.yahoo.com' => array('Yahoo!', 'p'), 'it.search.yahoo.com' => array('Yahoo!', 'p'), 'kr.search.yahoo.com' => array('Yahoo!', 'p'), 'mx.search.yahoo.com' => array('Yahoo!', 'p'), 'nl.search.yahoo.com' => array('Yahoo!', 'p'), 'qc.search.yahoo.com' => array('Yahoo!', 'p'), - 'uk.search.yahoo.com' => array('Yahoo!', 'p'), - 'cade.search.yahoo.com' => array('Yahoo!', 'p'), - 'tw.search.yahoo.com' => array('Yahoo!', 'p'), + 'ru.search.yahoo.com' => array('Yahoo!', 'p'), 'se.search.yahoo.com' => array('Yahoo!', 'p'), + 'tw.search.yahoo.com' => array('Yahoo!', 'p'), + 'uk.search.yahoo.com' => array('Yahoo!', 'p'), 'us.search.yahoo.com' => array('Yahoo!', 'p'), - 'ru.search.yahoo.com' => array('Yahoo!', 'p'), - 'www.yahoo.com.cn' => array('Yahoo!', 'p'), + 'search.cn.yahoo.com' => array('Yahoo!', 'p'), + 'one.cn.yahoo.com' => array('Yahoo!', 'p'), + 'cns.3721.com' => array('Yahoo!', 'p'), // acquired by Yahoo! 'au.yhs.search.yahoo.com' => array('Yahoo!', 'p', 'avg/search?p={k}'), - + 'de.yhs.search.yahoo.com' => array('Yahoo!', 'p', 'avg/search?p={k}'), + 'us.yhs.search.yahoo.com' => array('Yahoo!', 'p', 'avg/search?p={k}'), 'de.dir.yahoo.com' => array('Yahoo! Webverzeichnis', ''), 'cf.dir.yahoo.com' => array('Yahoo! Directory', ''), 'fr.dir.yahoo.com' => array('Yahoo! Directory', ''), @@ -1205,6 +1295,7 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) // Yandex 'yandex.ru' => array('Yandex', 'text', 'yandsearch?text={k}'), + 'yandex.ua' => array('Yandex', 'text'), 'www.yandex.ru' => array('Yandex', 'text'), 'search.yaca.yandex.ru' => array('Yandex', 'text'), 'ya.ru' => array('Yandex', 'text'), @@ -1224,33 +1315,6 @@ if(!isset($GLOBALS['Piwik_SearchEngines'] )) 'www.yellowmap.de' => array('Yellowmap', ' '), 'yellowmap.de' => array('Yellowmap', ' '), - // Wanadoo - 'search.ke.wanadoo.fr' => array('Wanadoo', 'kw'), - 'busca.wanadoo.es' => array('Wanadoo', 'buscar'), - - // Wedoo - 'fr.wedoo.com' => array('Wedoo', 'keyword'), - - // Web.nl - 'www.web.nl' => array('Web.nl', 'query'), - - // Weborama - 'www.weborama.fr' => array('weborama', 'query'), - - // WebSearch - 'is1.websearch.com' => array('WebSearch', 'qkw'), - 'www.websearch.com' => array('WebSearch', 'qkw'), - 'websearch.cs.com' => array('WebSearch', 'query'), - - // Witch - 'www.witch.de' => array('Witch', 'search'), - - // WWW - 'search.www.ee' => array('www värav', 'query'), - - // WXS - 'wxsl.nl' => array('Planet Internet', 'q'), - // Zoek 'www3.zoek.nl' => array('Zoek', 'q'), diff --git a/core/DataTable.php b/core/DataTable.php index a5590e1cbb..13f7f1e3ff 100644 --- a/core/DataTable.php +++ b/core/DataTable.php @@ -588,14 +588,13 @@ class Piwik_DataTable */ public function getRowsCount() { - $count = count($this->rows); if(is_null($this->summaryRow)) { - return $count; + return count($this->rows); } else { - return $count + 1; + return count($this->rows) + 1; } } @@ -820,10 +819,7 @@ class Piwik_DataTable $table1->rebuildIndex(); $table2->rebuildIndex(); - $countrows1 = $table1->getRowsCount(); - $countrows2 = $table2->getRowsCount(); - - if($countrows1 != $countrows2) + if($table1->getRowsCount() != $table2->getRowsCount()) { return false; } @@ -831,11 +827,8 @@ class Piwik_DataTable foreach($rows1 as $row1) { $row2 = $table2->getRowFromLabel($row1->getColumn('label')); - if($row2 === false) - { - return false; - } - if( !Piwik_DataTable_Row::isEqual($row1,$row2) ) + if($row2 === false + || !Piwik_DataTable_Row::isEqual($row1,$row2)) { return false; } diff --git a/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php b/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php index 1e1e703b66..1b126e42a5 100644 --- a/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php +++ b/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php @@ -17,7 +17,13 @@ class Piwik_DataTable_Filter_AddColumnsWhenShowAllColumns extends Piwik_DataTable_Filter { protected $roundPrecision = 1; - public function __construct( $table ) + + /** + * @param $table + * @param $enable Automatically set to true when filter_add_columns_when_show_all_columns is found in the API request + * @return void + */ + public function __construct( $table, $enable = true ) { parent::__construct($table); $this->filter(); diff --git a/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php b/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php index 90ea6b9ee5..d63d8298e3 100644 --- a/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php +++ b/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php @@ -19,61 +19,16 @@ * You can also specify the precision of the percentage value to be displayed (defaults to 0, eg "11%") * * Usage: - * $nbVisits = Piwik_VisitsSummary_API::getVisits($idSite, $period, $date); + * $nbVisits = Piwik_VisitsSummary_API::getInstance()->getVisits($idSite, $period, $date); * $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('nb_visits', 'nb_visits_percentage', $nbVisits, 1)); * * @package Piwik * @subpackage Piwik_DataTable */ -class Piwik_DataTable_Filter_ColumnCallbackAddColumnPercentage extends Piwik_DataTable_Filter +class Piwik_DataTable_Filter_ColumnCallbackAddColumnPercentage extends Piwik_DataTable_Filter_ColumnCallbackAddColumnQuotient { - private $columnValueToRead; - private $columnNamePercentageToAdd; - private $columnNameUsedAsDivisor; - private $totalValueUsedAsDivisor; - private $percentagePrecision; - - /** - * @param Piwik_DataTable $table - * @param string $columnValueToRead - * @param string $columnNamePercentageToAdd - * @param numeric|string $totalValueUsedToComputePercentageOrColumnName - * if a numeric value is given, we use this value as the divisor to process the percentage. - * if a string is given, this string is the column name's value used as the divisor. - * @param int $percentagePrecision precision 0 means "11", 1 means "11.2" - */ - public function __construct( $table, $columnValueToRead, $columnNamePercentageToAdd, $totalValueUsedToComputePercentageOrColumnName, $percentagePrecision = 0 ) + protected function formatValue($value, $divisor) { - parent::__construct($table); - $this->columnValueToRead = $columnValueToRead; - $this->columnNamePercentageToAdd = $columnNamePercentageToAdd; - if(is_numeric($totalValueUsedToComputePercentageOrColumnName)) - { - $this->totalValueUsedAsDivisor = $totalValueUsedToComputePercentageOrColumnName; - } - else - { - $this->columnNameUsedAsDivisor = $totalValueUsedToComputePercentageOrColumnName; - } - $this->percentagePrecision = $percentagePrecision; - $this->filter(); - } - - protected function filter() - { - foreach($this->table->getRows() as $key => $row) - { - $value = $row->getColumn($this->columnValueToRead); - if(!is_null($this->totalValueUsedAsDivisor)) - { - $divisor = $this->totalValueUsedAsDivisor; - } - else - { - $divisor = $row->getColumn($this->columnNameUsedAsDivisor); - } - $percentage = Piwik::getPercentageSafe($value, $divisor, $this->percentagePrecision); - $row->addColumn($this->columnNamePercentageToAdd, $percentage); - } + return Piwik::getPercentageSafe($value, $divisor, $this->quotientPrecision) . '%'; } } diff --git a/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php b/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php new file mode 100644 index 0000000000..1e389360dc --- /dev/null +++ b/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php @@ -0,0 +1,80 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Piwik + */ + +/** + * Adds a new column that is a division of two columns of the current row. + * Useful to process bounce rates, exit rates, average time on page, etc. + * + * @package Piwik + * @subpackage Piwik_DataTable + */ +class Piwik_DataTable_Filter_ColumnCallbackAddColumnQuotient extends Piwik_DataTable_Filter +{ + protected $columnValueToRead; + protected $columnNameToAdd; + protected $columnNameUsedAsDivisor; + protected $totalValueUsedAsDivisor; + protected $quotientPrecision; + + /** + * @param Piwik_DataTable $table + * @param string $columnValueToRead + * @param string $columnNameToAdd + * @param numeric|string $divisorValueOrDivisorColumnName + * if a numeric value is given, we use this value as the divisor to process the percentage. + * if a string is given, this string is the column name's value used as the divisor. + * @param numeric $quotientPrecision Division precision + */ + public function __construct( $table, $columnNameToAdd, $columnValueToRead, $divisorValueOrDivisorColumnName, $quotientPrecision = 0) + { + parent::__construct($table); + $this->columnValueToRead = $columnValueToRead; + $this->columnNameToAdd = $columnNameToAdd; + if(is_numeric($divisorValueOrDivisorColumnName)) + { + $this->totalValueUsedAsDivisor = $divisorValueOrDivisorColumnName; + } + else + { + $this->columnNameUsedAsDivisor = $divisorValueOrDivisorColumnName; + } + $this->filter(); + } + + protected function filter() + { + foreach($this->table->getRows() as $key => $row) + { + $value = $row->getColumn($this->columnValueToRead); + if(!is_null($this->totalValueUsedAsDivisor)) + { + $divisor = $this->totalValueUsedAsDivisor; + } + else + { + $divisor = $row->getColumn($this->columnNameUsedAsDivisor); + } + $formattedValue = $this->formatValue($value, $divisor); + $row->addColumn($this->columnNameToAdd, $formattedValue); + } + } + + protected function formatValue($value, $divisor) + { + $quotient = 0; + if($divisor > 0 && $value > 0) + { + $quotient = round($value / $divisor, $this->quotientPrecision); + } + return $quotient; + } +} diff --git a/core/DataTable/Filter/ColumnCallbackAddMetadata.php b/core/DataTable/Filter/ColumnCallbackAddMetadata.php index bef01a9945..d90079e3f1 100644 --- a/core/DataTable/Filter/ColumnCallbackAddMetadata.php +++ b/core/DataTable/Filter/ColumnCallbackAddMetadata.php @@ -27,7 +27,7 @@ class Piwik_DataTable_Filter_ColumnCallbackAddMetadata extends Piwik_DataTable_F private $functionParameters; private $metadataToAdd; - public function __construct( $table, $columnToRead, $metadataToAdd, $functionToApply, $functionParameters = null ) + public function __construct( $table, $columnToRead, $metadataToAdd, $functionToApply = null, $functionParameters = null ) { parent::__construct($table); $this->functionToApply = $functionToApply; @@ -47,7 +47,14 @@ class Piwik_DataTable_Filter_ColumnCallbackAddMetadata extends Piwik_DataTable_F { $parameters = array_merge($parameters, $this->functionParameters); } - $newValue = call_user_func_array( $this->functionToApply, $parameters); + if(!is_null($this->functionToApply)) + { + $newValue = call_user_func_array( $this->functionToApply, $parameters); + } + else + { + $newValue = $oldValue; + } $row->addMetadata($this->metadataToAdd, $newValue); } } diff --git a/core/DataTable/Filter/ColumnCallbackDeleteRow.php b/core/DataTable/Filter/ColumnCallbackDeleteRow.php index c9ed614bb0..14468b14ef 100644 --- a/core/DataTable/Filter/ColumnCallbackDeleteRow.php +++ b/core/DataTable/Filter/ColumnCallbackDeleteRow.php @@ -34,8 +34,7 @@ class Piwik_DataTable_Filter_ColumnCallbackDeleteRow extends Piwik_DataTable_Fil foreach($this->table->getRows() as $key => $row) { $columnValue = $row->getColumn($this->columnToFilter); - if( $columnValue !== false - && !call_user_func( $this->function, $columnValue)) + if( !call_user_func( $this->function, $columnValue)) { $this->table->deleteRow($key); } diff --git a/core/DataTable/Filter/ColumnCallbackReplace.php b/core/DataTable/Filter/ColumnCallbackReplace.php index 6b286f228c..0c0e05b67e 100644 --- a/core/DataTable/Filter/ColumnCallbackReplace.php +++ b/core/DataTable/Filter/ColumnCallbackReplace.php @@ -35,7 +35,13 @@ class Piwik_DataTable_Filter_ColumnCallbackReplace extends Piwik_DataTable_Filte { foreach($this->table->getRows() as $key => $row) { - $parameters = array($this->getElementToReplace($row, $this->columnToFilter)); + // when a value is not defined, we set it to zero by default (rather than displaying '-') + $value = $this->getElementToReplace($row, $this->columnToFilter); + if($value === false) + { + $value = 0; + } + $parameters = array($value); if(!is_null($this->functionParameters)) { $parameters = array_merge($parameters, $this->functionParameters); diff --git a/core/DataTable/Filter/ColumnDelete.php b/core/DataTable/Filter/ColumnDelete.php new file mode 100644 index 0000000000..c300f11d6d --- /dev/null +++ b/core/DataTable/Filter/ColumnDelete.php @@ -0,0 +1,36 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Piwik + */ + +/** + * Deletes a column from a datatable + * + * @package Piwik + * @subpackage Piwik_DataTable + */ +class Piwik_DataTable_Filter_ColumnDelete extends Piwik_DataTable_Filter +{ + private $columnToFilter; + private $functionToApply; + + public function __construct( $table, $columnToDelete ) + { + parent::__construct($table); + $this->columnToDelete = $columnToDelete; + $this->filter(); + } + + protected function filter() + { + $this->table->deleteColumn($this->columnToDelete); + } + +} diff --git a/core/DataTable/Filter/ReplaceColumnNames.php b/core/DataTable/Filter/ReplaceColumnNames.php index 8671f2f81a..3865cace7c 100644 --- a/core/DataTable/Filter/ReplaceColumnNames.php +++ b/core/DataTable/Filter/ReplaceColumnNames.php @@ -77,9 +77,10 @@ class Piwik_DataTable_Filter_ReplaceColumnNames extends Piwik_DataTable_Filter $newColumns = array(); foreach($columns as $columnName => $columnValue) { - if(isset(Piwik_Archive::$mappingFromIdToName[$columnName])) + if(isset($this->mappingToApply[$columnName])) { - $columnName = Piwik_Archive::$mappingFromIdToName[$columnName]; + $columnName = $this->mappingToApply[$columnName]; + if($columnName == 'goals') { $newSubColumns = array(); diff --git a/core/DataTable/Filter/Sort.php b/core/DataTable/Filter/Sort.php index eabf22415d..2b5dbff5ea 100644 --- a/core/DataTable/Filter/Sort.php +++ b/core/DataTable/Filter/Sort.php @@ -161,6 +161,10 @@ class Piwik_DataTable_Filter_Sort extends Piwik_DataTable_Filter return; } $row = current($rows); + if($row === false) + { + return; + } $this->columnToSort = $this->selectColumnToSort($row); $value = $row->getColumn($this->columnToSort); diff --git a/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php b/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php index 47caecf80d..26e296e2e2 100644 --- a/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php +++ b/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php @@ -16,12 +16,22 @@ */ class Piwik_DataTable_Filter_UpdateColumnsWhenShowAllGoals extends Piwik_DataTable_Filter { + const GOALS_OVERVIEW = -1; + const GOALS_FULL_TABLE = 0; + protected $mappingIdToNameGoal; - public function __construct( $table, $mappingToApply = null ) + /** + * @param $table + * @param $enable Automatically set to true when filter_update_columns_when_show_all_goals is found in the API request + * @param $processOnlyIdGoal + * @return unknown_type + */ + public function __construct( $table, $enable = true, $processOnlyIdGoal ) { parent::__construct($table); $this->mappingIdToNameGoal = Piwik_Archive::$mappingFromIdToNameGoal; + $this->processOnlyIdGoal = $processOnlyIdGoal; $this->filter(); } @@ -36,14 +46,13 @@ class Piwik_DataTable_Filter_UpdateColumnsWhenShowAllGoals extends Piwik_DataTab $newColumns = array(); $nbVisits = 0; - // visits could be undefined when there is a convertion but no visit + // visits could be undefined when there is a conversion but no visit if(isset($currentColumns[Piwik_Archive::INDEX_NB_VISITS])) { $nbVisits = $currentColumns[Piwik_Archive::INDEX_NB_VISITS]; } $newColumns['nb_visits'] = $nbVisits; $newColumns['label'] = $currentColumns['label']; - if(isset($currentColumns[Piwik_Archive::INDEX_GOALS])) { $nbVisitsConverted = $revenue = 0; @@ -61,6 +70,7 @@ class Piwik_DataTable_Filter_UpdateColumnsWhenShowAllGoals extends Piwik_DataTab { $conversionRate = round(100 * $nbVisitsConverted / $nbVisits, $roundingPrecision); } + $newColumns['goals_conversion_rate'] = $conversionRate; if($nbVisits == 0) { @@ -70,8 +80,16 @@ class Piwik_DataTable_Filter_UpdateColumnsWhenShowAllGoals extends Piwik_DataTab { $revenuePerVisit = round( $revenue / $nbVisits, $roundingPrecision ); } + $newColumns['revenue_per_visit'] = $revenuePerVisit; foreach($currentColumns[Piwik_Archive::INDEX_GOALS] as $goalId => $columnValue) { + if($this->processOnlyIdGoal > self::GOALS_FULL_TABLE + && $this->processOnlyIdGoal != $goalId) + { + continue; + } + + // Goal Conversion rate $name = 'goal_' . $goalId . '_conversion_rate'; if($nbVisits == 0) { @@ -84,12 +102,32 @@ class Piwik_DataTable_Filter_UpdateColumnsWhenShowAllGoals extends Piwik_DataTab $newColumns[$name] = $value; $expectedColumns[$name] = true; + // When the table is displayed by clicking on the flag icon, we only display the columns + // Visits, Conversions, Per goal conversion rate, Revenue + if($this->processOnlyIdGoal == self::GOALS_OVERVIEW) + { + continue; + } + + // Goal Conversions $name = 'goal_' . $goalId . '_nb_conversions'; $newColumns[$name] = $columnValue[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; $expectedColumns[$name] = true; + + // Goal Revenue per visit + $name = 'goal_' . $goalId . '_revenue_per_visit'; + if($nbVisits == 0) + { + $value = $invalidDivision; + } + else + { + $revenuePerVisit = round( $columnValue[Piwik_Archive::INDEX_GOAL_REVENUE] / $nbVisits, $roundingPrecision ); + } + $newColumns[$name] = $revenuePerVisit; + $expectedColumns[$name] = true; + } - $newColumns['revenue_per_visit'] = $revenuePerVisit; - $newColumns['goals_conversion_rate'] = $conversionRate; } $row->setColumns($newColumns); diff --git a/core/DataTable/Manager.php b/core/DataTable/Manager.php index 8e3506187e..54976f014c 100644 --- a/core/DataTable/Manager.php +++ b/core/DataTable/Manager.php @@ -122,22 +122,22 @@ class Piwik_DataTable_Manager */ public function dumpAllTables() { - echo "<hr>Piwik_DataTable_Manager->dumpAllTables()<br>"; + echo "<hr />Piwik_DataTable_Manager->dumpAllTables()<br />"; foreach($this->tables as $id => $table) { if(!($table instanceof Piwik_DataTable )) { - echo "Error table $id is not instance of datatable<br>"; + echo "Error table $id is not instance of datatable<br />"; var_dump($table); } else { - echo "<hr>"; - echo "Table (index=$id) TableId = ". $table->getId() . "<br>"; + echo "<hr />"; + echo "Table (index=$id) TableId = ". $table->getId() . "<br />"; echo $table; - echo "<br>"; + echo "<br />"; } } - echo "<br>-- End Piwik_DataTable_Manager->dumpAllTables()<hr>"; + echo "<br />-- End Piwik_DataTable_Manager->dumpAllTables()<hr />"; } } diff --git a/core/DataTable/Renderer.php b/core/DataTable/Renderer.php index c9458f16a3..5f1a42671f 100644 --- a/core/DataTable/Renderer.php +++ b/core/DataTable/Renderer.php @@ -24,8 +24,13 @@ abstract class Piwik_DataTable_Renderer { protected $table; + protected $exception; protected $renderSubTables = false; + public function __construct() + { + } + public function setRenderSubTables($enableRenderSubTable) { $this->renderSubTables = (bool)$enableRenderSubTable; @@ -44,6 +49,13 @@ abstract class Piwik_DataTable_Renderer abstract public function render(); /** + * Computes the exception output and returns the string/binary + * + * @return string + */ + abstract public function renderException(); + + /** * @see render() * @return string */ @@ -67,6 +79,19 @@ abstract class Piwik_DataTable_Renderer } /** + * Set the Exception to be rendered + * @param Exception $exception to be rendered + */ + public function setException($exception) + { + if(!($exception instanceof Exception)) + { + throw new Exception("The exception renderer accepts only an Exception object."); + } + $this->exception = $exception; + } + + /** * Returns the DataTable associated to the output format $name * * @throws exception If the renderer is unknown @@ -75,18 +100,25 @@ abstract class Piwik_DataTable_Renderer static public function factory( $name ) { $name = ucfirst(strtolower($name)); - $path = PIWIK_INCLUDE_PATH .'/core/DataTable/Renderer/'.$name.'.php'; $className = 'Piwik_DataTable_Renderer_' . $name; - if( Piwik_Common::isValidFilename($name) - && Zend_Loader::isReadable($path) ) - { - require_once $path; // prefixed by PIWIK_INCLUDE_PATH + try { + Piwik_Loader::autoload($className); return new $className; - } - else - { - throw new Exception("Renderer format '$name' not valid. Try 'xml' or 'json' or 'csv' or 'html' or 'php' or 'original' instead."); + } catch(Exception $e) { + $availableRenderers = 'xml, json, csv, tsv, html, php, original'; + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidRendererFormat', array($name, $availableRenderers))); } - } -} + } + + /** + * Returns $rawData after all applicable characters have been converted to HTML entities. + * + * @param String $rawData to be converted + * @return String + */ + static protected function renderHtmlEntities( $rawData ) + { + return htmlentities($rawData, ENT_COMPAT, "UTF-8"); + } +}
\ No newline at end of file diff --git a/core/DataTable/Renderer/Console.php b/core/DataTable/Renderer/Console.php index 0929de6744..7d0fb5f001 100644 --- a/core/DataTable/Renderer/Console.php +++ b/core/DataTable/Renderer/Console.php @@ -25,6 +25,12 @@ class Piwik_DataTable_Renderer_Console extends Piwik_DataTable_Renderer return $this->renderTable($this->table); } + function renderException() + { + $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage()); + return 'Error: '.$exceptionMessage; + } + function setPrefixRow($str) { $this->prefixRows = $str; @@ -32,25 +38,25 @@ class Piwik_DataTable_Renderer_Console extends Piwik_DataTable_Renderer protected function renderDataTableArray(Piwik_DataTable_Array $tableArray, $prefix ) { - $output = "Piwik_DataTable_Array<hr>"; + $output = "Piwik_DataTable_Array<hr />"; $prefix = $prefix . ' '; foreach($tableArray->getArray() as $descTable => $table) { - $output .= $prefix . "<b>". $descTable. "</b><br>"; + $output .= $prefix . "<b>". $descTable. "</b><br />"; $output .= $prefix . $this->renderTable($table, $prefix . ' '); - $output .= "<hr>"; + $output .= "<hr />"; } - $output .= "Metadata<br>"; + $output .= "Metadata<br />"; foreach($tableArray->metadata as $id => $metadata) { - $output .= "<br>"; - $output .= $prefix . " <b>$id</b> <br>"; + $output .= "<br />"; + $output .= $prefix . " <b>$id</b><br />"; foreach($metadata as $name => $value) { $output .= $prefix . $prefix . "$name => $value"; } } - $output .= "<hr>"; + $output .= "<hr />"; return $output; } @@ -63,7 +69,7 @@ class Piwik_DataTable_Renderer_Console extends Piwik_DataTable_Renderer if($table->getRowsCount() == 0) { - return "Empty table <br>\n"; + return "Empty table<br />\n"; } static $depth=0; @@ -103,7 +109,7 @@ class Piwik_DataTable_Renderer_Console extends Piwik_DataTable_Renderer $output.= str_repeat($this->prefixRows, $depth) . "- $i [".$columns."] [".$metadata."] [idsubtable = " - . $row->getIdSubDataTable()."]<br>\n"; + . $row->getIdSubDataTable()."]<br />\n"; if($row->getIdSubDataTable() !== null) { @@ -116,7 +122,7 @@ class Piwik_DataTable_Renderer_Console extends Piwik_DataTable_Renderer $prefix . ' ' ); } catch(Exception $e) { - $output.= "-- Sub DataTable not loaded<br>\n"; + $output.= "-- Sub DataTable not loaded<br />\n"; } $depth--; } diff --git a/core/DataTable/Renderer/Csv.php b/core/DataTable/Renderer/Csv.php index e756b518e6..3ff46a3dd4 100644 --- a/core/DataTable/Renderer/Csv.php +++ b/core/DataTable/Renderer/Csv.php @@ -31,7 +31,7 @@ class Piwik_DataTable_Renderer_Csv extends Piwik_DataTable_Renderer * * @var string */ - public $separator = ','; + public $separator = ","; /** * Line end @@ -61,11 +61,27 @@ class Piwik_DataTable_Renderer_Csv extends Piwik_DataTable_Renderer */ public $exportIdSubtable = true; - function render() + public function render() { return $this->output($this->renderTable($this->table)); } + function renderException() + { + $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage()); + return 'Error: '.$exceptionMessage; + } + + public function setConvertToUnicode($bool) + { + $this->convertToUnicode = $bool; + } + + public function setSeparator($separator) + { + $this->separator = $separator; + } + protected function renderTable($table) { if($table instanceof Piwik_DataTable_Array) @@ -234,7 +250,7 @@ class Piwik_DataTable_Renderer_Csv extends Piwik_DataTable_Renderer $value = 0; } if(strpos($value, '"') !== false - || strpos($value, ',') !== false ) + || strpos($value, $this->separator) !== false ) { $value = '"'. str_replace('"', '""', $value). '"'; } @@ -248,7 +264,7 @@ class Piwik_DataTable_Renderer_Csv extends Piwik_DataTable_Renderer return 'No data available'; } // silent fail otherwise unit tests fail - @header("Content-type: application/vnd.ms-excel"); + @header("Content-Type: application/vnd.ms-excel"); @header("Content-Disposition: attachment; filename=piwik-report-export.csv"); if($this->convertToUnicode && function_exists('mb_convert_encoding')) diff --git a/core/DataTable/Renderer/Html.php b/core/DataTable/Renderer/Html.php index 62739fd035..2ebc36c0ce 100644 --- a/core/DataTable/Renderer/Html.php +++ b/core/DataTable/Renderer/Html.php @@ -38,6 +38,12 @@ class Piwik_DataTable_Renderer_Html extends Piwik_DataTable_Renderer return $this->renderTable($this->table); } + function renderException() + { + $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage()); + return nl2br($exceptionMessage); + } + protected function renderTable($table) { if($table instanceof Piwik_DataTable_Array) @@ -96,7 +102,7 @@ class Piwik_DataTable_Renderer_Html extends Piwik_DataTable_Renderer if(count($metadata) != 0) { $someMetadata = true; - $metadata = implode("<br>", $metadata); + $metadata = implode("<br />", $metadata); $this->tableStructure[$i]['_metadata'] = $metadata; } diff --git a/core/DataTable/Renderer/Json.php b/core/DataTable/Renderer/Json.php index 5febed6a88..0ce45db6ba 100644 --- a/core/DataTable/Renderer/Json.php +++ b/core/DataTable/Renderer/Json.php @@ -21,9 +21,21 @@ class Piwik_DataTable_Renderer_Json extends Piwik_DataTable_Renderer { public function render() { + Piwik_DataTable_Renderer_Json::renderHeader(); return $this->renderTable($this->table); } + function renderException() + { + Piwik_DataTable_Renderer_Json::renderHeader(); + + $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage()); + $exceptionMessage = str_replace("\n", "", $exceptionMessage); + $exceptionMessage = '{"result":"error", "message":"'.$exceptionMessage.'"}'; + + return $this->jsonpWrap($exceptionMessage); + } + protected function renderTable($table) { $renderer = new Piwik_DataTable_Renderer_Php(); @@ -38,6 +50,11 @@ class Piwik_DataTable_Renderer_Json extends Piwik_DataTable_Renderer } $str = json_encode($array); + return $this->jsonpWrap($str); + } + + protected function jsonpWrap($str) + { if(($jsonCallback = Piwik_Common::getRequestVar('jsoncallback', false)) !== false) { if(preg_match('/^[0-9a-zA-Z]*$/', $jsonCallback) > 0) @@ -45,6 +62,12 @@ class Piwik_DataTable_Renderer_Json extends Piwik_DataTable_Renderer $str = $jsonCallback . "(" . $str . ")"; } } + return $str; } + + static private function renderHeader () + { + @header( "Content-Type: application/json" ); + } } diff --git a/core/DataTable/Renderer/Php.php b/core/DataTable/Renderer/Php.php index b7d5f216da..18f24452e0 100644 --- a/core/DataTable/Renderer/Php.php +++ b/core/DataTable/Renderer/Php.php @@ -64,6 +64,20 @@ class Piwik_DataTable_Renderer_Php extends Piwik_DataTable_Renderer return $toReturn; } + function renderException() + { + $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage()); + + $return = array('result' => 'error', 'message' => $exceptionMessage); + + if($this->serialize) + { + $return = serialize($return); + } + + return $return; + } + /** * Produces a flat php array from the DataTable, putting "columns" and "metadata" on the same level. * diff --git a/core/DataTable/Renderer/Rss.php b/core/DataTable/Renderer/Rss.php index 4dbfa58b7c..9c07515678 100644 --- a/core/DataTable/Renderer/Rss.php +++ b/core/DataTable/Renderer/Rss.php @@ -25,6 +25,12 @@ class Piwik_DataTable_Renderer_Rss extends Piwik_DataTable_Renderer return $this->renderTable($this->table); } + function renderException() + { + $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage()); + return 'Error: '.$exceptionMessage; + } + protected function renderTable($table) { if(!($table instanceof Piwik_DataTable_Array) @@ -47,8 +53,9 @@ class Piwik_DataTable_Renderer_Rss extends Piwik_DataTable_Renderer $site = $table->metadata[$date]['site']; $pudDate = date('r', $timestamp); - $dateUrl = date('Y-m-d', $timestamp); - $thisPiwikUrl = htmlentities($piwikUrl . "&date=$dateUrl"); + + $dateInSiteTimezone = Piwik_Date::factory($timestamp)->setTimezone($site->getTimezone())->toString('Y-m-d'); + $thisPiwikUrl = htmlentities($piwikUrl . "&date=$dateInSiteTimezone"); $siteName = $site->getName(); $title = $siteName . " on ". $date; @@ -98,7 +105,7 @@ class Piwik_DataTable_Renderer_Rss extends Piwik_DataTable_Renderer { if($table->getRowsCount() == 0) { - return "<b><i>Empty table</i></b> <br>\n"; + return "<b><i>Empty table</i></b><br />\n"; } $i = 1; diff --git a/core/DataTable/Renderer/Tsv.php b/core/DataTable/Renderer/Tsv.php new file mode 100644 index 0000000000..1af22ca122 --- /dev/null +++ b/core/DataTable/Renderer/Tsv.php @@ -0,0 +1,34 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Piwik + */ + +/** + * TSV export + * + * Excel doesn't import CSV properly, it expects TAB separated values by default. + * TSV is therefore the 'CSV' that is Excel compatible + * + * @package Piwik + * @subpackage Piwik_DataTable + */ +class Piwik_DataTable_Renderer_Tsv extends Piwik_DataTable_Renderer_Csv +{ + function __construct() + { + parent::__construct(); + $this->setSeparator("\t"); + } + + function render() + { + return parent::render(); + } +} diff --git a/core/DataTable/Renderer/Xml.php b/core/DataTable/Renderer/Xml.php index 9eb2c30bbf..828b890ab7 100644 --- a/core/DataTable/Renderer/Xml.php +++ b/core/DataTable/Renderer/Xml.php @@ -27,6 +27,20 @@ class Piwik_DataTable_Renderer_Xml extends Piwik_DataTable_Renderer return $this->renderTable($this->table); } + function renderException() + { + $exceptionMessage = self::renderHtmlEntities($this->exception->getMessage()); + + @header("Content-Type: text/xml;charset=utf-8"); + $return = + "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" . + "<result>\n". + "\t<error message=\"".$exceptionMessage."\" />\n". + "</result>"; + + return $return; + } + protected function getArrayFromDataTable($table) { $renderer = new Piwik_DataTable_Renderer_Php(); diff --git a/core/DataTable/Row.php b/core/DataTable/Row.php index cb8a20959b..1a07bd80a4 100644 --- a/core/DataTable/Row.php +++ b/core/DataTable/Row.php @@ -122,7 +122,7 @@ class Piwik_DataTable_Row $metadata[] = "'$name' => $value"; } $metadata = implode(", ", $metadata); - $output = "# [".$columns."] [".$metadata."] [idsubtable = " . $this->getIdSubDataTable()."]<br>\n"; + $output = "# [".$columns."] [".$metadata."] [idsubtable = " . $this->getIdSubDataTable()."]<br />\n"; return $output; } diff --git a/core/Date.php b/core/Date.php index f6aefce606..87fd5d55a0 100644 --- a/core/Date.php +++ b/core/Date.php @@ -18,66 +18,192 @@ class Piwik_Date { /** + * Builds a Piwik_Date object + * + * @param int timestamp + */ + protected function __construct( $timestamp, $timezone = 'UTC') + { + if(!is_int( $timestamp )) + { + throw new Exception("Piwik_Date is expecting a unix timestamp"); + } + $this->timezone = $timezone; + $this->timestamp = $timestamp ; + } + + + /** * Returns a Piwik_Date objects. - * Accepts strings 'today' 'yesterday' or any YYYY-MM-DD or timestamp * - * @param string $strDate - * @return Piwik_Date + * @param string $strDate 'today' 'yesterday' or any YYYY-MM-DD or timestamp + * @param string $timezone if specified, the dateString will be relative to this $timezone. + * For example, today in UTC+12 will be a timestamp in the future for UTC. + * This is different from using ->setTimezone() + * @return Piwik_Date */ - static public function factory($dateString) + static public function factory($dateString, $timezone = null) { - if($dateString == 'today') + if($dateString == 'now') + { + $date = self::now(); + } + elseif($dateString == 'today') { - return self::today(); + $date = self::today(); } - if($dateString == 'yesterday') + elseif($dateString == 'yesterday') { - return self::yesterday(); + $date = self::yesterday(); } - if (!is_int($dateString) + elseif($dateString == 'yesterdaySameTime') + { + $date = self::yesterdaySameTime(); + } + elseif (!is_int($dateString) && ($dateString = strtotime($dateString)) === false) { - throw new Exception("Date format must be: YYYY-MM-DD, or 'today' or 'yesterday' or any keyword supported by the strtotime function (see http://php.net/strtotime for more information)"); + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "http://php.net/strtotime"))); + } + else + { + $date = new Piwik_Date($dateString); + } + if(is_null($timezone)) + { + return $date; + } + + // manually adjust for UTC timezones + $utcOffset = self::extractUtcOffset($timezone); + if($utcOffset !== false) + { + return $date->addHour($utcOffset); } - return new Piwik_Date($dateString); + + date_default_timezone_set($timezone); + $datetime = $date->getDatetime(); + date_default_timezone_set('UTC'); + + $date = Piwik_Date::factory(strtotime($datetime)); + + return $date; } + /* + * The stored timestamp is always UTC based. + * The returned timestamp via getTimestamp() will have the conversion applied + */ protected $timestamp = null; + + /* + * Timezone the current date object is set to. + * Timezone will only affect the returned timestamp via getTimestamp() + */ + protected $timezone = 'UTC'; + + const DATE_TIME_FORMAT = 'Y-m-d H:i:s'; + + /** + * Returns the datetime start in UTC + * + * @return string + */ + function getDateStartUTC() + { + $dateStartUTC = date('Y-m-d', $this->timestamp); + $date = Piwik_Date::factory($dateStartUTC)->setTimezone($this->timezone); + return $date->toString(self::DATE_TIME_FORMAT); + } + /** + * Returns the datetime of the current timestamp + * + * @return string + */ + function getDatetime() + { + return $this->toString(self::DATE_TIME_FORMAT); + } + + /** + * Returns the datetime end in UTC + * + * @return string + */ + function getDateEndUTC() + { + $dateEndUTC = date('Y-m-d 23:59:59', $this->timestamp); + $date = Piwik_Date::factory($dateEndUTC)->setTimezone($this->timezone); + return $date->toString(self::DATE_TIME_FORMAT); + } + /** - * Returns the unix timestamp of the date - * - * @return int + * Returns a new date object, copy of $this, with the timezone set + * This timezone is used to offset the UTC timestamp returned by @see getTimestamp() + * Doesn't modify $this + * + * @param string $timezone 'UTC', 'Europe/London', ... */ - public function getTimestamp() + public function setTimezone($timezone) { - return $this->timestamp; + return new Piwik_Date($this->timestamp, $timezone); } /** - * Builds a Piwik_Date object + * Helper function that returns the offset in the timezone string 'UTC+14' + * Returns false if the timezone is not UTC+X or UTC-X * - * @param int timestamp + * @param $timezone + * @return int or false */ - protected function __construct( $date ) + static protected function extractUtcOffset($timezone) { - if(!is_int( $date )) + if($timezone == 'UTC') { - throw new Exception("Piwik_Date is expecting a unix timestamp"); + return 0; + } + $start = substr($timezone, 0, 4); + if($start != 'UTC-' + && $start != 'UTC+') + { + return false; } - $this->timestamp = $date ; + $offset = (float)substr($timezone, 4); + if($start == 'UTC-') { + $offset = -$offset; + } + return $offset; } /** - * Sets the time part of the date - * Doesn't modify $this - * - * @param string $time HH:MM:SS - * @return Piwik_Date The new date with the time part set + * Returns the unix timestamp of the date in UTC, + * converted from the date timezone + * + * @return int */ - public function setTime($time) + public function getTimestamp() { - return new Piwik_Date( strtotime( $this->get("j F Y") . " $time")); + $utcOffset = self::extractUtcOffset($this->timezone); + if($utcOffset !== false) { + return (int)($this->timestamp - $utcOffset * 3600); + } + // @fixme + // The following code seems clunky - I thought the DateTime php class would allow to return timestamps + // after applying the timezone offset. Instead, the underlying timestamp is not changed. + // I decided to get the date without the timezone information, and create the timestamp from the truncated string. + // Unit tests pass (@see Date.test.php) but I'm pretty sure this is not the right way to do it + date_default_timezone_set($this->timezone); + $dtzone = timezone_open('UTC'); + $time = date('r', $this->timestamp); + $dtime = date_create($time); + date_timezone_set($dtime, $dtzone); + $dateWithTimezone = date_format($dtime, 'r'); + $dateWithoutTimezone = substr($dateWithTimezone, 0, -6); + $timestamp = strtotime($dateWithoutTimezone); + date_default_timezone_set('UTC'); + + return (int)$timestamp; } /** @@ -113,7 +239,7 @@ class Piwik_Date { return date($part, $this->getTimestamp()); } - + /** * @see toString() * @@ -125,6 +251,114 @@ class Piwik_Date } /** + * Compares the week of the current date against the given $date + * Returns 0 if equal, -1 if current week is earlier or 1 if current week is later + * Example: 09.Jan.2007 13:07:25 -> compareWeek(2); -> 0 + * + * @param Piwik_Date $date + * @return integer 0 = equal, 1 = later, -1 = earlier + */ + public function compareWeek(Piwik_Date $date) + { + $currentWeek = date('W', $this->getTimestamp()); + $toCompareWeek = date('W', $date->getTimestamp()); + if( $currentWeek == $toCompareWeek) + { + return 0; + } + if( $currentWeek < $toCompareWeek) + { + return -1; + } + return 1; + } + + /** + * Compares the month of the current date against the given $date month + * Returns 0 if equal, -1 if current month is earlier or 1 if current month is later + * For example: 10.03.2000 -> 15.03.1950 -> 0 + * + * @param Piwik_Date $month Month to compare + * @return integer 0 = equal, 1 = later, -1 = earlier + */ + function compareMonth( Piwik_Date $date ) + { + $currentMonth = date('n', $this->getTimestamp()); + $toCompareMonth = date('n', $date->getTimestamp()); + if( $currentMonth == $toCompareMonth) + { + return 0; + } + if( $currentMonth < $toCompareMonth) + { + return -1; + } + return 1; + } + + /** + * Returns true if current date is today + * + * @return bool + */ + public function isToday() + { + return $this->toString('Y-m-d') === Piwik_Date::factory('today', $this->timezone)->toString('Y-m-d'); + } + + /** + * Returns a date object set to now (same as today, except that the time is also set) + * + * @return Piwik_Date + */ + static public function now() + { + return new Piwik_date(time()); + } + + /** + * Returns a date object set to today midnight + * + * @return Piwik_Date + */ + static public function today() + { + return new Piwik_Date(strtotime(date("Y-m-d 00:00:00"))); + } + + /** + * Returns a date object set to yesterday midnight + * + * @return Piwik_Date + */ + static public function yesterday() + { + return new Piwik_Date(strtotime("yesterday")); + } + + /** + * Returns a date object set to yesterday same time of day + * + * @return Piwik_Date + */ + static public function yesterdaySameTime() + { + return new Piwik_Date(strtotime("yesterday ".date('H:i:s'))); + } + + /** + * Sets the time part of the date + * Doesn't modify $this + * + * @param string $time HH:MM:SS + * @return Piwik_Date The new date with the time part set + */ + public function setTime($time) + { + return new Piwik_Date( strtotime( date("Y-m-d", $this->timestamp) . " $time"), $this->timezone); + } + + /** * Sets a new day * Returned is the new date object * Doesn't modify $this @@ -134,7 +368,7 @@ class Piwik_Date */ public function setDay( $day ) { - $ts = $this->getTimestamp(); + $ts = $this->timestamp; $result = mktime( date('H', $ts), date('i', $ts), @@ -143,7 +377,7 @@ class Piwik_Date 1, date('Y', $ts) ); - return new Piwik_Date( $result ); + return new Piwik_Date( $result, $this->timezone ); } /** @@ -156,7 +390,7 @@ class Piwik_Date */ public function setYear( $year ) { - $ts = $this->getTimestamp(); + $ts = $this->timestamp; $result = mktime( date('H', $ts), date('i', $ts), @@ -165,11 +399,9 @@ class Piwik_Date date('j', $ts), $year ); - return new Piwik_Date( $result ); + return new Piwik_Date( $result, $this->timezone ); } - - /** * Subtracts days from the existing date object and returns a new Piwik_Date object * Returned is the new date object @@ -183,8 +415,8 @@ class Piwik_Date { return clone $this; } - $ts = strtotime("-$n day", $this->getTimestamp()); - return new Piwik_Date( $ts ); + $ts = strtotime("-$n day", $this->timestamp); + return new Piwik_Date( $ts, $this->timezone ); } /** @@ -200,7 +432,7 @@ class Piwik_Date { return clone $this; } - $ts = $this->getTimestamp(); + $ts = $this->timestamp; $result = mktime( date('H', $ts), date('i', $ts), @@ -209,23 +441,8 @@ class Piwik_Date 1, // we set the day to 1 date('Y', $ts) ); - return new Piwik_Date( $result ); + return new Piwik_Date( $result, $this->timezone ); } - - /** - * Returns a representation of a date or datepart - * - * @param string OPTIONAL Part of the date to return, if null the timestamp is returned - * @return integer|string date or datepart - */ - public function get($part = null) - { - if(is_null($part)) - { - return $this->getTimestamp(); - } - return date($part, $this->getTimestamp()); - } /** * Returns a localized date string, given a template. @@ -247,12 +464,12 @@ class Piwik_Date "%longDay%" => Piwik_Translate('General_LongDay_'.$dayOfWeek), "%longYear%" => $this->toString('Y'), "%shortYear%" => $this->toString('y'), - "%time%" => $this->toString('H:i:s T') + "%time%" => $this->toString('H:i:s') ); $out = str_replace(array_keys($patternToValue), array_values($patternToValue), $template); return $out; } - + /** * Adds days to the existing date object. * Returned is the new date object @@ -263,82 +480,44 @@ class Piwik_Date */ public function addDay( $n ) { - $ts = strtotime("+$n day", $this->getTimestamp()); - return new Piwik_Date( $ts ); + $ts = strtotime("+$n day", $this->timestamp); + return new Piwik_Date( $ts, $this->timezone ); } - - /** - * Compares the week of the current date against the given $date - * Returns 0 if equal, -1 if current week is earlier or 1 if current week is later - * Example: 09.Jan.2007 13:07:25 -> compareWeek(2); -> 0 - * - * @param Piwik_Date $date - * @return integer 0 = equal, 1 = later, -1 = earlier - */ - public function compareWeek(Piwik_Date $date) - { - $currentWeek = date('W', $this->getTimestamp()); - $toCompareWeek = date('W', $date->getTimestamp()); - if( $currentWeek == $toCompareWeek) - { - return 0; - } - if( $currentWeek < $toCompareWeek) - { - return -1; - } - return 1; - } + /** - * Compares the month of the current date against the given $date month - * Returns 0 if equal, -1 if current month is earlier or 1 if current month is later - * For example: 10.03.2000 -> 15.03.1950 -> 0 - * - * @param Piwik_Date $month Month to compare - * @return integer 0 = equal, 1 = later, -1 = earlier + * Adds hours to the existing date object. + * Returned is the new date object + * Doesn't modify $this + * + * @param int Number of hours to add + * @return Piwik_Date new date */ - function compareMonth( Piwik_Date $date ) + public function addHour( $n ) { - $currentMonth = date('n', $this->getTimestamp()); - $toCompareMonth = date('n', $date->getTimestamp()); - if( $currentMonth == $toCompareMonth) + $minutes = 0; + if($n != round($n)) { - return 0; + $minutes = abs($n - floor($n)) * 60; + $n = floor($n); } - if( $currentMonth < $toCompareMonth) + if($n > 0 ) { - return -1; + $n = '+'.$n; } - return 1; + $ts = strtotime("$n hour $minutes minutes", $this->timestamp); + return new Piwik_Date( $ts, $this->timezone ); } - - /** - * Returns true if current date is today - * - * @return bool - */ - public function isToday() - { - return $this->get('Y-m-d') === date('Y-m-d', time()); - } - - /** - * Returns a date object set to today midnight - * - * @return Piwik_Date - */ - static public function today() - { - return new Piwik_Date(strtotime(date("Y-m-d 00:00:00"))); - } - + /** - * Returns a date object set to yesterday midnight - * - * @return Piwik_Date - */ - static public function yesterday() + * Substract hour to the existing date object. + * Returned is the new date object + * Doesn't modify $this + * + * @param int Number of hours to substract + * @return Piwik_Date new date + */ + public function subHour( $n ) { - return new Piwik_Date(strtotime("yesterday")); + return $this->addHour(-$n); } } diff --git a/core/Db.php b/core/Db/Adapter.php index a8efb337a8..7a3b4fccf6 100644 --- a/core/Db.php +++ b/core/Db/Adapter.php @@ -1,43 +1,63 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later * @version $Id$ - * + * * @category Piwik * @package Piwik */ /** * @package Piwik + * @subpackage Piwik_Db */ -class Piwik_Db +class Piwik_Db_Adapter { /** - * Get adapter class name + * Create adapter * - * @param string $adapterName - * @return string + * @param string $adapterName database adapter name + * @param array $dbInfos database connection info + * @return mixed (Piwik_Db_Adapter_Mysqli, Piwik_Db_Adapter_Pdo_Mysql, etc) */ - private static function getAdapterClassName($adapterName) + public static function factory($adapterName, & $dbInfos) { - return 'Piwik_Db_' . str_replace(' ', '_', ucwords(str_replace('_', ' ', strtolower($adapterName)))); + if($dbInfos['port'][0] == '/') + { + $dbInfos['unix_socket'] = $dbInfos['port']; + unset($dbInfos['host']); + unset($dbInfos['port']); + } + + // not used by Zend Framework + unset($dbInfos['tables_prefix']); + unset($dbInfos['adapter']); + unset($dbInfos['schema']); + + $className = self::getAdapterClassName($adapterName); + $adapter = new $className($dbInfos); + $adapter->getConnection(); + + Zend_Db_Table::setDefaultAdapter($adapter); + + // we don't want the connection information to appear in the logs + $adapter->resetConfig(); + + return $adapter; } /** - * Create adapter + * Get adapter class name * * @param string $adapterName - * @oaran array $config - * @return mixed (Piwik_Db_Mysqli, Piwik_Db_Pdo_Mysql, etc) + * @return string */ - public static function factory($adapterName, $config) + private static function getAdapterClassName($adapterName) { - $className = self::getAdapterClassName($adapterName); - $adapter = new $className($config); - return $adapter; + return 'Piwik_Db_Adapter_' . str_replace(' ', '_', ucwords(str_replace('_', ' ', strtolower($adapterName)))); } /** @@ -59,24 +79,41 @@ class Piwik_Db */ public static function getAdapters() { - $path = PIWIK_INCLUDE_PATH . '/core/Db'; - $pathLength = strlen($path) + 1; - $adapters = Piwik::globr($path, '*.php'); - $adapterNames = array(); - foreach($adapters as $adapter) + static $adapterNames = array( + // currently supported by Piwik + 'Pdo_Mysql', + 'Mysqli', + + // other adapters supported by Zend_Db +// 'Pdo_Pgsql', +// 'Pdo_Mssql', +// 'Sqlsrv', +// 'Pdo_Ibm', +// 'Db2', +// 'Pdo_Oci', +// 'Oracle', + ); + + $adapters = array(); + + foreach($adapterNames as $adapterName) { - $adapterName = str_replace('/', '_', substr($adapter, $pathLength, -strlen('.php'))); - $className = 'Piwik_Db_'.$adapterName; + $className = 'Piwik_Db_Adapter_'.$adapterName; if(call_user_func(array($className, 'isEnabled'))) { - $adapterNames[strtoupper($adapterName)] = call_user_func(array($className, 'getDefaultPort')); + $adapters[strtoupper($adapterName)] = call_user_func(array($className, 'getDefaultPort')); } } - return $adapterNames; + + return $adapters; } } -interface Piwik_Db_iAdapter +/** + * @package Piwik + * @subpackage Piwik_Db + */ +interface Piwik_Db_Adapter_Interface { /** * Reset the configuration variables in this adapter. diff --git a/core/Db/Mysqli.php b/core/Db/Adapter/Mysqli.php index 6f62f9f546..37bb46540f 100644 --- a/core/Db/Mysqli.php +++ b/core/Db/Adapter/Mysqli.php @@ -12,8 +12,9 @@ /** * @package Piwik + * @subpackage Piwik_Db */ -class Piwik_Db_Mysqli extends Zend_Db_Adapter_Mysqli implements Piwik_Db_iAdapter +class Piwik_Db_Adapter_Mysqli extends Zend_Db_Adapter_Mysqli implements Piwik_Db_Adapter_Interface { public function __construct($config) { @@ -43,15 +44,29 @@ class Piwik_Db_Mysqli extends Zend_Db_Adapter_Mysqli implements Piwik_Db_iAdapte */ public function checkServerVersion() { - $databaseVersion = $this->getServerVersion(); + $serverVersion = $this->getServerVersion(); $requiredVersion = Zend_Registry::get('config')->General->minimum_mysql_version; - if(version_compare($databaseVersion, $requiredVersion) === -1) + if(version_compare($serverVersion, $requiredVersion) === -1) { - throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MySQL', $databaseVersion, $requiredVersion))); + throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MySQL', $serverVersion, $requiredVersion))); } } /** + * Check client version compatibility against database server + */ + public function checkClientVersion() + { + $serverVersion = $this->getServerVersion(); + $clientVersion = $this->getClientVersion(); + if(version_compare($serverVersion, '5') >= 0 + && version_compare($clientVersion, '5') < 0) + { + throw new Exception(Piwik_TranslateException('General_ExceptionIncompatibleClientServerVersions', array('MySQL', $clientVersion, $serverVersion))); + } + } + + /** * Returns true if this adapter's required extensions are enabled * * @return bool @@ -59,7 +74,7 @@ class Piwik_Db_Mysqli extends Zend_Db_Adapter_Mysqli implements Piwik_Db_iAdapte public static function isEnabled() { $extensions = @get_loaded_extensions(); - return in_array('mysqli', $extensions) && function_exists('mysqli_set_charset'); + return in_array('mysqli', $extensions); } /** @@ -81,6 +96,15 @@ class Piwik_Db_Mysqli extends Zend_Db_Adapter_Mysqli implements Piwik_Db_iAdapte */ public function isErrNo($e, $errno) { + if(is_null($this->_connection)) + { + if(preg_match('/(?:\[|\s)([0-9]{4})(?:\]|\s)/', $e->getMessage(), $match)) + { + return $match[1] == $errno; + } + return mysqli_connect_errno() == $errno; + } + return mysqli_errno($this->_connection) == $errno; } @@ -114,4 +138,19 @@ class Piwik_Db_Mysqli extends Zend_Db_Adapter_Mysqli implements Piwik_Db_iAdapte $charset = mysqli_character_set_name($this->_connection); return $charset === 'utf8'; } + + /** + * Get client version + * + * @return string + */ + public function getClientVersion() + { + $this->_connect(); + $version = $this->_connection->server_version; + $major = (int) ($version / 10000); + $minor = (int) ($version % 10000 / 100); + $revision = (int) ($version % 100); + return $major . '.' . $minor . '.' . $revision; + } } diff --git a/core/Db/Adapter/Pdo/Mssql.php b/core/Db/Adapter/Pdo/Mssql.php new file mode 100644 index 0000000000..7246c3838e --- /dev/null +++ b/core/Db/Adapter/Pdo/Mssql.php @@ -0,0 +1,253 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Piwik + */ + +/** + * @package Piwik + * @subpackage Piwik_Db + */ +class Piwik_Db_Pdo_Mssql extends Zend_Db_Adapter_Pdo_Mssql implements Piwik_Db_Adapter_Interface +{ + /** + * Returns connection handle + * + * @return resource + */ + public function getConnection() + { + // if we already have a PDO object, no need to re-connect. + if ($this->_connection) + { + return $this->_connection; + } + + $this->_pdoType = "sqlsrv"; + // get the dsn first, because some adapters alter the $_pdoType + //$dsn = $this->_dsn(); + + // check for PDO extension + if (!extension_loaded('pdo')) + { + /** + * @see Zend_Db_Adapter_Exception + */ + throw new Zend_Db_Adapter_Exception('The PDO extension is required for this adapter but the extension is not loaded'); + } + + // check the PDO driver is available + if (!in_array($this->_pdoType, PDO::getAvailableDrivers())) + { + /** + * @see Zend_Db_Adapter_Exception + */ + throw new Zend_Db_Adapter_Exception('The ' . $this->_pdoType . ' driver is not currently installed'); + } + + // create PDO connection + $q = $this->_profiler->queryStart('connect', Zend_Db_Profiler::CONNECT); + + // add the persistence flag if we find it in our config array + if (isset($this->_config['persistent']) && ($this->_config['persistent'] == true)) + { + $this->_config['driver_options'][PDO::ATTR_PERSISTENT] = true; + } + + try { + $serverName = $this->_config["host"]; + $database = $this->_config["dbname"]; + if(is_null($database)) + { + $database = 'master'; + } + $uid = $this->_config['username']; + $pwd = $this->_config['password']; + if($this->_config["port"] != "") + { + $serverName = $serverName.",".$this->_config["port"]; + } + + $this->_connection = new PDO( "sqlsrv:$serverName", $uid, $pwd, array( 'Database' => $database )); + + + if( $this->_connection === false ) + { + die( self::FormatErrors( sqlsrv_errors() ) ); + } + + /* + $this->_connection = new PDO( + $dsn, + $this->_config['username'], + $this->_config['password'], + $this->_config['driver_options'] + ); + */ + + $this->_profiler->queryEnd($q); + + // set the PDO connection to perform case-folding on array keys, or not + $this->_connection->setAttribute(PDO::ATTR_CASE, $this->_caseFolding); + $this->_connection->setAttribute(PDO::SQLSRV_ENCODING_UTF8, true); + + + // always use exceptions. + $this->_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + return $this->_connection; + } catch (PDOException $e) { + /** + * @see Zend_Db_Adapter_Exception + */ + throw new Zend_Db_Adapter_Exception($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Reset the configuration variables in this adapter. + */ + public function resetConfig() + { + $this->_config = array(); + } + + /** + * Return default port. + * + * @return int + */ + public static function getDefaultPort() + { + return 1433; + } + + /** + * Check MSSQL version + */ + public function checkServerVersion() + { + $serverVersion = $this->getServerVersion(); + $requiredVersion = Zend_Registry::get('config')->General->minimum_mssql_version; + if(version_compare($serverVersion, $requiredVersion) === -1) + { + throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MSSQL', $serverVersion, $requiredVersion))); + } + + } + + public function getServerVersion() + { + try + { + $stmt = $this->query("SELECT CAST(SERVERPROPERTY('productversion') as VARCHAR) as productversion"); + $result = $stmt->fetchAll(Zend_Db::FETCH_NUM); + if (count($result)) + { + return $result[0][0]; + } + } + catch (PDOException $e) + { + } + + return null; + } + + /** + * Check client version compatibility against database server + */ + public function checkClientVersion() + { + $serverVersion = $this->getServerVersion(); + $clientVersion = $this->getClientVersion(); + if(version_compare($serverVersion, '10') >= 0 + && version_compare($clientVersion, '10') < 0) + { + throw new Exception(Piwik_TranslateException('General_ExceptionIncompatibleClientServerVersions', array('MSSQL', $clientVersion, $serverVersion))); + } + } + + /** + * Returns true if this adapter's required extensions are enabled + * + * @return bool + */ + public static function isEnabled() + { + $extensions = @get_loaded_extensions(); + return in_array('PDO', $extensions) && in_array('pdo_sqlsrv', $extensions); + } + + /** + * Returns true if this adapter supports blobs as fields + * + * @return bool + */ + public function hasBlobDataType() + { + return true; + } + + /** + * Test error number + * + * @param Exception $e + * @param string $errno + * @return bool + */ + public function isErrNo($e, $errno) + { + if(preg_match('/(?:\[|\s)([0-9]{4})(?:\]|\s)/', $e->getMessage(), $match)) + { + return $match[1] == $errno; + } + return false; + } + + /** + * Is the connection character set equal to utf8? + * + * @return bool + */ + public function isConnectionUTF8() + { + //check the getconnection, it's specified on the connection string. + return true; + } + + /** + * Retrieve client version in PHP style + * + * @return string + */ + public function getClientVersion() + { + $this->_connect(); + try + { + $version = $this->_connection->getAttribute(PDO::ATTR_CLIENT_VERSION); + $requiredVersion = Zend_Registry::get('config')->General->minimum_mssql_client_version; + if(version_compare($version['DriverVer'], $requiredVersion) === -1) + { + throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MSSQL', $serverVersion, $requiredVersion))); + } + else + { + return $version['DriverVer']; + } + } + catch (PDOException $e) + { + // In case of the driver doesn't support getting attributes + } + + return null; + } +} diff --git a/core/Db/Pdo/Mysql.php b/core/Db/Adapter/Pdo/Mysql.php index 86f804e76b..4e8f8793a1 100644 --- a/core/Db/Pdo/Mysql.php +++ b/core/Db/Adapter/Pdo/Mysql.php @@ -12,8 +12,9 @@ /** * @package Piwik + * @subpackage Piwik_Db */ -class Piwik_Db_Pdo_Mysql extends Zend_Db_Adapter_Pdo_Mysql implements Piwik_Db_iAdapter +class Piwik_Db_Adapter_Pdo_Mysql extends Zend_Db_Adapter_Pdo_Mysql implements Piwik_Db_Adapter_Interface { /** * Returns connection handle @@ -67,15 +68,29 @@ class Piwik_Db_Pdo_Mysql extends Zend_Db_Adapter_Pdo_Mysql implements Piwik_Db_i */ public function checkServerVersion() { - $databaseVersion = $this->getServerVersion(); + $serverVersion = $this->getServerVersion(); $requiredVersion = Zend_Registry::get('config')->General->minimum_mysql_version; - if(version_compare($databaseVersion, $requiredVersion) === -1) + if(version_compare($serverVersion, $requiredVersion) === -1) { - throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MySQL', $databaseVersion, $requiredVersion))); + throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MySQL', $serverVersion, $requiredVersion))); } } /** + * Check client version compatibility against database server + */ + public function checkClientVersion() + { + $serverVersion = $this->getServerVersion(); + $clientVersion = $this->getClientVersion(); + if(version_compare($serverVersion, '5') >= 0 + && version_compare($clientVersion, '5') < 0) + { + throw new Exception(Piwik_TranslateException('General_ExceptionIncompatibleClientServerVersions', array('MySQL', $clientVersion, $serverVersion))); + } + } + + /** * Returns true if this adapter's required extensions are enabled * * @return bool @@ -83,7 +98,7 @@ class Piwik_Db_Pdo_Mysql extends Zend_Db_Adapter_Pdo_Mysql implements Piwik_Db_i public static function isEnabled() { $extensions = @get_loaded_extensions(); - return in_array('PDO', $extensions) && in_array('pdo_mysql', $extensions); + return in_array('PDO', $extensions) && in_array('pdo_mysql', $extensions) && in_array('mysql', PDO::getAvailableDrivers()); } /** @@ -105,7 +120,7 @@ class Piwik_Db_Pdo_Mysql extends Zend_Db_Adapter_Pdo_Mysql implements Piwik_Db_i */ public function isErrNo($e, $errno) { - if(preg_match('/ ([0-9]{4}) /', $e->getMessage(), $match)) + if(preg_match('/(?:\[|\s)([0-9]{4})(?:\]|\s)/', $e->getMessage(), $match)) { return $match[1] == $errno; } @@ -123,4 +138,24 @@ class Piwik_Db_Pdo_Mysql extends Zend_Db_Adapter_Pdo_Mysql implements Piwik_Db_i $charset = $charsetInfo[0]['Value']; return $charset === 'utf8'; } + + /** + * Retrieve client version in PHP style + * + * @return string + */ + public function getClientVersion() + { + $this->_connect(); + try { + $version = $this->_connection->getAttribute(PDO::ATTR_CLIENT_VERSION); + $matches = null; + if (preg_match('/((?:[0-9]{1,2}\.){1,3}[0-9]{1,2})/', $version, $matches)) { + return $matches[1]; + } + } catch (PDOException $e) { + // In case of the driver doesn't support getting attributes + } + return null; + } } diff --git a/core/Db/Pdo/Pgsql.php b/core/Db/Adapter/Pdo/Pgsql.php index 8c3cf0a6c5..f83a023b01 100644 --- a/core/Db/Pdo/Pgsql.php +++ b/core/Db/Adapter/Pdo/Pgsql.php @@ -12,8 +12,9 @@ /** * @package Piwik + * @subpackage Piwik_Db */ -class Piwik_Db_Pdo_Pgsql extends Zend_Db_Adapter_Pdo_Pgsql implements Piwik_Db_iAdapter +class Piwik_Db_Adapter_Pdo_Pgsql extends Zend_Db_Adapter_Pdo_Pgsql implements Piwik_Db_Adapter_Interface { /** * Reset the configuration variables in this adapter. @@ -39,11 +40,18 @@ class Piwik_Db_Pdo_Pgsql extends Zend_Db_Adapter_Pdo_Pgsql implements Piwik_Db_i public function checkServerVersion() { $databaseVersion = $this->getServerVersion(); - $requiredVersion = Zend_Registry::get('config')->General->minimum_pgsql_version; - if(version_compare($databaseVersion, $requiredVersion) === -1) - { - throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('PostgreSQL', $databaseVersion, $requiredVersion))); - } + $requiredVersion = Zend_Registry::get('config')->General->minimum_pgsql_version; + if(version_compare($databaseVersion, $requiredVersion) === -1) + { + throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('PostgreSQL', $databaseVersion, $requiredVersion))); + } + } + + /** + * Check client version compatibility against database server + */ + public function checkClientVersion() + { } /** @@ -53,10 +61,6 @@ class Piwik_Db_Pdo_Pgsql extends Zend_Db_Adapter_Pdo_Pgsql implements Piwik_Db_i */ public static function isEnabled() { - /** - * @todo This adapter is incomplete. - */ - return false; $extensions = @get_loaded_extensions(); return in_array('PDO', $extensions) && in_array('pdo_pgsql', $extensions); } @@ -76,32 +80,6 @@ class Piwik_Db_Pdo_Pgsql extends Zend_Db_Adapter_Pdo_Pgsql implements Piwik_Db_i } /** - * Pre-process SQL to handle MySQL-isms - * - * @return string - */ - public function preprocessSql($query) - { - $search = array( - // In MySQL, OPTION is still a reserved keyword; Piwik uses - // backticking in case table_prefix is empty. - '`', - - // MySQL implicitly does 'ORDER BY column' when there's a - // 'GROUP BY column'; Piwik uses 'ORDER BY NULL' when order - // doesn't matter, for better performance. - 'ORDER BY NULL', - ); - - $replace = array( - '', - '', - ); - - $query = str_replace($search, $replace, $query); - } - - /** * Test error number * * @param Exception $e @@ -128,6 +106,10 @@ class Piwik_Db_Pdo_Pgsql extends Zend_Db_Adapter_Pdo_Pgsql implements Piwik_Db_i // PostgreSQL: column "%s" of relation "%s" already exists '1060' => '42701', + // MySQL: Duplicate key name '%s' + // PostgreSQL: relation "%s" already exists + '1061' => '42P07', + // MySQL: Duplicate entry '%s' for key '%s' // PostgreSQL: duplicate key violates unique constraint '1062' => '23505', @@ -160,24 +142,22 @@ class Piwik_Db_Pdo_Pgsql extends Zend_Db_Adapter_Pdo_Pgsql implements Piwik_Db_i } /** - * Returns a list of the tables in the database. + * Retrieve client version in PHP style * - * Replaces parent::listTables() which uses subqueries. - * @see ZF-8046 - * - * @return array + * @return string */ - public function listTables() + public function getClientVersion() { - $sql = "SELECT c.relname AS table_name " - . "FROM pg_catalog.pg_class c " - . "JOIN pg_catalog.pg_roles r ON r.oid = c.relowner " - . "LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace " - . "WHERE n.nspname <> 'pg_catalog' " - . "AND n.nspname !~ '^pg_toast' " - . "AND pg_catalog.pg_table_is_visible(c.oid) " - . "AND c.relkind = 'r' "; - - return $this->fetchCol($sql); + $this->_connect(); + try { + $version = $this->_connection->getAttribute(PDO::ATTR_CLIENT_VERSION); + $matches = null; + if (preg_match('/((?:[0-9]{1,2}\.){1,3}[0-9]{1,2})/', $version, $matches)) { + return $matches[1]; + } + } catch (PDOException $e) { + // In case of the driver doesn't support getting attributes + } + return null; } } diff --git a/core/Db/Schema.php b/core/Db/Schema.php new file mode 100644 index 0000000000..7afe91d0e8 --- /dev/null +++ b/core/Db/Schema.php @@ -0,0 +1,282 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Piwik + */ + +/** + * Schema abstraction + * + * Note: no relation to the ZF proposals for Zend_Db_Schema_Manager + * + * @package Piwik + * @subpackage Piwik_Db + */ +class Piwik_Db_Schema +{ + static private $instance = null; + + private $schema = null; + + /** + * Returns the singleton Piwik_Db_Schema + * + * @return Piwik_Db_Schema + */ + static public function getInstance() + { + if (self::$instance === null) + { + $c = __CLASS__; + self::$instance = new $c(); + } + return self::$instance; + } + + /** + * Get schema class name + * + * @param string $schemaName + * @return string + */ + private static function getSchemaClassName($schemaName) + { + return 'Piwik_Db_Schema_' . str_replace(' ', '_', ucwords(str_replace('_', ' ', strtolower($schemaName)))); + } + + /** + * Get list of schemas + * + * @return array + */ + public static function getSchemas($adapterName) + { + static $allSchemaNames = array( + // MySQL storage engines + 'MYSQL' => array( + 'Myisam', +// 'Innodb', +// 'Infinidb', + ), + + // Microsoft SQL Server +// 'MSSQL' => array( 'Mssql' ), + + // PostgreSQL +// 'PDO_PGSQL' => array( 'Pgsql' ), + + // IBM DB2 +// 'IBM' => array( 'Ibm' ), + + // Oracle +// 'OCI' => array( 'Oci' ), + ); + + $adapterName = strtoupper($adapterName); + switch($adapterName) + { + case 'PDO_MYSQL': + case 'MYSQLI': + $adapterName = 'MYSQL'; + break; + + case 'PDO_MSSQL': + case 'SQLSRV': + $adapterName = 'MSSQL'; + break; + + case 'PDO_IBM': + case 'DB2': + $adapterName = 'IBM'; + break; + + case 'PDO_OCI': + case 'ORACLE': + $adapterName = 'OCI'; + break; + } + $schemaNames = $allSchemaNames[$adapterName]; + + $schemas = array(); + + foreach($schemaNamess as $schemaName) + { + $className = 'Piwik_Db_Schema_'.$schemaName; + if(call_user_func(array($className, 'isAvailable'))) + { + $schemas[] = $schemaName; + } + } + + return $schemas; + } + + /** + * Load schema + */ + private function loadSchema() + { + $schema = null; + Piwik_PostEvent('Schema.loadSchema', $schema); + if($schema === null) + { + $config = Zend_Registry::get('config'); + $dbInfos = $config->database->toArray(); + if(isset($dbInfos['schema'])) + { + $schemaName = $dbInfos['schema']; + } + else + { + $schemaName = 'Myisam'; + } + $className = self::getSchemaClassName($schemaName); + $schema = new $className(); + } + $this->schema = $schema; + } + + /** + * Returns an instance that subclasses Piwik_Db_Schema + * + * @return Piwik_Db_Schema_Interface + */ + private function getSchema() + { + if ($this->schema === null) + { + $this->loadSchema(); + } + return $this->schema; + } + + /** + * Get the SQL to create a specific Piwik table + * + * @return string SQL + */ + public function getTableCreateSql( $tableName ) + { + return $this->getSchema()->getTableCreateSql($tableName); + } + + /** + * Get the SQL to create Piwik tables + * + * @return array of strings containing SQL + */ + public function getTablesCreateSql() + { + return $this->getSchema()->getTablesCreateSql(); + } + + /** + * Create database + */ + public function createDatabase( $dbName = null ) + { + $this->getSchema()->createDatabase($dbName); + } + + /** + * Drop database + */ + public function dropDatabase() + { + $this->getSchema()->dropDatabase(); + } + + /** + * Create all tables + */ + public function createTables() + { + $this->getSchema()->createTables(); + } + + /** + * Creates an entry in the User table for the "anonymous" user. + */ + public function createAnonymousUser() + { + $this->getSchema()->createAnonymousUser(); + } + + /** + * Truncate all tables + */ + public function truncateAllTables() + { + $this->getSchema()->truncateAllTables(); + } + + /** + * Drop specific tables + */ + public function dropTables( $doNotDelete = array() ) + { + $this->getSchema()->dropTables($doNotDelete); + } + + /** + * Names of all the prefixed tables in piwik + * Doesn't use the DB + * + * @return array Table names + */ + public function getTablesNames() + { + return $this->getSchema()->getTablesNames(); + } + + /** + * Get list of tables installed + * + * @param bool $forceReload Invalidate cache + * @param string $idSite + * @return array Tables installed + */ + public function getTablesInstalled($forceReload = true, $idSite = null) + { + return $this->getSchema()->getTablesInstalled($forceReload, $idSite); + } + + /** + * Returns true if Piwik tables exist + * + * @return bool True if tables exist; false otherwise + */ + public function hasTables() + { + return $this->getSchema()->hasTables(); + } +} + +/** + * @package Piwik + */ +interface Piwik_Db_Schema_Interface +{ + static public function isAvailable(); + + public function getTableCreateSql($tableName); + public function getTablesCreateSql(); + + public function createDatabase( $dbName = null ); + public function dropDatabase(); + + public function createTables(); + public function createAnonymousUser(); + public function truncateAllTables(); + public function dropTables( $doNotDelete = array() ); + + public function getTablesNames(); + public function getTablesInstalled($forceReload = true, $idSite = null); + public function hasTables(); +} diff --git a/core/Db/Schema/Myisam.php b/core/Db/Schema/Myisam.php new file mode 100644 index 0000000000..790e07db38 --- /dev/null +++ b/core/Db/Schema/Myisam.php @@ -0,0 +1,484 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Piwik + */ + +/** + * MySQL schema + * + * @package Piwik + * @subpackage Piwik_Db + */ +class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface +{ + /** + * Is this MySQL storage engine available? + * + * @param string $engineName + * @return bool True if available and enabled; false otherwise + */ + static private function hasStorageEngine($engineName) + { + $db = Zend_Registry::get('db'); + $allEngines = $db->fetchAssoc('SHOW ENGINES'); + if(key_exists($engineName, $allEngines)) + { + $support = $allEngines[$engineName]['Support']; + return $support == 'DEFAULT' || $support == 'YES'; + } + return false; + } + + /** + * Is this schema available? + * + * @return bool True if schema is available; false otherwise + */ + static public function isAvailable() + { + return self::hasStorageEngine('MyISAM'); + } + + /** + * Get the SQL to create Piwik tables + * + * @return array of strings containing SQL + */ + public function getTablesCreateSql() + { + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + $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, + date_registered TIMESTAMP NULL, + PRIMARY KEY(login), + UNIQUE KEY uniq_keytoken(token_auth) + ) 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) + ) 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, + timezone VARCHAR( 50 ) NOT NULL, + currency CHAR( 3 ) NOT NULL, + excluded_ips TEXT NOT NULL, + excluded_parameters VARCHAR ( 255 ) NOT NULL, + PRIMARY KEY(idsite) + ) 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) + ) 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, + `revenue` float NOT NULL, + `deleted` tinyint(4) NOT NULL default '0', + PRIMARY KEY (`idsite`,`idgoal`) + ) DEFAULT CHARSET=utf8 + ", + + 'logger_message' => "CREATE TABLE {$prefixTables}logger_message ( + idlogger_message INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp TIMESTAMP NULL, + message TEXT NULL, + PRIMARY KEY(idlogger_message) + ) DEFAULT CHARSET=utf8 + ", + + 'logger_api_call' => "CREATE TABLE {$prefixTables}logger_api_call ( + idlogger_api_call INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + class_name VARCHAR(255) NULL, + method_name VARCHAR(255) NULL, + parameter_names_default_values TEXT NULL, + parameter_values TEXT NULL, + execution_time FLOAT NULL, + caller_ip INT UNSIGNED NULL, + timestamp TIMESTAMP NULL, + returned_value TEXT NULL, + PRIMARY KEY(idlogger_api_call) + ) DEFAULT CHARSET=utf8 + ", + + 'logger_error' => "CREATE TABLE {$prefixTables}logger_error ( + idlogger_error INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp TIMESTAMP NULL, + message TEXT NULL, + errno INTEGER UNSIGNED NULL, + errline INTEGER UNSIGNED NULL, + errfile VARCHAR(255) NULL, + backtrace TEXT NULL, + PRIMARY KEY(idlogger_error) + ) DEFAULT CHARSET=utf8 + ", + + 'logger_exception' => "CREATE TABLE {$prefixTables}logger_exception ( + idlogger_exception INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, + timestamp TIMESTAMP NULL, + message TEXT NULL, + errno INTEGER UNSIGNED NULL, + errline INTEGER UNSIGNED NULL, + errfile VARCHAR(255) NULL, + backtrace TEXT NULL, + PRIMARY KEY(idlogger_exception) + ) 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, + PRIMARY KEY(idaction), + INDEX index_type_hash (type, hash) + ) DEFAULT CHARSET=utf8 + ", + + 'log_visit' => "CREATE TABLE {$prefixTables}log_visit ( + idvisit INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, + idsite INTEGER(10) UNSIGNED NOT NULL, + visitor_localtime TIME NOT NULL, + visitor_idcookie CHAR(32) NOT NULL, + visitor_returning TINYINT(1) NOT NULL, + visit_first_action_time DATETIME NOT NULL, + visit_last_action_time DATETIME NOT NULL, + visit_server_date DATE NOT NULL, + visit_exit_idaction_url INTEGER(11) NOT NULL, + visit_entry_idaction_url INTEGER(11) NOT NULL, + visit_total_actions SMALLINT(5) UNSIGNED NOT NULL, + visit_total_time SMALLINT(5) UNSIGNED NOT NULL, + visit_goal_converted TINYINT(1) NOT NULL, + referer_type INTEGER UNSIGNED NULL, + referer_name VARCHAR(70) NULL, + referer_url TEXT NOT NULL, + referer_keyword VARCHAR(255) NULL, + config_md5config CHAR(32) NOT NULL, + config_os CHAR(3) NOT NULL, + config_browser_name VARCHAR(10) NOT NULL, + config_browser_version VARCHAR(20) NOT NULL, + config_resolution VARCHAR(9) NOT NULL, + config_pdf TINYINT(1) NOT NULL, + config_flash TINYINT(1) NOT NULL, + config_java TINYINT(1) NOT NULL, + config_director TINYINT(1) NOT NULL, + config_quicktime TINYINT(1) NOT NULL, + config_realplayer TINYINT(1) NOT NULL, + config_windowsmedia TINYINT(1) NOT NULL, + config_gears TINYINT(1) NOT NULL, + config_silverlight TINYINT(1) NOT NULL, + config_cookie TINYINT(1) NOT NULL, + location_ip INT UNSIGNED NOT NULL, + location_browser_lang VARCHAR(20) NOT NULL, + location_country CHAR(3) NOT NULL, + location_continent CHAR(3) NOT NULL, + PRIMARY KEY(idvisit), + INDEX index_idsite_idvisit (idsite, idvisit), + INDEX index_idsite_date_config (idsite, visit_server_date, config_md5config(8)), + INDEX index_idsite_datetime_config (idsite, visit_last_action_time, config_md5config(8)) + ) DEFAULT CHARSET=utf8 + ", + + 'log_conversion' => "CREATE TABLE `{$prefixTables}log_conversion` ( + idvisit int(10) unsigned NOT NULL, + idsite int(10) unsigned NOT NULL, + visitor_idcookie char(32) NOT NULL, + server_time datetime NOT NULL, + idaction_url int(11) default NULL, + idlink_va int(11) default NULL, + referer_idvisit int(10) unsigned default NULL, + referer_visit_server_date date default NULL, + referer_type int(10) unsigned default NULL, + referer_name varchar(70) default NULL, + referer_keyword varchar(255) default NULL, + visitor_returning tinyint(1) NOT NULL, + location_country char(3) NOT NULL, + location_continent char(3) NOT NULL, + url text NOT NULL, + idgoal int(10) unsigned NOT NULL, + revenue float default NULL, + PRIMARY KEY (idvisit, idgoal), + INDEX index_idsite_datetime ( idsite, server_time ) + ) DEFAULT CHARSET=utf8 + ", + + 'log_link_visit_action' => "CREATE TABLE {$prefixTables}log_link_visit_action ( + idlink_va INTEGER(11) NOT NULL AUTO_INCREMENT, + idvisit INTEGER(10) UNSIGNED NOT NULL, + idaction_url INTEGER(10) UNSIGNED NOT NULL, + idaction_url_ref INTEGER(10) UNSIGNED NOT NULL, + idaction_name INTEGER(10) UNSIGNED, + time_spent_ref_action INTEGER(10) UNSIGNED NOT NULL, + PRIMARY KEY(idlink_va), + INDEX index_idvisit(idvisit) + ) 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)) + ) DEFAULT CHARSET=utf8 + ", + + 'option' => "CREATE TABLE `{$prefixTables}option` ( + option_name VARCHAR( 64 ) NOT NULL, + option_value LONGTEXT NOT NULL, + autoload TINYINT NOT NULL DEFAULT '1', + PRIMARY KEY ( option_name ) + ) 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 FLOAT NULL, + PRIMARY KEY(idarchive, name), + INDEX index_idsite_dates_period(idsite, date1, date2, period, ts_archived), + INDEX index_period_archived(period, ts_archived) + ) 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) + ) DEFAULT CHARSET=utf8 + ", + ); + return $tables; + } + + /** + * Get the SQL to create a specific Piwik table + * + * @param string $tableName + * @return string SQL + */ + public function getTableCreateSql( $tableName ) + { + $tables = Piwik::getTablesCreateSql(); + + if(!isset($tables[$tableName])) + { + throw new Exception("The table '$tableName' SQL creation code couldn't be found."); + } + + return $tables[$tableName]; + } + + /** + * Names of all the prefixed tables in piwik + * Doesn't use the DB + * + * @return array Table names + */ + public function getTablesNames() + { + $aTables = array_keys($this->getTablesCreateSql()); + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + $return = array(); + foreach($aTables as $table) + { + $return[] = $prefixTables.$table; + } + return $return; + } + + private $tablesInstalled = null; + + /** + * Get list of tables installed + * + * @param bool $forceReload Invalidate cache + * @param string $idSite + * @return array Tables installed + */ + public function getTablesInstalled($forceReload = true, $idSite = null) + { + if(is_null($this->tablesInstalled) + || $forceReload === true) + { + $db = Zend_Registry::get('db'); + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + + $allTables = $db->fetchCol("SHOW TABLES"); + + // all the tables to be installed + $allMyTables = $this->getTablesNames(); + + // we get the intersection between all the tables in the DB and the tables to be installed + $tablesInstalled = array_intersect($allMyTables, $allTables); + + // at this point we have only the piwik tables which is good + // but we still miss the piwik generated tables (using the class Piwik_TablePartitioning) + $idSiteInSql = "no"; + if(!is_null($idSite)) + { + $idSiteInSql = $idSite; + } + $allArchiveNumeric = $db->fetchCol("/* SHARDING_ID_SITE = ".$idSiteInSql." */ + SHOW TABLES LIKE '".$prefixTables."archive_numeric%'"); + $allArchiveBlob = $db->fetchCol("/* SHARDING_ID_SITE = ".$idSiteInSql." */ + SHOW TABLES LIKE '".$prefixTables."archive_blob%'"); + + $allTablesReallyInstalled = array_merge($tablesInstalled, $allArchiveNumeric, $allArchiveBlob); + + $this->tablesInstalled = $allTablesReallyInstalled; + } + return $this->tablesInstalled; + } + + /** + * Do tables exist? + * + * @return bool True if tables exist; false otherwise + */ + public function hasTables() + { + return count($this->getTablesInstalled()) != 0; + } + + /** + * Create database + * + * @param string $dbName + */ + public function createDatabase( $dbName = null ) + { + if(is_null($dbName)) + { + $dbName = Zend_Registry::get('config')->database->dbname; + } + Piwik_Exec("CREATE DATABASE IF NOT EXISTS ".$dbName); + } + + /** + * Drop database + */ + public function dropDatabase() + { + $dbName = Zend_Registry::get('config')->database->dbname; + Piwik_Exec("DROP DATABASE IF EXISTS " . $dbName); + + } + + /** + * Create all tables + */ + public function createTables() + { + $db = Zend_Registry::get('db'); + $config = Zend_Registry::get('config'); + $prefixTables = $config->database->tables_prefix; + + $tablesAlreadyInstalled = $this->getTablesInstalled(); + $tablesToCreate = $this->getTablesCreateSql(); + unset($tablesToCreate['archive_blob']); + unset($tablesToCreate['archive_numeric']); + + foreach($tablesToCreate as $tableName => $tableSql) + { + $tableName = $prefixTables . $tableName; + if(!in_array($tableName, $tablesAlreadyInstalled)) + { + $db->query( $tableSql ); + } + } + } + + /** + * Creates an entry in the User table for the "anonymous" user. + */ + public function createAnonymousUser() + { + // 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 = Zend_Registry::get('db'); + $db->query("INSERT INTO ". Piwik_Common::prefixTable("user") . " + VALUES ( 'anonymous', '', 'anonymous', 'anonymous@example.org', 'anonymous', '".Piwik_Date::factory('now')->getDatetime()."' );" ); + } + + /** + * Truncate all tables + */ + public function truncateAllTables() + { + $tablesAlreadyInstalled = $this->getTablesInstalled($forceReload = true); + foreach($tablesAlreadyInstalled as $table) + { + Piwik_Query("TRUNCATE `$table`"); + } + } + + /** + * Drop specific tables + * + * @param array $doNotDelete Names of tables to not delete + */ + public function dropTables( $doNotDelete = array() ) + { + $tablesAlreadyInstalled = $this->getTablesInstalled(); + $db = Zend_Registry::get('db'); + + $doNotDeletePattern = '/('.implode('|',$doNotDelete).')/'; + + foreach($tablesAlreadyInstalled as $tableName) + { + if( count($doNotDelete) == 0 + || (!in_array($tableName,$doNotDelete) + && !preg_match($doNotDeletePattern,$tableName) + ) + ) + { + $db->query("DROP TABLE `$tableName`"); + } + } + } +} diff --git a/core/ExceptionHandler.php b/core/ExceptionHandler.php index f5554ad89c..7c8d070ec0 100644 --- a/core/ExceptionHandler.php +++ b/core/ExceptionHandler.php @@ -32,7 +32,7 @@ function Piwik_ExceptionHandler(Exception $exception) $formatter = new Piwik_Log_Exception_Formatter_ScreenFormatter(); $message = $formatter->format($event); - $message .= "<br><br>And this exception raised another exception \"". $e->getMessage()."\""; + $message .= "<br /><br />And this exception raised another exception \"". $e->getMessage()."\""; Piwik::exitWithErrorMessage( $message ); } diff --git a/core/Form.php b/core/Form.php index 6c00803938..4dfbdc087e 100644 --- a/core/Form.php +++ b/core/Form.php @@ -23,13 +23,13 @@ abstract class Piwik_Form extends HTML_QuickForm { protected $a_formElements = array(); - function __construct( $action = '' ) + function __construct( $action = '', $attributes = '' ) { if(empty($action)) { $action = Piwik_Url::getCurrentQueryString(); } - parent::HTML_QuickForm('form', 'POST', $action); + parent::HTML_QuickForm('form', 'POST', $action, $target='', $attributes); $this->registerRule( 'checkEmail', 'function', 'Piwik_Form_isValidEmailString'); $this->registerRule( 'fieldHaveSameValue', 'function', 'Piwik_Form_fieldHaveSameValue'); @@ -96,6 +96,16 @@ abstract class Piwik_Form extends HTML_QuickForm } } } + function setSelected( $nameElement, $value ) + { + foreach( $this->_elements as $key => $value) + { + if($value->_attributes['name'] == $nameElement) + { + $this->_elements[$key]->_attributes['selected'] = 'selected'; + } + } + } } function Piwik_Form_fieldHaveSameValue($element, $value, $arg) diff --git a/core/FrontController.php b/core/FrontController.php index 9d691d4421..22c99c84b2 100644 --- a/core/FrontController.php +++ b/core/FrontController.php @@ -10,9 +10,6 @@ * @package Piwik */ -// no direct access -defined('PIWIK_INCLUDE_PATH') or die; - /** * @see core/PluginsManager.php * @see core/Translate.php @@ -108,7 +105,7 @@ class Piwik_FrontController if(!class_exists($controllerClassName, false)) { $moduleController = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/Controller.php'; - if( !Zend_Loader::isReadable($moduleController)) + if(!is_readable($moduleController)) { throw new Exception("Module controller $moduleController not found!"); } @@ -163,13 +160,14 @@ class Piwik_FrontController try { Piwik::printSqlProfilingReportZend(); Piwik::printQueryCount(); +/* + if(Piwik::getModule() !== 'API') + { + Piwik::printMemoryUsage(); + Piwik::printTimer(); + } + */ } catch(Exception $e) {} - - if(Piwik::getModule() !== 'API') - { -// Piwik::printMemoryUsage(); -// Piwik::printTimer(); - } } /** @@ -206,16 +204,21 @@ class Piwik_FrontController } $pluginsManager = Piwik_PluginsManager::getInstance(); - $pluginsManager->setPluginsToLoad( Zend_Registry::get('config')->Plugins->Plugins->toArray() ); + $pluginsManager->loadPlugins( Zend_Registry::get('config')->Plugins->Plugins->toArray() ); if($exceptionToThrow) { throw $exceptionToThrow; } - Piwik_Translate::getInstance()->loadUserTranslation(); - Piwik::createDatabaseObject(); + try { + Piwik::createDatabaseObject(); + } catch(Exception $e) { + Piwik_PostEvent('FrontController.badConfigurationFile', $e); + throw $e; + } + Piwik::createLogObject(); // creating the access object, so that core/Updates/* can enforce Super User and use some APIs @@ -230,16 +233,18 @@ class Piwik_FrontController $authAdapter = Zend_Registry::get('auth'); } catch(Exception $e){ throw new Exception("Authentication object cannot be found in the Registry. Maybe the Login plugin is not activated? - <br>You can activate the plugin by adding:<br> - <code>Plugins[] = Login</code><br> + <br />You can activate the plugin by adding:<br /> + <code>Plugins[] = Login</code><br /> under the <code>[Plugins]</code> section in your config/config.inc.php"); } Zend_Registry::get('access')->reloadAccess($authAdapter); + Piwik_Translate::getInstance()->loadUserTranslation(); Piwik::raiseMemoryLimitIfNecessary(); $pluginsManager->setLanguageToLoad( Piwik_Translate::getInstance()->getLanguageToLoad() ); + $pluginsManager->loadTranslations(); $pluginsManager->postLoadPlugins(); Piwik_PostEvent('FrontController.checkForUpdates'); diff --git a/core/Http.php b/core/Http.php new file mode 100644 index 0000000000..9ca4d229e4 --- /dev/null +++ b/core/Http.php @@ -0,0 +1,385 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Piwik + */ + +/** + * Server-side http client to retrieve content from remote servers, and optionally save to a local file. + * Used to check for the latest Piwik version and download updates. + * + * @package Piwik + */ +class Piwik_Http +{ + /** + * Get "best" available transport method for sendHttpRequest() calls. + * + * @return string + */ + static public function getTransportMethod() + { + $method = 'curl'; + if(!extension_loaded('curl')) + { + $method = 'stream'; + if(@ini_get('allow_url_fopen') != '1') + { + $method = 'socket'; + if(!function_exists('fsockopen')) + { + return null; + } + } + } + return $method; + } + + /** + * Sends http request ensuring the request will fail before $timeout seconds + * + * If no $destinationPath is specified, the trimmed response (without header) is returned as a string. + * If a $destinationPath is specified, the response (without header) is saved to a file. + * + * @param string $aUrl + * @param int $timeout + * @param string $userAgent + * @param string $destinationPath + * @param int $followDepth + * @return bool true (or string) on success; false on HTTP response error code (1xx or 4xx) + * @throws Exception for all other errors + */ + static public function sendHttpRequest($aUrl, $timeout, $userAgent = null, $destinationPath = null, $followDepth = 0) + { + // create output file + $file = null; + if($destinationPath) + { + if (($file = @fopen($destinationPath, 'wb')) === false || !is_resource($file)) + { + throw new Exception('Error while creating the file: ' . $destinationPath); + } + } + + return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth); + } + + /** + * Sends http request using the specified transport method + * + * @param string $method + * @param string $aUrl + * @param int $timeout + * @param string $userAgent + * @param string $destinationPath + * @param resource $file + * @param int $followDepth + * @return bool true (or string) on success; false on HTTP response error code (1xx or 4xx) + * @throws Exception for all other errors + */ + static public function sendHttpRequestBy($method = 'socket', $aUrl, $timeout, $userAgent = null, $destinationPath = null, $file = null, $followDepth = 0) + { + if ($followDepth > 5) + { + throw new Exception('Too many redirects ('.$followDepth.')'); + } + + $contentLength = 0; + $fileLength = 0; + + if($method == 'socket') + { + // initialization + $url = @parse_url($aUrl); + if($url === false || !isset($url['scheme'])) + { + throw new Exception('Malformed URL: '.$aUrl); + } + + if($url['scheme'] != 'http') + { + throw new Exception('Invalid protocol/scheme: '.$url['scheme']); + } + $host = $url['host']; + $port = isset($url['port)']) ? $url['port'] : 80; + $path = isset($url['path']) ? $url['path'] : '/'; + if(isset($url['query'])) + { + $path .= '?'.$url['query']; + } + $errno = null; + $errstr = null; + + // connection attempt + if (($fsock = @fsockopen($host, $port, $errno, $errstr, $timeout)) === false || !is_resource($fsock)) + { + if(is_resource($file)) { @fclose($file); } + throw new Exception("Error while connecting to: $host. Please try again later. $errstr"); + } + + // send HTTP request header + fwrite($fsock, + "GET $path HTTP/1.0\r\n" + ."Host: $host".($port != 80 ? ':'.$port : '')."\r\n" + ."User-Agent: Piwik/".Piwik_Version::VERSION.($userAgent ? " $userAgent" : '')."\r\n" + .'Referer: http://'.Piwik_Common::getIpString()."/\r\n" + ."Connection: close\r\n" + ."\r\n" + ); + + $streamMetaData = array('timed_out' => false); + @stream_set_blocking($fsock, true); + @stream_set_timeout($fsock, $timeout); + + // process header + $status = null; + $expectRedirect = false; + + while(!feof($fsock)) + { + $line = fgets($fsock, 4096); + + $streamMetaData = @stream_get_meta_data($fsock); + if($streamMetaData['timed_out']) + { + if(is_resource($file)) { @fclose($file); } + @fclose($fsock); + throw new Exception('Timed out waiting for server response'); + } + + // a blank line marks the end of the server response header + if(rtrim($line, "\r\n") == '') + { + break; + } + + // parse first line of server response header + if(!$status) + { + // expect first line to be HTTP response status line, e.g., HTTP/1.1 200 OK + if(!preg_match('~^HTTP/(\d\.\d)\s+(\d+)(\s*.*)?~', $line, $m)) + { + if(is_resource($file)) { @fclose($file); } + @fclose($fsock); + throw new Exception('Expected server response code. Got '.rtrim($line, "\r\n")); + } + + $status = (integer) $m[2]; + + // Informational 1xx or Client Error 4xx + if ($status < 200 || $status >= 400) + { + if(is_resource($file)) { @fclose($file); } + @fclose($fsock); + return false; + } + + continue; + } + + // handle redirect + if(preg_match('/^Location:\s*(.+)/', rtrim($line, "\r\n"), $m)) + { + if(is_resource($file)) { @fclose($file); } + @fclose($fsock); + // Successful 2xx vs Redirect 3xx + if($status < 300) + { + throw new Exception('Unexpected redirect to Location: '.rtrim($line).' for status code '.$status); + } + return self::sendHttpRequestBy($method, trim($m[1]), $timeout, $userAgent, $pathDestination, $file, $followDepth+1); + } + + // save expected content length for later verification + if(preg_match('/^Content-Length:\s*(\d+)/', $line, $m)) + { + $contentLength = (integer) $m[1]; + } + } + + if(feof($fsock)) + { + throw new Exception('Unexpected end of transmission'); + } + + // process content/body + $response = ''; + + while(!feof($fsock)) + { + $line = fread($fsock, 8192); + + $streamMetaData = @stream_get_meta_data($fsock); + if($streamMetaData['timed_out']) + { + if(is_resource($file)) { @fclose($file); } + @fclose($fsock); + throw new Exception('Timed out waiting for server response'); + } + + $fileLength += strlen($line); + + if(is_resource($file)) + { + // save to file + fwrite($file, $line); + } + else + { + // concatenate to response string + $response .= $line; + } + } + + // determine success or failure + @fclose(@$fsock); + } + else if($method == 'stream') + { + $response = false; + + // we make sure the request takes less than a few seconds to fail + // we create a stream_context (works in php >= 5.2.1) + // we also set the socket_timeout (for php < 5.2.1) + $default_socket_timeout = @ini_get('default_socket_timeout'); + @ini_set('default_socket_timeout', $timeout); + + $ctx = null; + if(function_exists('stream_context_create')) { + $stream_options = array( + 'http' => array( + 'header' => 'User-Agent: Piwik/'.Piwik_Version::VERSION.($userAgent ? " $userAgent" : '')."\r\n" + .'Referer: http://'.Piwik_Common::getIpString()."/\r\n", + 'max_redirects' => 5, // PHP 5.1.0 + 'timeout' => $timeout, // PHP 5.2.1 + ) + ); + $ctx = stream_context_create($stream_options); + } + + $response = @file_get_contents($aUrl, 0, $ctx); + $fileLength = strlen($response); + + if(is_resource($file)) + { + // save to file + fwrite($file, $response); + } + + // restore the socket_timeout value + if(!empty($default_socket_timeout)) + { + @ini_set('default_socket_timeout', $default_socket_timeout); + } + } + else if($method == 'curl') + { + $ch = @curl_init(); + + $curl_options = array( + // internal to ext/curl + CURLOPT_BINARYTRANSFER => is_resource($file), + + // curl options (sorted oldest to newest) + CURLOPT_URL => $aUrl, + CURLOPT_REFERER => 'http://'.Piwik_Common::getIpString(), + CURLOPT_USERAGENT => 'Piwik/'.Piwik_Version::VERSION.($userAgent ? " $userAgent" : ''), + CURLOPT_HEADER => false, + CURLOPT_CONNECTTIMEOUT => $timeout, + ); + @curl_setopt_array($ch, $curl_options); + + /* + * as of php 5.2.0, CURLOPT_FOLLOWLOCATION can't be set if + * in safe_mode or open_basedir is set + */ + if((string)ini_get('safe_mode') == '' && ini_get('open_basedir') == '') + { + $curl_options = array( + // curl options (sorted oldest to newest) + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 5, + ); + @curl_setopt_array($ch, $curl_options); + } + + if(is_resource($file)) + { + // write output directly to file + @curl_setopt($ch, CURLOPT_FILE, $file); + } + else + { + // internal to ext/curl + @curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + } + + ob_start(); + $response = @curl_exec($ch); + ob_end_clean(); + + if($response === true) + { + $response = ''; + } + else if($response === false) + { + $errstr = curl_error($ch); + if($errstr != '') + { + throw new Exception('curl_exec: '.$errstr); + } + $response = ''; + } + + $contentLength = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); + $fileLength = is_resource($file) ? curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : strlen($response); + + @curl_close($ch); + unset($ch); + } + else + { + throw new Exception('Invalid request method: '.$method); + } + + if(is_resource($file)) + { + fflush($file); + @fclose($file); + + $fileSize = filesize($destinationPath); + if((($contentLength > 0) && ($fileLength != $contentLength)) || ($fileSize != $fileLength)) + { + throw new Exception('File size error: '.$destinationPath.'; expected '.$contentLength.' bytes; received '.$fileLength.' bytes; saved '.$fileSize.' bytes to file'); + } + return true; + } + + if(($contentLength > 0) && ($fileLength != $contentLength)) + { + throw new Exception('Content length error: expected '.$contentLength.' bytes; received '.$fileLength.' bytes'); + } + return trim($response); + } + + /** + * Fetch the file at $url in the destination $pathDestination + * @param string $url + * @param string $pathDestination + * @param int $tries + * @return true on success, throws Exception on failure + */ + static public function fetchRemoteFile($url, $pathDestination, $tries = 0) + { + @ignore_user_abort(true); + Piwik::setMaxExecutionTime(0); + return self::sendHttpRequest($url, 10, 'Update', $pathDestination, $tries); + } +} diff --git a/core/Log.php b/core/Log.php index 0be03c98d0..15481b315d 100644 --- a/core/Log.php +++ b/core/Log.php @@ -42,7 +42,7 @@ abstract class Piwik_Log extends Zend_Log $this->fileFormatter = $fileFormatter; $this->screenFormatter = $screenFormatter; - $this->logToDatabaseTableName = Piwik::prefixTable($logToDatabaseTableName); + $this->logToDatabaseTableName = Piwik_Common::prefixTable($logToDatabaseTableName); $this->logToDatabaseColumnMapping = $logToDatabaseColumnMapping; } @@ -61,7 +61,6 @@ abstract class Piwik_Log extends Zend_Log function addWriteToNull() { - Zend_Loader::loadClass('Zend_Log_Writer_Null'); $this->addWriter( new Zend_Log_Writer_Null ); } @@ -90,7 +89,7 @@ abstract class Piwik_Log extends Zend_Log /** * Log an event */ - public function log($event, $priority) + public function log($event, $priority, $extras = null) { // sanity checks if (empty($this->_writers)) { @@ -139,8 +138,7 @@ class Piwik_Log_Formatter_FileFormatter implements Zend_Log_Formatter_Interface } $ts = $event['timestamp']; unset($event['timestamp']); - $str = $ts . ' ' . implode(" ", $event) . "\n"; - return $str; + return $ts . ' ' . implode(" ", $event) . "\n"; } } @@ -156,11 +154,10 @@ class Piwik_Log_Formatter_ScreenFormatter implements Zend_Log_Formatter_Interfac // no injection in error messages, backtrace when displayed on screen return array_map('htmlspecialchars', $event); } - + function format($string) { - $string = self::getFormattedString($string); - return $string; + return self::getFormattedString($string); } static public function getFormattedString($string) @@ -168,7 +165,17 @@ class Piwik_Log_Formatter_ScreenFormatter implements Zend_Log_Formatter_Interfac if(Piwik_Common::isPhpCliMode()) { $string = str_replace(array('<br>','<br />','<br/>'), "\n", $string); - $string = strip_tags($string); + if(is_array($string)) + { + for($i=0; $i< count($string); $i++) + { + $string[$i] = strip_tags($string[$i]); + } + } + else + { + $string = strip_tags($string); + } } return $string; } diff --git a/core/Log/APICall.php b/core/Log/APICall.php index ae390798b4..8148c77014 100644 --- a/core/Log/APICall.php +++ b/core/Log/APICall.php @@ -47,7 +47,7 @@ class Piwik_Log_APICall extends Piwik_Log $event['execution_time'] = $executionTime; $event['returned_value'] = is_array($returnedValue) ? serialize($returnedValue) : $returnedValue; - parent::log($event, Piwik_Log::INFO); + parent::log($event, Piwik_Log::INFO, null); } } @@ -67,8 +67,8 @@ class Piwik_Log_APICall_Formatter_ScreenFormatter extends Piwik_Log_Formatter_Sc */ public function format($event) { - $str = "\n<br> "; - $str .= "Called: {$event['class_name']}.{$event['method_name']} (took {$event['execution_time']}ms) \n<br>"; + $str = "\n<br /> "; + $str .= "Called: {$event['class_name']}.{$event['method_name']} (took {$event['execution_time']}ms)\n<br /> "; $str .= "Parameters: "; $parameterNamesAndDefault = unserialize($event['parameter_names_default_values']); $parameterValues = unserialize($event['parameter_values']); @@ -90,10 +90,10 @@ class Piwik_Log_APICall_Formatter_ScreenFormatter extends Piwik_Log_Formatter_Sc $i++; } - $str .= "\n<br> "; + $str .= "\n<br /> "; // $str .= "Returned: ".$this->formatValue($event['returned_value']); - $str .= "\n<br> "; + $str .= "\n<br /> "; return parent::format($str); } diff --git a/core/Log/Error.php b/core/Log/Error.php index 7c41fa91e6..a6f71ac718 100644 --- a/core/Log/Error.php +++ b/core/Log/Error.php @@ -51,7 +51,7 @@ class Piwik_Log_Error extends Piwik_Log $event['errline'] = $errline; $event['backtrace'] = $backtrace; - parent::log($event, Piwik_Log::ERR); + parent::log($event, Piwik_Log::ERR, null); } } @@ -106,10 +106,10 @@ class Piwik_Log_Error_Formatter_ScreenFormatter extends Piwik_Log_Formatter_Scre default: $strReturned .= "Unknown error ($errno)"; break; } $strReturned .= ":</b> <i>$errstr</i> in <b>$errfile</b> on line <b>$errline</b>\n"; - $strReturned .= "<br><br>Backtrace --><DIV style='font-family:Courier;font-size:10pt'>"; - $strReturned .= str_replace("\n", "<br>\n", $backtrace); - $strReturned .= "</div><br><br>"; - $strReturned .= "\n</pre></div><br>"; + $strReturned .= "<br /><br />Backtrace --><div style=\"font-family:Courier;font-size:10pt\">"; + $strReturned .= str_replace("\n", "<br />\n", $backtrace); + $strReturned .= "</div><br /><br />"; + $strReturned .= "\n</pre></div><br />"; return parent::format($strReturned); } diff --git a/core/Log/Exception.php b/core/Log/Exception.php index 6ae10b64cd..fe3e6ef98e 100644 --- a/core/Log/Exception.php +++ b/core/Log/Exception.php @@ -52,7 +52,7 @@ class Piwik_Log_Exception extends Piwik_Log $event['errline'] = $exception->getLine(); $event['backtrace'] = $exception->getTraceAsString(); - parent::log($event, Piwik_Log::CRIT); + parent::log($event, Piwik_Log::CRIT, null); } } @@ -80,7 +80,7 @@ class Piwik_Log_Exception_Formatter_ScreenFormatter extends Piwik_Log_Formatter_ $backtrace = $event['backtrace'] ; $outputFormat = strtolower(Piwik_Common::getRequestVar('format', 'html', 'string')); - $response = new Piwik_API_ResponseBuilder(null, $outputFormat); + $response = new Piwik_API_ResponseBuilder($outputFormat); $message = $response->getResponseException(new Exception($errstr)); return parent::format($message); } diff --git a/core/Log/Message.php b/core/Log/Message.php index 239fd1bc53..f5b1120a2e 100644 --- a/core/Log/Message.php +++ b/core/Log/Message.php @@ -38,7 +38,7 @@ class Piwik_Log_Message extends Piwik_Log { $event = array(); $event['message'] = $message; - parent::log($event, Piwik_Log::INFO); + parent::log($event, Piwik_Log::INFO, null); } } diff --git a/core/Nonce.php b/core/Nonce.php new file mode 100644 index 0000000000..632ed55b07 --- /dev/null +++ b/core/Nonce.php @@ -0,0 +1,81 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Piwik + */ + +/** + * Nonce class. + * + * A cryptographic nonce -- "number used only once" -- is often recommended as part of a robust defense against cross-site request forgery (CSRF/XSRF). + * Desrable characteristics: limited lifetime, uniqueness, unpredictability (pseudo-randomness). + * + * We use a session-dependent nonce with a configurable expiration that combines and hashes: + * - a private salt because it's non-public + * - time() because it's unique + * - a mix of PRNGs (pseudo-random number generators) to increase entropy and make it less predictable + * + * @package Piwik + */ +class Piwik_Nonce +{ + /** + * Generate nonce + * + * @param string $id Unique id to avoid namespace conflicts, e.g., ModuleName.ActionName + * @param int $ttl Optional time-to-live in seconds; default is 5 minutes + * @return string Nonce + */ + static public function getNonce($id, $ttl = 300) + { + // save session-dependent nonce + $ns = new Zend_Session_Namespace($id); + $nonce = $ns->nonce; + + // re-use an unexpired nonce (a small deviation from the "used only once" principle, so long as we do not reset the expiration) + // to handle browser pre-fetch or double fetch caused by some browser add-ons/extensions + if(empty($nonce)) + { + // generate a new nonce + $nonce = md5(Piwik_Common::getSalt() . time() . Piwik_Common::generateUniqId()); + $ns->nonce = $nonce; + $ns->setExpirationSeconds($ttl, 'nonce'); + } + + return $nonce; + } + + /** + * Verify nonce and check referrer (if present, i.e., it may be suppressed by the browser or a proxy/network). + * + * @param string $id Unique id + * @param string $cnonce Nonce sent to client + * @return bool true if valid; false otherwise + */ + static public function verifyNonce($id, $cnonce) + { + $ns = new Zend_Session_Namespace($id); + $nonce = $ns->nonce; + + // validate token + if(empty($cnonce) || $cnonce !== $nonce) + { + return false; + } + + // validate referer + $referer = Piwik_Url::getReferer(); + if(!empty($referer) && (Piwik_Url::getLocalReferer() === false)) + { + return false; + } + + return true; + } +} diff --git a/core/Option.php b/core/Option.php index da1905dc82..75244c7f3a 100644 --- a/core/Option.php +++ b/core/Option.php @@ -54,7 +54,7 @@ class Piwik_Option return $this->all[$name]; } $value = Piwik_FetchOne( 'SELECT option_value - FROM `' . Piwik::prefixTable('option') . '` + FROM `' . Piwik_Common::prefixTable('option') . '` WHERE option_name = ?', $name); if($value === false) { @@ -74,7 +74,7 @@ class Piwik_Option public function set($name, $value, $autoload = 0) { $autoload = (int)$autoload; - Piwik_Query('INSERT INTO `'. Piwik::prefixTable('option') . '` (option_name, option_value, autoload) '. + Piwik_Query('INSERT INTO `'. Piwik_Common::prefixTable('option') . '` (option_name, option_value, autoload) '. ' VALUES (?, ?, ?) '. ' ON DUPLICATE KEY UPDATE option_value = ?', array($name, $value, $autoload, $value)); @@ -89,7 +89,7 @@ class Piwik_Option return; } $all = Piwik_FetchAll('SELECT option_value, option_name - FROM `'. Piwik::prefixTable('option') . '` + FROM `'. Piwik_Common::prefixTable('option') . '` WHERE autoload = 1'); foreach($all as $option) { @@ -97,6 +97,17 @@ class Piwik_Option } $loaded = true; } + + /** + * Clears the cache + * Used in unit tests to reset the state of the object between tests + * + * @return void + */ + public function clearCache() + { + $this->all = array(); + } } function Piwik_GetOption($name) diff --git a/core/Period.php b/core/Period.php index 7fd59b84e8..4e5e6e37bb 100644 --- a/core/Period.php +++ b/core/Period.php @@ -30,8 +30,7 @@ abstract class Piwik_Period protected $subperiodsProcessed = false; protected $label = null; protected $date = null; - - protected static $unknowPeriodException = "The period '%s' is not supported. Try 'day' or 'week' or 'month' or 'year'"; + static protected $errorAvailablePeriods = 'day, week, month, year'; public function __construct( $date ) { @@ -44,7 +43,7 @@ abstract class Piwik_Period * @param $date Piwik_Date object * @return Piwik_Period */ - static public function factory($strPeriod, $date) + static public function factory($strPeriod, Piwik_Date $date) { switch ($strPeriod) { case 'day': @@ -64,11 +63,12 @@ abstract class Piwik_Period break; default: - throw new Exception(sprintf(self::$unknowPeriodException, $strPeriod)); + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidPeriod', array($strPeriod, self::$errorAvailablePeriods))); break; } } + /** * Returns the first day of the period * @@ -186,25 +186,6 @@ abstract class Piwik_Period $this->subperiods[] = $date; } - /** - * A period is finished if all the subperiods are finished - */ - public function isFinished() - { - if(!$this->subperiodsProcessed) - { - $this->generate(); - } - foreach($this->subperiods as $period) - { - if(!$period->isFinished()) - { - return false; - } - } - return true; - } - public function toString() { if(!$this->subperiodsProcessed) @@ -221,8 +202,7 @@ abstract class Piwik_Period public function __toString() { - $elements = $this->toString(); - return implode(",", $elements); + return implode(",", $this->toString()); } public function get( $part= null ) @@ -231,7 +211,7 @@ abstract class Piwik_Period { $this->generate(); } - return $this->date->get($part); + return $this->date->toString($part); } abstract public function getPrettyString(); diff --git a/core/Period/Day.php b/core/Period/Day.php index 32686a0f51..a5eda257ed 100644 --- a/core/Period/Day.php +++ b/core/Period/Day.php @@ -41,15 +41,6 @@ class Piwik_Period_Day extends Piwik_Period return $out; } - public function isFinished() - { - $todayMidnight = Piwik_Date::today(); - if($this->date->isEarlier($todayMidnight)) - { - return true; - } - } - public function getNumberOfSubperiods() { return 0; diff --git a/core/Period/Month.php b/core/Period/Month.php index 40fda847ca..1918ec3650 100644 --- a/core/Period/Month.php +++ b/core/Period/Month.php @@ -55,18 +55,4 @@ class Piwik_Period_Month extends Piwik_Period $currentDay = $currentDay->addDay(1); } } - - public function isFinished() - { - if(!$this->subperiodsProcessed) - { - $this->generate(); - } - // a month is finished - // if current month > month AND current year == year - // OR if current year > year - $year = $this->date->get("Y"); - return ( date("m") > $this->date->get("m") && date("Y") == $year) - || date("Y") > $year; - } } diff --git a/core/Period/Range.php b/core/Period/Range.php index ba30cb9f36..2f1b33bdad 100644 --- a/core/Period/Range.php +++ b/core/Period/Range.php @@ -18,11 +18,12 @@ */ class Piwik_Period_Range extends Piwik_Period { - public function __construct( $strPeriod, $strDate ) + public function __construct( $strPeriod, $strDate, $timezone = 'UTC' ) { $this->strPeriod = $strPeriod; $this->strDate = $strDate; $this->defaultEndDate = null; + $this->timezone = $timezone; } public function getLocalizedShortString() { @@ -71,10 +72,6 @@ class Piwik_Period_Range extends Piwik_Period case 'year': $startDate = $date->subMonth( 12 * $n ); break; - - default: - throw new Exception(sprintf(self::$unknowPeriodException, $this->strPeriod)); - break; } return $startDate; } @@ -125,7 +122,7 @@ class Piwik_Period_Range extends Piwik_Period } else { - $defaultEndDate = Piwik_Date::today(); + $defaultEndDate = Piwik_Date::factory('now', $this->timezone); } if($lastOrPrevious == 'last') { @@ -154,7 +151,7 @@ class Piwik_Period_Range extends Piwik_Period } else { - throw new Exception("The date '$this->strDate' is not a date range. Should have the following format: 'lastN' or 'previousN' or 'YYYY-MM-DD,YYYY-MM-DD'."); + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidDateRange', array($this->strDate, ' \'lastN\', \'previousN\', \'YYYY-MM-DD,YYYY-MM-DD\''))); } $endSubperiod = Piwik_Period::factory($this->strPeriod, $endDate); diff --git a/core/Period/Year.php b/core/Period/Year.php index 7e04b8c898..19500be9ed 100644 --- a/core/Period/Year.php +++ b/core/Period/Year.php @@ -43,7 +43,7 @@ class Piwik_Period_Year extends Piwik_Period } parent::generate(); - $year = $this->date->get("Y"); + $year = $this->date->toString("Y"); for($i=1; $i<=12; $i++) { $this->addSubperiod( new Piwik_Period_Month( diff --git a/core/Piwik.php b/core/Piwik.php index af7c5f083c..67fd9a24c4 100644 --- a/core/Piwik.php +++ b/core/Piwik.php @@ -10,69 +10,279 @@ * @package Piwik */ -// no direct access -defined('PIWIK_INCLUDE_PATH') or die; - /** * @see core/Translate.php */ require_once PIWIK_INCLUDE_PATH . '/core/Translate.php'; /** + * @see mysqli_set_charset + * @see parse_ini_file + */ +require_once PIWIK_INCLUDE_PATH . '/libs/upgradephp/common.php'; + +/** * Main piwik helper class. * Contains static functions you can call from the plugins. - * + * * @package Piwik */ class Piwik { const CLASSES_PREFIX = "Piwik_"; - + public static $idPeriods = array( 'day' => 1, 'week' => 2, 'month' => 3, 'year' => 4, ); - + +/* + * Prefix/unprefix class name + */ + + /** + * Prefix class name (if needed) + * + * @param string $class + * @return string + */ + static public function prefixClass( $class ) + { + if(substr_count($class, Piwik::CLASSES_PREFIX) > 0) + { + return $class; + } + return Piwik::CLASSES_PREFIX.$class; + } + + /** + * Unprefix class name (if needed) + * + * @param string $class + * @return string + */ + static public function unprefixClass( $class ) + { + $lenPrefix = strlen(Piwik::CLASSES_PREFIX); + if(substr($class, 0, $lenPrefix) == Piwik::CLASSES_PREFIX) + { + return substr($class, $lenPrefix); + } + return $class; + } + +/* + * Installation / Uninstallation + */ + + /** + * Installation helper + */ + static public function install() + { + Piwik_Common::mkdir(Zend_Registry::get('config')->smarty->compile_dir); + } + + /** + * Uninstallation helper + */ + static public function uninstall() + { + Piwik_Db_Schema::getInstance()->dropTables(); + } + + /** + * Returns true if Piwik is installed + * + * @since 0.6.3 + * + * @return bool True if installed; false otherwise + */ + static public function isInstalled() + { + return Piwik_Db_Schema::getInstance()->hasTables(); + } + +/* + * File and directory operations + */ + + /** + * Copy recursively from $source to $target. + * + * @param string $source eg. './tmp/latest' + * @param string $target eg. '.' + * @param bool $excludePhp + */ + static public function copyRecursive($source, $target, $excludePhp=false ) + { + if ( is_dir( $source ) ) + { + @mkdir( $target ); + $d = dir( $source ); + while ( false !== ( $entry = $d->read() ) ) + { + if ( $entry == '.' || $entry == '..' ) + { + continue; + } + + $sourcePath = $source . '/' . $entry; + if ( is_dir( $sourcePath ) ) + { + self::copyRecursive( $sourcePath, $target . '/' . $entry, $excludePhp ); + continue; + } + $destPath = $target . '/' . $entry; + self::copy($sourcePath, $destPath, $excludePhp); + } + $d->close(); + } + else + { + self::copy($source, $target, $excludePhp); + } + } + + /** + * Copy individual file from $source to $target. + * + * @param string $source eg. './tmp/latest/index.php' + * @param string $target eg. './index.php' + * @param bool $excludePhp + * @return bool + */ + static public function copy($source, $dest, $excludePhp=false) + { + static $phpExtensions = array('php', 'tpl'); + + if($excludePhp) + { + $path_parts = pathinfo($source); + if(in_array($path_parts['extension'], $phpExtensions)) + { + return true; + } + } + + if(!@copy( $source, $dest )) + { + @chmod($dest, 0755); + if(!@copy( $source, $dest )) + { + throw new Exception(" + Error while copying file to <code>$dest</code>. <br /> + Please check that the web server has enough permission to overwrite this file. <br /> + For example, on a linux server, if your apache user is www-data you can try to execute:<br /> + <code>chown -R www-data:www-data ".Piwik_Common::getPathToPiwikRoot()."</code><br /> + <code>chmod -R 0755 ".Piwik_Common::getPathToPiwikRoot()."</code><br /> + "); + } + } + return true; + } + + /** + * Recursively delete a directory + * + * @param string $dir Directory name + * @param boolean $deleteRootToo Delete specified top-level directory as well + */ + static public function unlinkRecursive($dir, $deleteRootToo) + { + if(!$dh = @opendir($dir)) + { + return; + } + while (false !== ($obj = readdir($dh))) + { + if($obj == '.' || $obj == '..') + { + continue; + } + + if (!@unlink($dir . '/' . $obj)) + { + self::unlinkRecursive($dir.'/'.$obj, true); + } + } + closedir($dh); + if ($deleteRootToo) + { + @rmdir($dir); + } + return; + } + + /** + * Recursively find pathnames that match a pattern + * @see glob() + * + * @param string $sDir directory + * @param string $sPattern pattern + * @param int $nFlags glob() flags + * @return array + */ + public static function globr($sDir, $sPattern, $nFlags = NULL) + { + if(($aFiles = glob("$sDir/$sPattern", $nFlags)) == false) + { + $aFiles = array(); + } + if(($aDirs = glob("$sDir/*", GLOB_ONLYDIR)) != false) + { + foreach ($aDirs as $sSubDir) + { + $aSubFiles = self::globr($sSubDir, $sPattern, $nFlags); + $aFiles = array_merge($aFiles, $aSubFiles); + } + } + return $aFiles; + } + /** * Checks that the directories Piwik needs write access are actually writable * Displays a nice error page if permissions are missing on some directories + * + * @param array $directoriesToCheck Array of directory names to check */ static public function checkDirectoriesWritableOrDie( $directoriesToCheck = null ) { $resultCheck = Piwik::checkDirectoriesWritable( $directoriesToCheck ); - if( array_search(false, $resultCheck) !== false ) - { - $directoryList = ''; - foreach($resultCheck as $dir => $bool) + if( array_search(false, $resultCheck) === false ) + { + return; + } + $directoryList = ''; + foreach($resultCheck as $dir => $bool) + { + $realpath = Piwik_Common::realpath($dir); + if(!empty($realpath) && $bool === false) { - $realpath = Piwik_Common::realpath($dir); - if(!empty($realpath) && $bool === false) - { - $directoryList .= "<code>chmod 777 $realpath</code><br>"; - } + $directoryList .= "<code>chmod 777 $realpath</code><br />"; } - $directoryList .= ''; - $directoryMessage = "<p><b>Piwik couldn't write to some directories</b>.</p> <p>Try to Execute the following commands on your Linux server:</P>"; - $directoryMessage .= $directoryList; - $directoryMessage .= "<p>If this doesn't work, you can try to create the directories with your FTP software, and set the CHMOD to 777 (with your FTP software, right click on the directories, permissions)."; - $directoryMessage .= "<p>After applying the modifications, you can <a href='index.php'>refresh the page</a>."; - $directoryMessage .= "<p>If you need more help, try <a href='misc/redirectToUrl.php?url=http://piwik.org'>Piwik.org</a>."; - - Piwik_ExitWithMessage($directoryMessage, false, true); } + $directoryList .= ''; + $directoryMessage = "<p><b>Piwik couldn't write to some directories</b>.</p> <p>Try to Execute the following commands on your Linux server:</P>"; + $directoryMessage .= $directoryList; + $directoryMessage .= "<p>If this doesn't work, you can try to create the directories with your FTP software, and set the CHMOD to 777 (with your FTP software, right click on the directories, permissions)."; + $directoryMessage .= "<p>After applying the modifications, you can <a href='index.php'>refresh the page</a>."; + $directoryMessage .= "<p>If you need more help, try <a href='misc/redirectToUrl.php?url=http://piwik.org'>Piwik.org</a>."; + + Piwik_ExitWithMessage($directoryMessage, false, true); } - + /** * Checks if directories are writable and create them if they do not exist. - * + * * @param array $directoriesToCheck array of directories to check - if not given default Piwik directories that needs write permission are checked * @return array direcory name => true|false (is writable) */ static public function checkDirectoriesWritable($directoriesToCheck = null) { - if( $directoriesToCheck == null ) + if( $directoriesToCheck == null ) { $directoriesToCheck = array( '/config', @@ -80,9 +290,9 @@ class Piwik '/tmp/templates_c', '/tmp/cache', '/tmp/latest', - ); + ); } - + $resultCheck = array(); foreach($directoriesToCheck as $directoryToCheck) { @@ -90,12 +300,12 @@ class Piwik { $directoryToCheck = PIWIK_USER_PATH . $directoryToCheck; } - + if(!file_exists($directoryToCheck)) { Piwik_Common::mkdir($directoryToCheck, 0755, false); } - + $directory = Piwik_Common::realpath($directoryToCheck); $resultCheck[$directory] = false; if($directory !== false // realpath() returns FALSE on failure @@ -106,29 +316,168 @@ class Piwik } return $resultCheck; } - + /** - * Returns the Javascript code to be inserted on every page to track + * Generate .htaccess files at runtime to avoid permission problems. + */ + static public function createHtAccessFiles() + { + // deny access to these folders + $directoriesToProtect = array( + '/config', + '/core', + '/lang', + ); + foreach($directoriesToProtect as $directoryToProtect) + { + Piwik_Common::createHtAccess(PIWIK_INCLUDE_PATH . $directoryToProtect); + } + + // more selective allow/deny filters + $allowAny = "<Files \"*\">\nAllow from all\nSatisfy any\n</Files>\n"; + $allowStaticAssets = "<Files ~ \"\\.(test\.php|gif|ico|jpg|png|js|css|swf)$\">\nSatisfy any\nAllow from all\n</Files>\n"; + $denyDirectPhp = "<Files ~ \"\\.(php|php4|php5|inc|tpl)$\">\nDeny from all\n</Files>\n"; + $directoriesToProtect = array( + '/js' => $allowAny, + '/libs' => $denyDirectPhp . $allowStaticAssets, + '/plugins' => $denyDirectPhp . $allowStaticAssets, + '/themes' => $denyDirectPhp . $allowStaticAssets, + ); + foreach($directoriesToProtect as $directoryToProtect => $content) + { + Piwik_Common::createHtAccess(PIWIK_INCLUDE_PATH . $directoryToProtect, $content); + } + } + + /** + * Generate web.config files at runtime * - * @param int $idSite - * @param string $piwikUrl http://path/to/piwik/directory/ - * @param string $actionName - * @return string + * Note: for IIS 7 and above */ - static public function getJavascriptCode($idSite, $piwikUrl, $actionName = "''") - { - $jsTag = file_get_contents( PIWIK_INCLUDE_PATH . "/core/Tracker/javascriptTag.tpl"); - $jsTag = nl2br(htmlentities($jsTag)); - $piwikUrl = preg_match('~^(http|https)://(.*)$~', $piwikUrl, $matches); - $piwikUrl = $matches[2]; - $jsTag = str_replace('{$actionName}', $actionName, $jsTag); - $jsTag = str_replace('{$idSite}', $idSite, $jsTag); - $jsTag = str_replace('{$piwikUrl}', $piwikUrl, $jsTag); - $jsTag = str_replace('{$hrefTitle}', Piwik::getRandomTitle(), $jsTag); - return $jsTag; + static public function createWebConfigFiles() + { + @file_put_contents(PIWIK_INCLUDE_PATH . '/web.config', +'<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <system.webServer> + <security> + <requestFiltering> + <hiddenSegments> + <add segment="config" /> + <add segment="core" /> + <add segment="lang" /> + </hiddenSegments> + <fileExtensions> + <add fileExtension=".tpl" allowed="false" /> + </fileExtensions> + </requestFiltering> + </security> + <directoryBrowse enabled="false" /> + <defaultDocument> + <files> + <remove value="index.php" /> + <add value="index.php" /> + </files> + </defaultDocument> + </system.webServer> +</configuration>'); + + // deny direct access to .php files + $directoriesToProtect = array( + '/libs', + '/plugins', + ); + foreach($directoriesToProtect as $directoryToProtect) + { + @file_put_contents(PIWIK_INCLUDE_PATH . $directoryToProtect . '/web.config', +'<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <system.webServer> + <security> + <requestFiltering> + <denyUrlSequences> + <add sequence=".php" /> + </denyUrlSequences> + </requestFiltering> + </security> + </system.webServer> +</configuration>'); + } } /** + * Get file integrity information (in PIWIK_INCLUDE_PATH). + * + * @return array(bool, string, ...) Return code (true/false), followed by zero or more error messages + */ + static public function getFileIntegrityInformation() + { + $exclude = array( + 'robots.txt', + ); + $messages = array(); + $messages[] = true; + + // ignore dev environments + if(file_exists(PIWIK_INCLUDE_PATH . '/.svn')) + { + $messages[] = Piwik_Translate('General_WarningFileIntegritySkipped'); + return $messages; + } + + $manifest = PIWIK_INCLUDE_PATH . '/config/manifest.inc.php'; + if(!file_exists($manifest)) + { + $messages[] = Piwik_Translate('General_WarningFileIntegrityNoManifest'); + return $messages; + } + + require_once $manifest; + + $files = Manifest::$files; + + $hasMd5file = function_exists('md5_file'); + foreach($files as $path => $props) + { + if(in_array($path, $exclude)) + { + continue; + } + + $file = PIWIK_INCLUDE_PATH . '/' . $path; + + if(!file_exists($file)) + { + $messages[] = Piwik_Translate('General_ExceptionMissingFile', $file); + } + else if(filesize($file) != $props[0]) + { + $messages[] = Piwik_Translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file))); + } + else if($hasMd5file && (@md5_file($file) !== $props[1])) + { + $messages[] = Piwik_Translate('General_ExceptionFileIntegrity', $file); + } + } + + if(count($messages) > 1) + { + $messages[0] = false; + } + + if(!$hasMd5file) + { + $messages[] = Piwik_Translate('General_WarningFileIntegrityNoMd5file'); + } + + return $messages; + } + +/* + * PHP environment settings + */ + + /** * Set maximum script execution time. * * @param int max execution time in seconds (0 = no limit) @@ -140,15 +489,46 @@ class Piwik @set_time_limit($executionTime); } + /** + * Get php memory_limit + * + * Prior to PHP 5.2.1, or on Windows, --enable-memory-limit is not a + * compile-time default, so ini_get('memory_limit') may return false. + * + * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes + * @return int memory limit in megabytes + */ static public function getMemoryLimitValue() { if($memory = ini_get('memory_limit')) { - return substr($memory, 0, strlen($memory) - 1); + // handle shorthand byte options (case-insensitive) + $shorthandByteOption = substr($memory, -1); + switch($shorthandByteOption) + { + case 'G': + case 'g': + return substr($memory, 0, -1) * 1024; + case 'M': + case 'm': + return substr($memory, 0, -1); + case 'K': + case 'k': + return substr($memory, 0, -1) / 1024; + } + return $memory / 1048576; } return false; } - + + /** + * Set PHP memory limit + * + * Note: system settings may prevent scripts from overriding the master value + * + * @param int $minimumMemoryLimit + * @return bool true if set; false otherwise + */ static public function setMemoryLimit($minimumMemoryLimit) { $currentValue = self::getMemoryLimitValue(); @@ -160,7 +540,12 @@ class Piwik } return false; } - + + /** + * Raise PHP memory limit if below the minimum required + * + * @return bool true if set; false otherwise + */ static public function raiseMemoryLimitIfNecessary() { $minimumMemoryLimit = Zend_Registry::get('config')->General->minimum_memory_limit; @@ -170,22 +555,25 @@ class Piwik { return self::setMemoryLimit($minimumMemoryLimit); } - + return false; } - + +/* + * Logging and error handling + */ + static public function log($message = '') { Zend_Registry::get('logger_message')->logEvent($message); - Zend_Registry::get('logger_message')->logEvent( "<br>" . PHP_EOL); + Zend_Registry::get('logger_message')->logEvent( "<br />" . PHP_EOL); } - - + static public function error($message = '') { trigger_error($message, E_USER_ERROR); } - + /** * Display the message in a nice red font with a nice icon * ... and dies @@ -194,46 +582,54 @@ class Piwik { $output = "<style>a{color:red;}</style>\n". "<div style='color:red;font-family:Georgia;font-size:120%'>". - "<p><img src='themes/default/images/error_medium.png' style='vertical-align:middle; float:left;padding:20 20 20 20'>". + "<p><img src='themes/default/images/error_medium.png' style='vertical-align:middle; float:left;padding:20 20 20 20' />". $message. "</p></div>"; print(Piwik_Log_Formatter_ScreenFormatter::getFormattedString($output)); exit; } - + +/* + * Profiling + */ + /** - * Computes the division of i1 by i2. If either i1 or i2 are not number, or if i2 has a value of zero - * we return 0 to avoid the division by zero. + * Get total number of queries * - * @param numeric $i1 - * @param numeric $i2 - * @return numeric The result of the division or zero + * @return int number of queries */ - static public function secureDiv( $i1, $i2 ) - { - if ( is_numeric($i1) && is_numeric($i2) && floatval($i2) != 0) - { - return $i1 / $i2; - } - return 0; - } static public function getQueryCount() { $profiler = Zend_Registry::get('db')->getProfiler(); return $profiler->getTotalNumQueries(); } + + /** + * Get total elapsed time (in seconds) + * + * @return int elapsed time + */ static public function getDbElapsedSecs() { $profiler = Zend_Registry::get('db')->getProfiler(); return $profiler->getTotalElapsedSecs(); } + + /** + * Print number of queries and elapsed time + */ static public function printQueryCount() { $totalTime = self::getDbElapsedSecs(); $queryCount = self::getQueryCount(); Piwik::log("Total queries = $queryCount (total sql time = ".round($totalTime,2)."s)"); } - + + /** + * Print profiling report for the tracker + * + * @param Piwik_Tracker_Db $db Tracker database object (or null) + */ static public function printSqlProfilingReportTracker( $db = null ) { if(!function_exists('maxSumMsFirst')) @@ -243,20 +639,20 @@ class Piwik return $a['sum_time_ms'] < $b['sum_time_ms']; } } - + if(is_null($db)) { $db = Piwik_Tracker::getDatabase(); } $tableName = Piwik_Common::prefixTable('log_profiling'); - + $all = $db->fetchAll('SELECT * FROM '.$tableName ); - if($all === false) + if($all === false) { return; } uasort($all, 'maxSumMsFirst'); - + $infoIndexedByQuery = array(); foreach($all as $infoQuery) { @@ -264,24 +660,23 @@ class Piwik $count = $infoQuery['count']; $sum_time_ms = $infoQuery['sum_time_ms']; $infoIndexedByQuery[$query] = array('count' => $count, 'sumTimeMs' => $sum_time_ms); - } + } Piwik::getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery); } /** - * Outputs SQL Profiling reports + * Outputs SQL Profiling reports * It is automatically called when enabling the SQL profiling in the config file enable_sql_profiler - * */ static function printSqlProfilingReportZend() { $profiler = Zend_Registry::get('db')->getProfiler(); - + if(!$profiler->getEnabled()) { throw new Exception("To display the profiler you should enable enable_sql_profiler on your config/config.ini.php file"); } - + $infoIndexedByQuery = array(); foreach($profiler->getQueryProfiles() as $query) { @@ -306,9 +701,9 @@ class Piwik } } uasort( $infoIndexedByQuery, 'sortTimeDesc'); - - Piwik::log('<hr><b>SQL Profiler</b>'); - Piwik::log('<hr><b>Summary</b>'); + + Piwik::log('<hr /><b>SQL Profiler</b>'); + Piwik::log('<hr /><b>Summary</b>'); $totalTime = $profiler->getTotalElapsedSecs(); $queryCount = $profiler->getTotalNumQueries(); $longestTime = 0; @@ -321,44 +716,63 @@ class Piwik } $str = 'Executed ' . $queryCount . ' queries in ' . round($totalTime,3) . ' seconds' . "\n"; $str .= '(Average query length: ' . round($totalTime / $queryCount,3) . ' seconds)' . "\n"; - $str .= '<br>Queries per second: ' . round($queryCount / $totalTime,1) . "\n"; - $str .= '<br>Longest query length: ' . round($longestTime,3) . " seconds (<code>$longestQuery</code>) \n"; + $str .= '<br />Queries per second: ' . round($queryCount / $totalTime,1) . "\n"; + $str .= '<br />Longest query length: ' . round($longestTime,3) . " seconds (<code>$longestQuery</code>) \n"; Piwik::log($str); Piwik::getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery); } - + + /** + * Log a breakdown by query + * + * @param array $infoIndexedByQuery + */ static private function getSqlProfilingQueryBreakdownOutput( $infoIndexedByQuery ) { - Piwik::log('<hr><b>Breakdown by query</b>'); + Piwik::log('<hr /><b>Breakdown by query</b>'); $output = ''; - foreach($infoIndexedByQuery as $query => $queryInfo) + foreach($infoIndexedByQuery as $query => $queryInfo) { $timeMs = round($queryInfo['sumTimeMs'],1); $count = $queryInfo['count']; $avgTimeString = ''; - if($count > 1) + if($count > 1) { $avgTimeMs = $timeMs / $count; - $avgTimeString = " (average = <b>". round($avgTimeMs,1) . "ms</b>)"; + $avgTimeString = " (average = <b>". round($avgTimeMs,1) . "ms</b>)"; } $query = preg_replace('/([\t\n\r ]+)/', ' ', $query); $output .= "Executed <b>$count</b> time". ($count==1?'':'s') ." in <b>".$timeMs."ms</b> $avgTimeString <pre>\t$query</pre>"; } Piwik::log($output); } - + + /** + * Print timer + */ static public function printTimer() { echo Zend_Registry::get('timer'); } - static public function printMemoryLeak($prefix = '', $suffix = '<br>') + /** + * Print memory leak + * + * @param string $prefix + * @param string $suffix + */ + static public function printMemoryLeak($prefix = '', $suffix = '<br />') { echo $prefix; echo Zend_Registry::get('timer')->getMemoryLeak(); echo $suffix; } - + + /** + * Print memory usage + * + * @param string $prefixString + */ static public function printMemoryUsage( $prefixString = null ) { $memory = false; @@ -370,7 +784,7 @@ class Piwik { $memory = memory_get_usage(); } - + if($memory !== false) { $usage = round( $memory / 1024 / 1024, 2); @@ -385,385 +799,220 @@ class Piwik Piwik::log("Memory usage function not found."); } } - - static public function getPrettySizeFromBytes($size) + +/* + * Amounts, Percentages, Currency, Time, Math Operations, and Pretty Printing + */ + + /** + * Computes the division of i1 by i2. If either i1 or i2 are not number, or if i2 has a value of zero + * we return 0 to avoid the division by zero. + * + * @param numeric $i1 + * @param numeric $i2 + * @return numeric The result of the division or zero + */ + static public function secureDiv( $i1, $i2 ) { - $bytes = array('','K','M','G','T'); - foreach($bytes as $val) + if ( is_numeric($i1) && is_numeric($i2) && floatval($i2) != 0) { - if($size > 1024) - { - $size = $size / 1024; - } - else - { - break; - } + return $i1 / $i2; } - return round($size, 1)." ".$val; + return 0; } /** - * Returns true if PHP was invoked as CGI or command-line interface (shell) + * Safely compute a percentage. Return 0 to avoid division by zero. * - * @deprecated deprecated in 0.4.4 - * @see Piwik_Common::isPhpCliMode() - * @return bool true if PHP invoked as a CGI or from CLI + * @param numeric $dividend + * @param numeric $divisor + * @param int $precision + * @return numeric */ - static public function isPhpCliMode() + static public function getPercentageSafe($dividend, $divisor, $precision = 0) { - return Piwik_Common::isPhpCliMode(); + if($divisor == 0) + { + return 0; + } + return round(100 * $dividend / $divisor, $precision); } - - static public function getCurrency() + + /** + * Get currency symbol for a site + * + * @param int $idSite + * @return string + */ + static public function getCurrency($idSite) { - static $symbol = null; - if(is_null($symbol)) + static $symbols = null; + if(is_null($symbols)) { - $symbol = trim(Zend_Registry::get('config')->General->default_currency); + $symbols = Piwik_SitesManager_API::getInstance()->getCurrencySymbols(); } - return $symbol; + $site = new Piwik_Site($idSite); + return $symbols[$site->getCurrency()]; } - static public function getPrettyMoney($value) + /** + * Pretty format monetary value for a site + * + * @param numeric $value + * @param int $idSite + * @return string + */ + static public function getPrettyMoney($value, $idSite) { - $symbol = self::getCurrency(); - return sprintf("$symbol%.2f", $value); + $currencyBefore = self::getCurrency($idSite); + $currencyAfter = ''; + + // manually put the currency symbol after the amount for euro + // (maybe more currencies prefer this notation?) + if(in_array($currencyBefore,array('€'))) + { + $currencyAfter = ' '.$currencyBefore; + $currencyBefore = ''; + } + return sprintf("$currencyBefore %s$currencyAfter", $value); } - - static public function getPercentageSafe($dividend, $divisor, $precision = 0) + + /** + * Pretty format a memory size value + * + * @param numeric $size in bytes + * @return string + */ + static public function getPrettySizeFromBytes($size) { - if($divisor == 0) + $bytes = array('','K','M','G','T'); + foreach($bytes as $val) { - return 0; + if($size > 1024) + { + $size = $size / 1024; + } + else + { + break; + } } - return round(100 * $dividend / $divisor, $precision); + return round($size, 1)." ".$val; } - + + /** + * Pretty format a time + * + * @param numeric $numberOfSeconds + * @return string + */ static public function getPrettyTimeFromSeconds($numberOfSeconds) { $numberOfSeconds = (double)$numberOfSeconds; $days = floor($numberOfSeconds / 86400); - + $minusDays = $numberOfSeconds - $days * 86400; $hours = floor($minusDays / 3600); - + $minusDaysAndHours = $minusDays - $hours * 3600; $minutes = floor($minusDaysAndHours / 60 ); - + $seconds = $minusDaysAndHours - $minutes * 60; - + if($days > 0) { - return sprintf("%d days %d hours", $days, $hours); + $return = sprintf(Piwik_Translate('General_DaysHours'), $days, $hours); } elseif($hours > 0) { - return sprintf("%d hours %d min", $hours, $minutes); + $return = sprintf(Piwik_Translate('General_HoursMinutes'), $hours, $minutes); } elseif($minutes > 0) { - return sprintf("%d min %ds", $minutes, $seconds); + $return = sprintf(Piwik_Translate('General_MinutesSeconds'), $minutes, $seconds); } else { - return sprintf("%ds", $seconds); + $return = sprintf(Piwik_Translate('General_Seconds'), $seconds); } + return str_replace(' ', ' ', $return); } - - static public function getRandomTitle() - { - $titles = array( 'Web analytics', - 'Analytics', - 'Web analytics api', - 'Open source analytics', - 'Open source web analytics', - 'Google Analytics alternative', - 'open source Google Analytics', - 'Free analytics', - 'Analytics software', - 'Free web analytics', - 'Free web statistics', - 'Web 2.0 analytics', - 'Statistics web 2.0', - ); - $id = abs(intval(md5(substr(Piwik_Url::getCurrentHost(),7)))); - $title = $titles[ $id % count($titles)]; - return $title; - } - - static public function getTableCreateSql( $tableName ) + + /** + * Returns the Javascript code to be inserted on every page to track + * + * @param int $idSite + * @param string $piwikUrl http://path/to/piwik/directory/ + * @param string $actionName + * @return string + */ + static public function getJavascriptCode($idSite, $piwikUrl, $actionName = "''") { - $tables = Piwik::getTablesCreateSql(); - - if(!isset($tables[$tableName])) - { - throw new Exception("The table '$tableName' SQL creation code couldn't be found."); - } - - return $tables[$tableName]; + $jsTag = file_get_contents( PIWIK_INCLUDE_PATH . "/core/Tracker/javascriptTag.tpl"); + $jsTag = nl2br(htmlentities($jsTag)); + $piwikUrl = preg_match('~^(http|https)://(.*)$~', $piwikUrl, $matches); + $piwikUrl = $matches[2]; + $jsTag = str_replace('{$actionName}', $actionName, $jsTag); + $jsTag = str_replace('{$idSite}', $idSite, $jsTag); + $jsTag = str_replace('{$piwikUrl}', $piwikUrl, $jsTag); + $jsTag = str_replace('{$hrefTitle}', Piwik::getRandomTitle(), $jsTag); + return $jsTag; } - - static public function getTablesCreateSql() + + /** + * Generate a title for image tags + * + * @return string + */ + static public function getRandomTitle() { - $config = Zend_Registry::get('config'); - $prefixTables = $config->database->tables_prefix; - $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, - date_registered TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(login), - UNIQUE INDEX uniq_keytoken(token_auth) - ) 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) - ) 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 DEFAULT CURRENT_TIMESTAMP NOT NULL, - PRIMARY KEY(idsite) - ) 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) - ) 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, - `revenue` float NOT NULL, - `deleted` tinyint(4) NOT NULL default '0', - PRIMARY KEY (`idsite`,`idgoal`) - ) DEFAULT CHARSET=utf8 - ", - - 'logger_message' => "CREATE TABLE {$prefixTables}logger_message ( - idlogger_message INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - timestamp TIMESTAMP NULL, - message TEXT NULL, - PRIMARY KEY(idlogger_message) - ) DEFAULT CHARSET=utf8 - ", - - 'logger_api_call' => "CREATE TABLE {$prefixTables}logger_api_call ( - idlogger_api_call INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - class_name VARCHAR(255) NULL, - method_name VARCHAR(255) NULL, - parameter_names_default_values TEXT NULL, - parameter_values TEXT NULL, - execution_time FLOAT NULL, - caller_ip BIGINT UNSIGNED NULL, - timestamp TIMESTAMP NULL, - returned_value TEXT NULL, - PRIMARY KEY(idlogger_api_call) - ) DEFAULT CHARSET=utf8 - ", - - 'logger_error' => "CREATE TABLE {$prefixTables}logger_error ( - idlogger_error INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - timestamp TIMESTAMP NULL, - message TEXT NULL, - errno INTEGER UNSIGNED NULL, - errline INTEGER UNSIGNED NULL, - errfile VARCHAR(255) NULL, - backtrace TEXT NULL, - PRIMARY KEY(idlogger_error) - ) DEFAULT CHARSET=utf8 - ", - - 'logger_exception' => "CREATE TABLE {$prefixTables}logger_exception ( - idlogger_exception INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, - timestamp TIMESTAMP NULL, - message TEXT NULL, - errno INTEGER UNSIGNED NULL, - errline INTEGER UNSIGNED NULL, - errfile VARCHAR(255) NULL, - backtrace TEXT NULL, - PRIMARY KEY(idlogger_exception) - ) DEFAULT CHARSET=utf8 - ", - - - 'log_action' => "CREATE TABLE {$prefixTables}log_action ( - idaction INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - name VARCHAR(255) NOT NULL, - hash INTEGER(10) UNSIGNED NOT NULL, - type TINYINT UNSIGNED NULL, - PRIMARY KEY(idaction), - INDEX index_type_hash (type, hash) - ) DEFAULT CHARSET=utf8 - ", - - 'log_visit' => "CREATE TABLE {$prefixTables}log_visit ( - idvisit INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, - idsite INTEGER(10) UNSIGNED NOT NULL, - visitor_localtime TIME NOT NULL, - visitor_idcookie CHAR(32) NOT NULL, - visitor_returning TINYINT(1) NOT NULL, - visit_first_action_time DATETIME NOT NULL, - visit_last_action_time DATETIME NOT NULL, - visit_server_date DATE NOT NULL, - visit_exit_idaction_url INTEGER(11) NOT NULL, - visit_entry_idaction_url INTEGER(11) NOT NULL, - visit_total_actions SMALLINT(5) UNSIGNED NOT NULL, - visit_total_time SMALLINT(5) UNSIGNED NOT NULL, - visit_goal_converted TINYINT(1) NOT NULL, - referer_type INTEGER UNSIGNED NULL, - referer_name VARCHAR(70) NULL, - referer_url TEXT NOT NULL, - referer_keyword VARCHAR(255) NULL, - config_md5config CHAR(32) NOT NULL, - config_os CHAR(3) NOT NULL, - config_browser_name VARCHAR(10) NOT NULL, - config_browser_version VARCHAR(20) NOT NULL, - config_resolution VARCHAR(9) NOT NULL, - config_pdf TINYINT(1) NOT NULL, - config_flash TINYINT(1) NOT NULL, - config_java TINYINT(1) NOT NULL, - config_director TINYINT(1) NOT NULL, - config_quicktime TINYINT(1) NOT NULL, - config_realplayer TINYINT(1) NOT NULL, - config_windowsmedia TINYINT(1) NOT NULL, - config_gears TINYINT(1) NOT NULL, - config_silverlight TINYINT(1) NOT NULL, - config_cookie TINYINT(1) NOT NULL, - location_ip BIGINT UNSIGNED NOT NULL, - location_browser_lang VARCHAR(20) NOT NULL, - location_country CHAR(3) NOT NULL, - location_continent CHAR(3) NOT NULL, - PRIMARY KEY(idvisit), - INDEX index_idsite_date (idsite, visit_server_date) - ) DEFAULT CHARSET=utf8 - ", - - 'log_conversion' => "CREATE TABLE `{$prefixTables}log_conversion` ( - `idvisit` int(10) unsigned NOT NULL, - `idsite` int(10) unsigned NOT NULL, - `visitor_idcookie` char(32) NOT NULL, - `server_time` datetime NOT NULL, - `visit_server_date` date NOT NULL, - `idaction_url` int(11) default NULL, - `idlink_va` int(11) default NULL, - `referer_idvisit` int(10) unsigned default NULL, - `referer_visit_server_date` date default NULL, - `referer_type` int(10) unsigned default NULL, - `referer_name` varchar(70) default NULL, - `referer_keyword` varchar(255) default NULL, - `visitor_returning` tinyint(1) NOT NULL, - `location_country` char(3) NOT NULL, - `location_continent` char(3) NOT NULL, - `url` text NOT NULL, - `idgoal` int(10) unsigned NOT NULL, - `revenue` float default NULL, - PRIMARY KEY (`idvisit`,`idgoal`), - KEY `index_idsite_date` (`idsite`,`visit_server_date`) - ) DEFAULT CHARSET=utf8 - ", - - 'log_link_visit_action' => "CREATE TABLE {$prefixTables}log_link_visit_action ( - idlink_va INTEGER(11) NOT NULL AUTO_INCREMENT, - idvisit INTEGER(10) UNSIGNED NOT NULL, - idaction_url INTEGER(10) UNSIGNED NOT NULL, - idaction_url_ref INTEGER(10) UNSIGNED NOT NULL, - idaction_name INTEGER(10) UNSIGNED, - time_spent_ref_action INTEGER(10) UNSIGNED NOT NULL, - PRIMARY KEY(idlink_va), - INDEX index_idvisit(idvisit) - ) DEFAULT CHARSET=utf8 - ", - - 'log_profiling' => "CREATE TABLE {$prefixTables}log_profiling ( - query TEXT NOT NULL, - count INTEGER UNSIGNED NULL, - sum_time_ms FLOAT NULL, - UNIQUE INDEX query(query(100)) - ) DEFAULT CHARSET=utf8 - ", - - 'option' => "CREATE TABLE `{$prefixTables}option` ( - option_name VARCHAR( 64 ) NOT NULL , - option_value LONGTEXT NOT NULL , - autoload TINYINT NOT NULL DEFAULT '1', - PRIMARY KEY ( option_name ) - ) 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 FLOAT NULL, - PRIMARY KEY(idarchive, name), - KEY `index_all` (`idsite`,`date1`,`date2`,`name`,`ts_archived`) - ) 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), - KEY `index_all` (`idsite`,`date1`,`date2`,`name`,`ts_archived`) - ) DEFAULT CHARSET=utf8 - ", + static $titles = array( + 'Web analytics', + 'Analytics', + 'Real time web analytics', + 'Real time analytics', + 'Open source analytics', + 'Open source web analytics', + 'Google Analytics alternative', + 'Open source Google Analytics', + 'Free analytics', + 'Analytics software', + 'Free web analytics', + 'Free web statistics', ); - return $tables; + $id = abs(intval(md5(Piwik_Url::getCurrentHost()))); + $title = $titles[ $id % count($titles)]; + return $title; } - + +/* + * Access + */ + + /** + * Get current user login + * + * @return string + */ static public function getCurrentUserLogin() { return Zend_Registry::get('access')->getLogin(); } - - static public function getCurrentUserTokenAuth() - { - return Zend_Registry::get('access')->getTokenAuth(); - } - + /** - * Returns the plugin currently being used to display the page + * Get current user's token auth * - * @return Piwik_Plugin + * @return string */ - static public function getCurrentPlugin() + static public function getCurrentUserTokenAuth() { - return Piwik_PluginsManager::getInstance()->getLoadedPlugin(Piwik::getModule()); + return Zend_Registry::get('access')->getTokenAuth(); } - + /** * Returns true if the current user is either the super user, or the user $theUser * Used when modifying user preference: this usually requires super user or being the user itself. - * + * * @param string $theUser * @return bool */ @@ -776,8 +1025,10 @@ class Piwik return false; } } - + /** + * Check that current user is either the specified user or the superuser + * * @param string $theUser * @throws exception if the user is neither the super user nor the user $theUser */ @@ -793,9 +1044,10 @@ class Piwik throw new Piwik_Access_NoAccessException("The user has to be either the Super User or the user '$theUser' itself."); } } - + /** * Returns true if the current user is the Super User + * * @return bool */ static public function isUserIsSuperUser() @@ -807,21 +1059,32 @@ class Piwik return false; } } - + /** * Helper method user to set the current as Super User. * This should be used with great care as this gives the user all permissions. */ - static public function setUserIsSuperUser() + static public function setUserIsSuperUser( $bool = true ) { - Zend_Registry::get('access')->setSuperUser(); + Zend_Registry::get('access')->setSuperUser($bool); } - + + /** + * Check that user is the superuser + * + * @throws Exception if not the superuser + */ static public function checkUserIsSuperUser() { Zend_Registry::get('access')->checkUserIsSuperUser(); } - + + /** + * Returns true if the user has admin access to the sites + * + * @param mixed $idSites + * @return bool + */ static public function isUserHasAdminAccess( $idSites ) { try{ @@ -831,12 +1094,23 @@ class Piwik return false; } } - + + /** + * Check user has admin access to the sites + * + * @param mixed $idSites + * @throws Exception if user doesn't have admin access to the sites + */ static public function checkUserHasAdminAccess( $idSites ) { Zend_Registry::get('access')->checkUserHasAdminAccess( $idSites ); } - + + /** + * Returns true if the user has admin access to any sites + * + * @return bool + */ static public function isUserHasSomeAdminAccess() { try{ @@ -846,17 +1120,23 @@ class Piwik return false; } } - + + /** + * Check user has admin access to any sites + * + * @throws Exception if user doesn't have admin access to any sites + */ static public function checkUserHasSomeAdminAccess() { Zend_Registry::get('access')->checkUserHasSomeAdminAccess(); } - - static public function checkUserHasSomeViewAccess() - { - Zend_Registry::get('access')->checkUserHasSomeViewAccess(); - } - + + /** + * Returns true if the user has view access to the sites + * + * @param mixed $idSites + * @return bool + */ static public function isUserHasViewAccess( $idSites ) { try{ @@ -866,635 +1146,139 @@ class Piwik return false; } } - - static public function checkUserHasViewAccess( $idSites ) - { - Zend_Registry::get('access')->checkUserHasViewAccess( $idSites ); - } - - static public function prefixClass( $class ) - { - if(substr_count($class, Piwik::CLASSES_PREFIX) > 0) - { - return $class; - } - return Piwik::CLASSES_PREFIX.$class; - } - static public function unprefixClass( $class ) - { - $lenPrefix = strlen(Piwik::CLASSES_PREFIX); - if(substr($class, 0, $lenPrefix) == Piwik::CLASSES_PREFIX) - { - return substr($class, $lenPrefix); - } - return $class; - } /** - * Returns the current module read from the URL (eg. 'API', 'UserSettings', etc.) - * - * @return string - */ - static public function getModule() - { - return Piwik_Common::getRequestVar('module', '', 'string'); - } - - /** - * Returns the current action read from the URL + * Check user has view access to the sites * - * @return string + * @param mixed $idSites + * @throws Exception if user doesn't have view access to sites */ - static public function getAction() - { - return Piwik_Common::getRequestVar('action', '', 'string'); - } - - /** - * returns false if the URL to redirect to is already this URL - */ - static public function redirectToModule( $newModule, $newAction = '' ) + static public function checkUserHasViewAccess( $idSites ) { - $currentModule = self::getModule(); - $currentAction = self::getAction(); - - if($currentModule != $newModule - || $currentAction != $newAction ) - { - - $newUrl = 'index.php' . Piwik_Url::getCurrentQueryStringWithParametersModified( - array('module' => $newModule, 'action' => $newAction) - ); - - Piwik_Url::redirectToUrl($newUrl); - } - return false; + Zend_Registry::get('access')->checkUserHasViewAccess( $idSites ); } /** - * Get "best" available transport method for sendHttpRequest() calls. + * Returns true if the user has view access to any sites + * + * @return bool */ - static public function getTransportMethod() + static public function isUserHasSomeViewAccess() { - $method = 'curl'; - if(!extension_loaded('curl')) - { - $method = 'stream'; - if(@ini_get('allow_url_fopen') != '1') - { - $method = 'socket'; - if(preg_match('/(^|,|\s)fsockopen($|,|\s)/', @ini_get('disable_functions'))) - { - return null; - } - } + try{ + self::checkUserHasSomeViewAccess(); + return true; + } catch( Exception $e){ + return false; } - return $method; } /** - * Sends http request ensuring the request will fail before $timeout seconds + * Check user has view access to any sites * - * If no $destinationPath is specified, the trimmed response (without header) is returned as a string. - * If a $destinationPath is specified, the response (without header) is saved to a file. - * - * @param string $aUrl - * @param int $timeout - * @param string $userAgent - * @param string $destinationPath - * @param int $followDepth - * @return true (or string) on success; false on HTTP response error code (1xx or 4xx); throws exception on all other errors + * @throws Exception if user doesn't have view access to any sites */ - static public function sendHttpRequest($aUrl, $timeout, $userAgent = null, $destinationPath = null, $followDepth = 0) + static public function checkUserHasSomeViewAccess() { - // create output file - $file = null; - if($destinationPath) - { - if (($file = @fopen($destinationPath, 'wb')) === false || !is_resource($file)) - { - throw new Exception('Error while creating the file: ' . $destinationPath); - } - } - - return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth); + Zend_Registry::get('access')->checkUserHasSomeViewAccess(); } - static public function sendHttpRequestBy($method = 'socket', $aUrl, $timeout, $userAgent = null, $destinationPath = null, $file = null, $followDepth = 0) - { - if ($followDepth > 3) - { - throw new Exception('Too many redirects ('.$followDepth.')'); - } - - $contentLength = 0; - - if($method == 'socket') - { - // initialization - $url = @parse_url($aUrl); - if($url === false || !isset($url['scheme'])) - { - throw new Exception('Malformed URL: '.$aUrl); - } - - if($url['scheme'] != 'http') - { - throw new Exception('Invalid protocol/scheme: '.$url['scheme']); - } - $host = $url['host']; - $port = isset($url['port)']) ? $url['port'] : 80; - $path = isset($url['path']) ? $url['path'] : '/'; - if(isset($url['query'])) - { - $path .= '?'.$url['query']; - } - $errno = null; - $errstr = null; - - // connection attempt - if (($fsock = @fsockopen($host, $port, $errno, $errstr, $timeout)) === false || !is_resource($fsock)) - { - if(is_resource($file)) { @fclose($file); } - throw new Exception("Error while connecting to: $host. Please try again later. $errstr"); - } - - // send HTTP request header - fwrite($fsock, - "GET $path HTTP/1.0\r\n" - ."Host: $host".($port != 80 ? ':'.$port : '')."\r\n" - ."User-Agent: Piwik/".Piwik_Version::VERSION.($userAgent ? " $userAgent" : '')."\r\n" - .'Referer: http://'.Piwik_Common::getIpString()."/\r\n" - ."Connection: close\r\n" - ."\r\n" - ); - - $streamMetaData = array('timed_out' => false); - @stream_set_blocking($fsock, true); - @stream_set_timeout($fsock, $timeout); - - // process header - $status = null; - $expectRedirect = false; - $fileLength = 0; - - while (!feof($fsock)) - { - $line = fgets($fsock, 4096); - - $streamMetaData = @stream_get_meta_data($fsock); - if($streamMetaData['timed_out']) - { - if(is_resource($file)) { @fclose($file); } - @fclose($fsock); - throw new Exception('Timed out waiting for server response'); - } - - // a blank line marks the end of the server response header - if(rtrim($line, "\r\n") == '') - { - break; - } - - // parse first line of server response header - if(!$status) - { - // expect first line to be HTTP response status line, e.g., HTTP/1.1 200 OK - if(!preg_match('~^HTTP/(\d\.\d)\s+(\d+)(\s*.*)?~', $line, $m)) - { - if(is_resource($file)) { @fclose($file); } - @fclose($fsock); - throw new Exception('Expected server response code. Got '.rtrim($line, "\r\n")); - } - - $status = (integer) $m[2]; - - // Informational 1xx or Client Error 4xx - if ($status < 200 || $status >= 400) - { - if(is_resource($file)) { @fclose($file); } - @fclose($s); - return false; - } - - continue; - } - - // handle redirect - if(preg_match('/^Location:\s*(.+)/', rtrim($line, "\r\n"), $m)) - { - if(is_resource($file)) { @fclose($file); } - @fclose($s); - // Successful 2xx vs Redirect 3xx - if($status < 300) - { - throw new Exception('Unexpected redirect to Location: '.rtrim($line).' for status code '.$status); - } - return self::sendHttpRequest(trim($m[1]), $pathDestination, $tries+1); - } - - // save expected content length for later verification - if(preg_match('/^Content-Length:\s*(\d+)/', $line, $m)) - { - $contentLength = (integer) $m[1]; - } - } - - if(feof($fsock)) - { - throw new Exception('Unexpected end of transmission'); - } - - // process content/body - $response = ''; - - while (!feof($fsock)) - { - $line = fread($fsock, 8192); - - $streamMetaData = @stream_get_meta_data($fsock); - if($streamMetaData['timed_out']) - { - if(is_resource($file)) { @fclose($file); } - @fclose($fsock); - throw new Exception('Timed out waiting for server response'); - } - - if(is_resource($file)) - { - // save to file - $fileLength += fwrite($file, $line); - } - else - { - // concatenate to response string - $response .= $line; - } - } - - // determine success or failure - @fclose(@$fsock); - } - else if($method == 'stream') - { - $response = false; - - // we make sure the request takes less than a few seconds to fail - // we create a stream_context (works in php >= 5.2.1) - // we also set the socket_timeout (for php < 5.2.1) - $default_socket_timeout = @ini_get('default_socket_timeout'); - @ini_set('default_socket_timeout', $timeout); - - $ctx = null; - if(function_exists('stream_context_create')) { - $stream_options = array( - 'http' => array( - 'header' => 'User-Agent: Piwik/'.Piwik_Version::VERSION.($userAgent ? " $userAgent" : '')."\r\n" - .'Referer: http://'.Piwik_Common::getIpString()."/\r\n", - 'max_redirects' => 3, // PHP 5.1.0 - 'timeout' => $timeout, // PHP 5.2.1 - ) - ); - $ctx = stream_context_create($stream_options); - } - - $response = @file_get_contents($aUrl, 0, $ctx); - if(is_resource($file)) - { - // save to file - fwrite($file, $response); - } - - // restore the socket_timeout value - if(!empty($default_socket_timeout)) - { - @ini_set('default_socket_timeout', $default_socket_timeout); - } - } - else if($method == 'curl') - { - $ch = @curl_init(); - - $curl_options = array( - CURLOPT_URL => $aUrl, - CURLOPT_HEADER => false, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $timeout, - CURLOPT_BINARYTRANSFER => is_resource($file), - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 3, - CURLOPT_USERAGENT => 'Piwik/'.Piwik_Version::VERSION.($userAgent ? " $userAgent" : ''), - CURLOPT_REFERER => 'http://'.Piwik_Common::getIpString(), - ); - @curl_setopt_array($ch, $curl_options); - - $response = @curl_exec($ch); - if(is_resource($file)) - { - // save to file - fwrite($file, $response); - } - - @curl_close($ch); - unset($ch); - } - else - { - throw new Exception('Invalid request method: '.$method); - } - - if(is_resource($file)) - { - fflush($file); - @fclose($file); - if($contentLength && (($fileLength != $contentLength) || (filesize($destinationPath) != $contentLength))) - { - throw new Exception('File size error: '.$destinationPath.'; expected '.$contentLength.' bytes; received '.$fileLength.' bytes'); - } - return true; - } - - if($contentLength && strlen($response) != $contentLength) - { - throw new Exception('Content length error: expected '.$contentLength.' bytes; received '.$fileLength.' bytes'); - } - return trim($response); - } +/* + * Current module, action, plugin + */ /** - * Fetch the file at $url in the destination $pathDestination - * @param string $url - * @param string $pathDestination - * @param int $tries - * @return true on success, throws Exception on failure + * Returns the name of the Login plugin currently being used. + * Must be used since it is not allowed to hardcode 'Login' in URLs + * in case another Login plugin is being used. + * + * @return string */ - static public function fetchRemoteFile($url, $pathDestination, $tries = 0) + static public function getLoginPluginName() { - return self::sendHttpRequest($url, 10, 'Update', $pathDestination, $tries); + return Zend_Registry::get('auth')->getName(); } /** - * Recursively delete a directory + * Returns the plugin currently being used to display the page * - * @param string $dir Directory name - * @param boolean $deleteRootToo Delete specified top-level directory as well - */ - static public function unlinkRecursive($dir, $deleteRootToo) - { - if(!$dh = @opendir($dir)) - { - return; - } - while (false !== ($obj = readdir($dh))) - { - if($obj == '.' || $obj == '..') - { - continue; - } - - if (!@unlink($dir . '/' . $obj)) - { - self::unlinkRecursive($dir.'/'.$obj, true); - } - } - closedir($dh); - if ($deleteRootToo) - { - @rmdir($dir); - } - return; - } - - /** - * Copy recursively from $source to $target. - * - * @param string $source eg. './tmp/latest' - * @param string $target eg. '.' - * @param bool $excludePhp + * @return Piwik_Plugin */ - static public function copyRecursive($source, $target, $excludePhp=false ) + static public function getCurrentPlugin() { - if ( is_dir( $source ) ) - { - @mkdir( $target ); - $d = dir( $source ); - while ( false !== ( $entry = $d->read() ) ) - { - if ( $entry == '.' || $entry == '..' ) - { - continue; - } - - $sourcePath = $source . '/' . $entry; - if ( is_dir( $sourcePath ) ) - { - self::copyRecursive( $sourcePath, $target . '/' . $entry, $excludePhp ); - continue; - } - $destPath = $target . '/' . $entry; - self::copy($sourcePath, $destPath, $excludePhp); - } - $d->close(); - } - else - { - self::copy($source, $target, $excludePhp); - } + return Piwik_PluginsManager::getInstance()->getLoadedPlugin(Piwik::getModule()); } - + /** - * Copy individual file from $source to $target. - * - * @param string $source eg. './tmp/latest/index.php' - * @param string $target eg. './index.php' - * @param bool $excludePhp - * @return bool + * Returns the current module read from the URL (eg. 'API', 'UserSettings', etc.) + * + * @return string */ - static public function copy($source, $dest, $excludePhp=false) + static public function getModule() { - static $phpExtensions = array('php', 'tpl'); - - if($excludePhp) - { - $path_parts = pathinfo($source); - if(in_array($path_parts['extension'], $phpExtensions)) - { - return true; - } - } - - if(!@copy( $source, $dest )) - { - @chmod($dest, 0755); - if(!@copy( $source, $dest )) - { - throw new Exception(" - Error while copying file to <code>$dest</code>. <br /> - Please check that the web server has enough permission to overwrite this file. <br/> - For example, on a linux server, if your apache user is www-data you can try to execute:<br> - <code>chown -R www-data:www-data ".Piwik_Common::getPathToPiwikRoot()."</code><br> - <code>chmod -R 0755 ".Piwik_Common::getPathToPiwikRoot()."</code><br> - "); - } - } - return true; + return Piwik_Common::getRequestVar('module', '', 'string'); } /** - * Recursively find pathnames that match a pattern - * @see glob() + * Returns the current action read from the URL * - * @param string $sDir directory - * @param string $sPattern pattern - * @param int $nFlags glob() flags - * @return array + * @return string */ - public static function globr($sDir, $sPattern, $nFlags = NULL) + static public function getAction() { - if(($aFiles = glob("$sDir/$sPattern", $nFlags)) == false) - { - $aFiles = array(); - } - if(($aDirs = glob("$sDir/*", GLOB_ONLYDIR)) != false) - { - foreach ($aDirs as $sSubDir) - { - $aSubFiles = self::globr($sSubDir, $sPattern, $nFlags); - $aFiles = array_merge($aFiles, $aSubFiles); - } - } - return $aFiles; + return Piwik_Common::getRequestVar('action', '', 'string'); } /** - * API was simplified in 0.2.27, but we maintain backward compatibility - * when calling Piwik::prefixTable + * Redirect to module (and action) * - * @deprecated as of 0.2.27 + * @param string $newModule + * @param string $newAction + * @return bool false if the URL to redirect to is already this URL */ - static public function prefixTable( $table ) + static public function redirectToModule( $newModule, $newAction = '', $parameters = array() ) { - return Piwik_Common::prefixTable($table); + $newUrl = 'index.php' . Piwik_Url::getCurrentQueryStringWithParametersModified( + array('module' => $newModule, 'action' => $newAction) + + $parameters + ); + Piwik_Url::redirectToUrl($newUrl); } - + +/* + * Global database object + */ + /** - * Names of all the prefixed tables in piwik - * Doesn't use the DB + * Create database object and connect to database */ - static public function getTablesNames() - { - $aTables = array_keys(self::getTablesCreateSql()); - $config = Zend_Registry::get('config'); - $prefixTables = $config->database->tables_prefix; - $return = array(); - foreach($aTables as $table) - { - $return[] = $prefixTables.$table; - } - return $return; - } - - static $tablesInstalled = null; - - static public function getTablesInstalled($forceReload = true, $idSite = null) - { - if(is_null(self::$tablesInstalled) - || $forceReload === true) - { - $db = Zend_Registry::get('db'); - $config = Zend_Registry::get('config'); - $prefixTables = $config->database->tables_prefix; - - $allTables = $db->fetchCol("SHOW TABLES"); - - // all the tables to be installed - $allMyTables = self::getTablesNames(); - - // we get the intersection between all the tables in the DB and the tables to be installed - $tablesInstalled = array_intersect($allMyTables, $allTables); - - // at this point we have only the piwik tables which is good - // but we still miss the piwik generated tables (using the class Piwik_TablePartitioning) - $idSiteInSql = "no"; - if(!is_null($idSite)) - { - $idSiteInSql = $idSite; - } - $allArchiveNumeric = $db->fetchCol("/* SHARDING_ID_SITE = ".$idSiteInSql." */ - SHOW TABLES LIKE '".$prefixTables."archive_numeric%'"); - $allArchiveBlob = $db->fetchCol("/* SHARDING_ID_SITE = ".$idSiteInSql." */ - SHOW TABLES LIKE '".$prefixTables."archive_blob%'"); - - $allTablesReallyInstalled = array_merge($tablesInstalled, $allArchiveNumeric, $allArchiveBlob); - - self::$tablesInstalled = $allTablesReallyInstalled; - } - return self::$tablesInstalled; - } - - static public function createDatabase( $dbName = null ) - { - if(is_null($dbName)) - { - $dbName = Zend_Registry::get('config')->database->dbname; - } - Piwik_Exec("CREATE DATABASE IF NOT EXISTS ".$dbName); - } - - static public function dropDatabase() - { - $dbName = Zend_Registry::get('config')->database->dbname; - Piwik_Exec("DROP DATABASE IF EXISTS " . $dbName); - } - static public function createDatabaseObject( $dbInfos = null ) { $config = Zend_Registry::get('config'); - + if(is_null($dbInfos)) { $dbInfos = $config->database->toArray(); } - + $dbInfos['profiler'] = $config->Debug->enable_sql_profiler; - + $db = null; Piwik_PostEvent('Reporting.createDatabase', $db); if(is_null($db)) { - if($dbInfos['port'][0] == '/') - { - $dbInfos['unix_socket'] = $dbInfos['port']; - unset($dbInfos['host']); - unset($dbInfos['port']); - } - - // not used by Zend Framework - unset($dbInfos['tables_prefix']); - unset($dbInfos['adapter']); - - $db = Piwik_Db::factory($config->database->adapter, $dbInfos); - $db->getConnection(); - - Zend_Db_Table::setDefaultAdapter($db); - $db->resetConfig(); // we don't want this information to appear in the logs + $adapter = $dbInfos['adapter']; + $db = Piwik_Db_Adapter::factory($adapter, $dbInfos); } Zend_Registry::set('db', $db); } - - static public function disconnectDatabase() - { - Zend_Registry::get('db')->closeConnection(); - } - + /** - * Returns the MySQL database server version - * - * @deprecated 0.4.4 + * Disconnect from database */ - static public function getMysqlVersion() + static public function disconnectDatabase() { - return Piwik_FetchOne("SELECT VERSION()"); + Zend_Registry::get('db')->closeConnection(); } /** @@ -1520,23 +1304,30 @@ class Piwik return Zend_Registry::get('db')->isConnectionUTF8(); } +/* + * Global log object + */ + + /** + * Create log object + */ static public function createLogObject() { $configAPI = Zend_Registry::get('config')->log; - + $aLoggers = array( 'logger_api_call' => new Piwik_Log_APICall, 'logger_exception' => new Piwik_Log_Exception, 'logger_error' => new Piwik_Log_Error, 'logger_message' => new Piwik_Log_Message, - ); - + ); + foreach($configAPI as $loggerType => $aRecordTo) { if(isset($aLoggers[$loggerType])) { $logger = $aLoggers[$loggerType]; - + foreach($aRecordTo as $recordTo) { switch($recordTo) @@ -1544,15 +1335,15 @@ class Piwik case 'screen': $logger->addWriteToScreen(); break; - + case 'database': $logger->addWriteToDatabase(); break; - + case 'file': - $logger->addWriteToFile(); + $logger->addWriteToFile(); break; - + default: throw new Exception("'$recordTo' is not a valid Log type. Valid logger types are: screen, database, file."); break; @@ -1560,7 +1351,7 @@ class Piwik } } } - + foreach($aLoggers as $loggerType =>$logger) { if($logger->getWritersCount() == 0) @@ -1570,7 +1361,16 @@ class Piwik Zend_Registry::set($loggerType, $logger); } } - + +/* + * Global config object + */ + + /** + * Create configuration object + * + * @param string $pathConfigFile + */ static public function createConfigObject( $pathConfigFile = null ) { $config = new Piwik_Config($pathConfigFile); @@ -1578,93 +1378,181 @@ class Piwik $config->init(); } +/* + * Global access object + */ + + /** + * Create access object + */ static public function createAccessObject() { Zend_Registry::set('access', new Piwik_Access()); } - - static public function dropTables( $doNotDelete = array() ) - { - $tablesAlreadyInstalled = self::getTablesInstalled(); - $db = Zend_Registry::get('db'); - - $doNotDeletePattern = '/('.implode('|',$doNotDelete).')/'; - - foreach($tablesAlreadyInstalled as $tableName) - { - - if( count($doNotDelete) == 0 - || (!in_array($tableName,$doNotDelete) - && !preg_match($doNotDeletePattern,$tableName) - ) - ) - { - $db->query("DROP TABLE `$tableName`"); - } - } - } - + +/* + * User input validation + */ + /** * Returns true if the email is a valid email - * + * * @param string email * @return bool */ - static public function isValidEmailString( $email ) + static public function isValidEmailString( $email ) { return (preg_match('/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9_.-]+\.[a-zA-Z]{2,4}$/', $email) > 0); } - + /** - * Creates an entry in the User table for the "anonymous" user. + * Returns true if the login is valid. + * Warning: does not check if the login already exists! You must use UsersManager_API->userExists as well. + * + * @param string $login + * @return bool or throws exception */ - static public function createAnonymousUser() + static public function checkValidLoginString( $userLogin ) + { + $loginMinimumLength = 3; + $loginMaximumLength = 100; + $l = strlen($userLogin); + if(!($l >= $loginMinimumLength + && $l <= $loginMaximumLength + && (preg_match('/^[A-Za-z0-9_.-]*$/', $userLogin) > 0)) + ) + { + throw new Exception(Piwik_TranslateException('UsersManager_ExceptionInvalidLoginFormat', array($loginMinimumLength, $loginMaximumLength))); + } + } + +/* + * Date / Timezone + */ + + /** + * Returns true if the current php version supports timezone manipulation + * (most likely if php >= 5.2) + * + * @return bool + */ + static public function isTimezoneSupportEnabled() { - // 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 = Zend_Registry::get('db'); - $db->query("INSERT INTO ". Piwik::prefixTable("user") . " - VALUES ( 'anonymous', '', 'anonymous', 'anonymous@example.org', 'anonymous', CURRENT_TIMESTAMP );" ); + return + function_exists( 'date_create' ) && + function_exists( 'date_default_timezone_set' ) && + function_exists( 'timezone_identifiers_list' ) && + function_exists( 'timezone_open' ) && + function_exists( 'timezone_offset_get' ); } - - static public function createTables() + +/* + * Database and table definition methods + */ + + /** + * Is the schema available? + * + * @return bool True if schema is available; false otherwise + */ + static public function isAvailable() { - $db = Zend_Registry::get('db'); - $config = Zend_Registry::get('config'); - $prefixTables = $config->database->tables_prefix; + return Piwik_Db_Schema::getInstance()->isAvailable(); + } - $tablesAlreadyInstalled = self::getTablesInstalled(); - $tablesToCreate = self::getTablesCreateSql(); - unset($tablesToCreate['archive_blob']); - unset($tablesToCreate['archive_numeric']); + /** + * Get the SQL to create a specific Piwik table + * + * @param string $tableName + * @return string SQL + */ + static public function getTableCreateSql( $tableName ) + { + return Piwik_Db_Schema::getInstance()->getTableCreateSql($tableName); + } - foreach($tablesToCreate as $tableName => $tableSql) - { - $tableName = $prefixTables . $tableName; - if(!in_array($tableName, $tablesAlreadyInstalled)) - { - $db->query( $tableSql ); - } - } + /** + * Get the SQL to create Piwik tables + * + * @return array of strings containing SQL + */ + static public function getTablesCreateSql() + { + return Piwik_Db_Schema::getInstance()->getTablesCreateSql(); + } + + /** + * Create database + * + * @param string $dbName + */ + static public function createDatabase( $dbName = null ) + { + Piwik_Db_Schema::getInstance()->createDatabase($dbName); } - + + /** + * Drop database + */ + static public function dropDatabase() + { + Piwik_Db_Schema::getInstance()->dropDatabase(); + } + + /** + * Create all tables + */ + static public function createTables() + { + Piwik_Db_Schema::getInstance()->createTables(); + } + + /** + * Creates an entry in the User table for the "anonymous" user. + */ + static public function createAnonymousUser() + { + Piwik_Db_Schema::getInstance()->createAnonymousUser(); + } + + /** + * Truncate all tables + */ static public function truncateAllTables() { - $tablesAlreadyInstalled = self::getTablesInstalled($forceReload = true); - foreach($tablesAlreadyInstalled as $table) - { - Piwik_Query("TRUNCATE `$table`"); - } + Piwik_Db_Schema::getInstance()->truncateAllTables(); } - - static public function install() + + /** + * Drop specific tables + * + * @param array $doNotDelete Names of tables to not delete + */ + static public function dropTables( $doNotDelete = array() ) { - Piwik_Common::mkdir(Zend_Registry::get('config')->smarty->compile_dir); + Piwik_Db_Schema::getInstance()->dropTables($doNotDelete); } - - static public function uninstall() + + /** + * Names of all the prefixed tables in piwik + * Doesn't use the DB + * + * @return array Table names + */ + static public function getTablesNames() + { + return Piwik_Db_Schema::getInstance()->getTablesNames(); + } + + /** + * Get list of tables installed + * + * @param bool $forceReload Invalidate cache + * @param string $idSite + * @return array Tables installed + */ + static public function getTablesInstalled($forceReload = true, $idSite = null) { - $db = Zend_Registry::get('db'); - $db->query( "DROP TABLE IF EXISTS ". implode(", ", self::getTablesNames()) ); + return Piwik_Db_Schema::getInstance()->getTablesInstalled($forceReload, $idSite); } } diff --git a/core/Plugin.php b/core/Plugin.php index d0d118180a..82ac6c523f 100644 --- a/core/Plugin.php +++ b/core/Plugin.php @@ -20,14 +20,14 @@ abstract class Piwik_Plugin { /** * Returns the plugin details - * 'name' => string // plugin name - * 'description' => string // 1/2 sentences description of the plugin - * 'author' => string // plugin author - * 'author_homepage' => string // author homepage (or email "mailto:youremail@example.org") - * 'homepage' => string // plugin homepage - * 'version' => string // plugin version number + * 'description' => string // 1-2 sentence description of the plugin + * 'author' => string // plugin author + * 'author_homepage' => string // author homepage URL (or email "mailto:youremail@example.org") + * 'homepage' => string // plugin homepage URL + * 'version' => string // plugin version number; examples and 3rd party plugins must not use Piwik_Version::VERSION; + * // 3rd party plugins must increment the version number with each plugin release * 'translationAvailable' => bool // is there a translation file in plugins/your-plugin/lang/* ? - * 'TrackerPlugin' => bool // should we load this plugin during the stats logging process? + * 'TrackerPlugin' => bool // should we load this plugin during the stats logging process? */ abstract function getInformation(); @@ -69,17 +69,6 @@ abstract class Piwik_Plugin } /** - * Returns the plugin name - * - * @return string - */ - public function getName() - { - $info = $this->getInformation(); - return $info['name']; - } - - /** * Returns the plugin version number * * @return string @@ -91,12 +80,13 @@ abstract class Piwik_Plugin } /** - * Returns the UserCountry part when the plugin class is Piwik_UserCountry + * Returns the plugin's base name without the "Piwik_" prefix, + * e.g., "UserCountry" when the plugin class is "Piwik_UserCountry" * * @return string */ - public function getClassName() + final public function getClassName() { - return substr(get_class($this), strlen("Piwik_")); + return Piwik::unprefixClass(get_class($this)); } } diff --git a/core/PluginsFunctions/AdminMenu.php b/core/PluginsFunctions/AdminMenu.php index e926c77d7f..44142e9841 100644 --- a/core/PluginsFunctions/AdminMenu.php +++ b/core/PluginsFunctions/AdminMenu.php @@ -16,6 +16,7 @@ class Piwik_AdminMenu { private $adminMenu = null; + private $adminMenuOrdered = null; static private $instance = null; /** @@ -36,31 +37,37 @@ class Piwik_AdminMenu */ public function get() { - if(!is_null($this->adminMenu)) + if(!is_null($this->adminMenuOrdered)) { - return; + return $this->adminMenuOrdered; } Piwik_PostEvent('AdminMenu.add'); - - foreach($this->adminMenu as $key => &$element) + + $this->adminMenuOrdered = array(); + ksort($this->adminMenu); + foreach($this->adminMenu as $order => $menu) { - if(is_null($element)) - { - unset($this->adminMenu[$key]); - } + foreach($menu as $key => &$element) + { + if(!is_null($element)) + { + $this->adminMenuOrdered[$key] = $element; + } + } } - return $this->adminMenu; + return $this->adminMenuOrdered; } /* * */ - public function add($adminMenuName, $url) + public function add($adminMenuName, $url, $displayedForCurrentUser, $order) { - if(!isset($this->adminMenu[$adminMenuName])) + if($displayedForCurrentUser + && !isset($this->adminMenu[$adminMenuName])) { - $this->adminMenu[$adminMenuName] = $url; + $this->adminMenu[$order][$adminMenuName] = $url; } } @@ -74,15 +81,30 @@ class Piwik_AdminMenu $this->adminMenu[$adminMenuRenamed] = $save; } } +function Piwik_GetCurrentAdminMenuName() +{ + $menu = Piwik_GetAdminMenu(); + $currentModule = Piwik::getModule(); + $currentAction = Piwik::getAction(); + foreach($menu as $name => $parameters) + { + if($parameters['module'] == $currentModule + && $parameters['action'] == $currentAction) + { + return $name; + } + } + return false; +} function Piwik_GetAdminMenu() { return Piwik_AdminMenu::getInstance()->get(); } -function Piwik_AddAdminMenu( $adminMenuName, $url ) +function Piwik_AddAdminMenu( $adminMenuName, $url, $displayedForCurrentUser = true, $order = 10 ) { - return Piwik_AdminMenu::getInstance()->add($adminMenuName, $url); + return Piwik_AdminMenu::getInstance()->add($adminMenuName, $url, $displayedForCurrentUser, $order); } function Piwik_RenameAdminMenuEntry($adminMenuOriginal, $adminMenuRenamed) diff --git a/core/PluginsFunctions/Menu.php b/core/PluginsFunctions/Menu.php index d2b33675c6..73041c1d6d 100644 --- a/core/PluginsFunctions/Menu.php +++ b/core/PluginsFunctions/Menu.php @@ -60,6 +60,21 @@ class Piwik_Menu } } + function isUrlFound($url) + { + $menu = Piwik_Menu::getInstance()->get(); + foreach($menu as $mainMenuName => $subMenus) + { + foreach($subMenus as $subMenuName => $menuUrl) + { + if($menuUrl == $url) + { + return true; + } + } + } + return false; + } /* * */ @@ -163,6 +178,11 @@ class Piwik_Menu } } +function Piwik_IsMenuUrlFound($url) +{ + return Piwik_Menu::getInstance()->isUrlFound($url); +} + function Piwik_GetMenu() { return Piwik_Menu::getInstance()->get(); diff --git a/core/PluginsFunctions/Sql.php b/core/PluginsFunctions/Sql.php index 5cd5ff6784..aa219d619b 100644 --- a/core/PluginsFunctions/Sql.php +++ b/core/PluginsFunctions/Sql.php @@ -11,15 +11,64 @@ */ /** + * SQL wrapper + * * @package PluginsFunctions */ class Piwik_Sql { + static private function getDb() + { + $db = null; + if(!empty($GLOBALS['PIWIK_TRACKER_MODE'])) + { + $db = Piwik_Tracker::getDatabase(); + } + if($db === null) + { + $db = Zend_Registry::get('db'); + } + return $db; + } + + static public function exec($sql) + { + return self::getDb()->exec($sql); + } + + static public function query($sql, $parameters = array()) + { + return self::getDb()->query($sql, $parameters); + } + + static public function fetchAll($sql, $parameters = array()) + { + return self::getDb()->fetchAll($sql, $parameters); + } + + static public function fetchRow($sql, $parameters = array()) + { + return self::getDb()->fetchRow($sql, $parameters); + } + + static public function fetchOne($sql, $parameters = array()) + { + return self::getDb()->fetchOne($sql, $parameters); + } } -function Piwik_Exec( $sqlQuery ) +/** + * Executes an unprepared SQL query on the DB. Recommended for DDL statements, e.g., CREATE/DROP/ALTER. + * The return result is DBMS-specific. For MySQLI, it returns the number of rows affected. For PDO, it returns the Zend_Db_Statement object + * If you want to fetch data from the DB you should use the function Piwik_FetchAll() + * + * @param string $sqlQuery + * @param array Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return integer|Zend_Db_Statement + */ +function Piwik_Exec($sqlQuery) { - return Zend_Registry::get('db')->exec( $sqlQuery ); + return Piwik_Sql::exec($sqlQuery); } /** @@ -32,25 +81,43 @@ function Piwik_Exec( $sqlQuery ) * @param array Parameters to bind in the query, array( param1 => value1, param2 => value2) * @return Zend_Db_Statement */ -function Piwik_Query( $sqlQuery, $parameters = array()) +function Piwik_Query($sqlQuery, $parameters = array()) { - return Zend_Registry::get('db')->query( $sqlQuery, $parameters); + return Piwik_Sql::query($sqlQuery, $parameters); } /** - * Executes the SQL Query and fetches all the rows from the database + * Executes the SQL Query and fetches all the rows from the database query * * @param string $sqlQuery - * @param array Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) * @return array (one row in the array per row fetched in the DB) */ function Piwik_FetchAll( $sqlQuery, $parameters = array()) { - return Zend_Registry::get('db')->fetchAll( $sqlQuery, $parameters ); + return Piwik_Sql::fetchAll($sqlQuery, $parameters); } -function Piwik_FetchOne( $sqlQuery, $parameters = array()) +/** + * Fetches first row of result from the database query + * + * @param string $sqlQuery + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return array + */ +function Piwik_FetchRow($sqlQuery, $parameters = array()) { - return Zend_Registry::get('db')->fetchOne( $sqlQuery, $parameters ); + return Piwik_Sql::fetchRow($sqlQuery, $parameters); } +/** + * Fetches first column of first row of result from the database query + * + * @param string $sqlQuery + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return string + */ +function Piwik_FetchOne( $sqlQuery, $parameters = array()) +{ + return Piwik_Sql::fetchOne($sqlQuery, $parameters); +} diff --git a/core/PluginsFunctions/WidgetsList.php b/core/PluginsFunctions/WidgetsList.php index ce1b7a417f..1ab172a645 100644 --- a/core/PluginsFunctions/WidgetsList.php +++ b/core/PluginsFunctions/WidgetsList.php @@ -16,10 +16,15 @@ class Piwik_WidgetsList { static protected $widgets = null; + static protected $hookCalled = false; static function get() { - Piwik_PostEvent('WidgetsList.add'); + if(!self::$hookCalled) + { + self::$hookCalled = true; + Piwik_PostEvent('WidgetsList.add'); + } return self::$widgets; } @@ -28,6 +33,10 @@ class Piwik_WidgetsList $widgetCategory = Piwik_Translate($widgetCategory); $widgetName = Piwik_Translate($widgetName); $widgetUniqueId = 'widget' . $controllerName . $controllerAction; + foreach($customParameters as $name => $value) + { + $widgetUniqueId .= $name . $value; + } self::$widgets[$widgetCategory][] = array( 'name' => $widgetName, 'uniqueId' => $widgetUniqueId, @@ -36,6 +45,23 @@ class Piwik_WidgetsList ) + $customParameters ); } + + static function isDefined($controllerName, $controllerAction) + { + $widgetsList = self::get(); + foreach($widgetsList as $widgetCategory => $widgets) + { + foreach($widgets as $widget) + { + if($widget['parameters']['module'] == $controllerName + && $widget['parameters']['action'] == $controllerAction) + { + return true; + } + } + } + return false; + } } function Piwik_GetWidgetsList() @@ -47,3 +73,8 @@ function Piwik_AddWidget( $widgetCategory, $widgetName, $controllerName, $contro { Piwik_WidgetsList::add($widgetCategory, $widgetName, $controllerName, $controllerAction, $customParameters); } + +function Piwik_IsWidgetDefined($controllerName, $controllerAction) +{ + return Piwik_WidgetsList::isDefined($controllerName, $controllerAction); +} diff --git a/core/PluginsManager.php b/core/PluginsManager.php index 5e2c3e20d7..d31289ac23 100644 --- a/core/PluginsManager.php +++ b/core/PluginsManager.php @@ -10,9 +10,6 @@ * @package Piwik */ -// no direct access -defined('PIWIK_INCLUDE_PATH') or die; - /** * @see core/PluginsFunctions/Menu.php * @see core/PluginsFunctions/AdminMenu.php @@ -42,7 +39,13 @@ class Piwik_PluginsManager protected $loadedPlugins = array(); protected $doLoadAlwaysActivatedPlugins = true; - protected $pluginToAlwaysActivate = array( 'CoreHome', 'CoreUpdater', 'CoreAdminHome', 'CorePluginsAdmin' ); + protected $pluginToAlwaysActivate = array( 'CoreHome', + 'CoreUpdater', + 'CoreAdminHome', + 'CorePluginsAdmin', + 'Installation', + 'SitesManager', + 'UsersManager' ); static private $instance = null; @@ -112,7 +115,7 @@ class Piwik_PluginsManager if($key !== false) { unset($pluginsTracker[$key]); - Zend_Registry::get('config')->Plugins_Tracker = $pluginsTracker; + Zend_Registry::get('config')->Plugins_Tracker = array('Plugins_Tracker' => $pluginsTracker); } } } @@ -154,7 +157,7 @@ class Piwik_PluginsManager Zend_Registry::get('config')->Plugins = $plugins; } - public function setPluginsToLoad( array $pluginsToLoad ) + public function loadPlugins( array $pluginsToLoad ) { // case no plugins to load if(is_null($pluginsToLoad)) @@ -162,7 +165,7 @@ class Piwik_PluginsManager $pluginsToLoad = array(); } $this->pluginsToLoad = $pluginsToLoad; - $this->loadPlugins(); + $this->reloadPlugins(); } public function doNotLoadPlugins() @@ -174,13 +177,22 @@ class Piwik_PluginsManager { $this->doLoadAlwaysActivatedPlugins = false; } - - public function postLoadPlugins() + + public function loadTranslations() { $plugins = $this->getLoadedPlugins(); + foreach($plugins as $plugin) { $this->loadTranslation( $plugin, $this->languageToLoad ); + } + } + + public function postLoadPlugins() + { + $plugins = $this->getLoadedPlugins(); + foreach($plugins as $plugin) + { $plugin->postLoad(); } } @@ -192,9 +204,7 @@ class Piwik_PluginsManager */ public function getLoadedPluginsName() { - $oPlugins = $this->getLoadedPlugins(); - $pluginNames = array_map('get_class',$oPlugins); - return $pluginNames; + return array_map('get_class', $this->getLoadedPlugins()); } /** @@ -230,7 +240,7 @@ class Piwik_PluginsManager * Register the observers for every plugin. * */ - public function loadPlugins() + private function reloadPlugins() { $this->pluginsToLoad = array_unique($this->pluginsToLoad); @@ -248,17 +258,17 @@ class Piwik_PluginsManager && $this->isPluginActivated($pluginName)) { $this->addPluginObservers( $newPlugin ); - $this->addLoadedPlugin( $pluginName, $newPlugin); } } } } /** - * Loads the plugin filename and instanciates the plugin with the given name, eg. UserCountry + * Loads the plugin filename and instantiates the plugin with the given name, eg. UserCountry * Do NOT give the class name ie. Piwik_UserCountry, but give the plugin name ie. UserCountry * - * @param Piwik_Plugin $pluginName + * @param string $pluginName + * @return Piwik_Plugin */ public function loadPlugin( $pluginName ) { @@ -278,7 +288,8 @@ class Piwik_PluginsManager if(!file_exists($path)) { - throw new Exception("Unable to load plugin '$pluginName' because '$path' couldn't be found."); + throw new Exception("Unable to load plugin '$pluginName' because '$path' couldn't be found. + You can manually uninstall the plugin by removing the line <code>Plugins[] = $pluginName</code> from the Piwik config file."); } // Don't remove this. @@ -295,6 +306,9 @@ class Piwik_PluginsManager { throw new Exception("The plugin $pluginClassName in the file $path must inherit from Piwik_Plugin."); } + + $this->addLoadedPlugin( $pluginName, $newPlugin); + return $newPlugin; } @@ -347,7 +361,7 @@ class Piwik_PluginsManager try{ $plugin->install(); } catch(Exception $e) { - throw new Piwik_PluginsManager_PluginException($plugin->getName(), $plugin->getClassName(), $e->getMessage()); } + throw new Piwik_PluginsManager_PluginException($plugin->getClassName(), $e->getMessage()); } } @@ -381,12 +395,12 @@ class Piwik_PluginsManager */ private function loadTranslation( $plugin, $langCode ) { - // we are certainly in Tracker mode, Zend is not loaded - if(!class_exists('Zend_Loader', false)) + // we are in Tracker mode if Piwik_Loader is not (yet) loaded + if(!class_exists('Piwik_Loader', false)) { return ; } - + $infos = $plugin->getInformation(); if(!isset($infos['translationAvailable'])) { @@ -398,7 +412,7 @@ class Piwik_PluginsManager { return; } - + $pluginName = $plugin->getClassName(); $path = PIWIK_INCLUDE_PATH . '/plugins/' . $pluginName .'/lang/%s.php'; @@ -436,13 +450,6 @@ class Piwik_PluginsManager return $pluginNames; } - public function getInstalledPlugins() - { - $plugins = $this->getLoadedPlugins(); - $installed = $this->getInstalledPluginsName(); - return array_intersect_key($plugins, array_combine($installed, array_fill(0, count($installed), 1))); - } - private function installPluginIfNecessary( Piwik_Plugin $plugin ) { $pluginName = $plugin->getClassName(); @@ -486,18 +493,24 @@ class Piwik_PluginsManager */ class Piwik_PluginsManager_PluginException extends Exception { - function __construct($pluginName, $className, $message) + function __construct($pluginName, $message) { parent::__construct("There was a problem installing the plugin ". $pluginName . ": " . $message. " If this plugin has already been installed, and if you want to hide this message</b>, you must add the following line under the [PluginsInstalled] entry in your config/config.ini.php file: - PluginsInstalled[] = $className" ); + PluginsInstalled[] = $pluginName" ); } } /** * Post an event to the dispatcher which will notice the observers + * + * @param $eventName The event name + * @param $object Object, array or string that the listeners can read and/or modify. + * Listeners can call $object =& $notification->getNotificationObject(); to fetch and then modify this variable. + * @param $info Additional array of data that can be used by the listeners, but not edited + * @return void */ function Piwik_PostEvent( $eventName, &$object = null, $info = array() ) { @@ -530,11 +543,11 @@ class Piwik_Event_Notification extends Event_Notification $className = is_object($callback[0]) ? get_class($callback[0]) : $callback[0]; $method = $callback[1]; - echo "after $className -> $method <br>"; + echo "after $className -> $method <br />"; echo "-"; Piwik::printTimer(); - echo "<br>"; + echo "<br />"; echo "-"; Piwik::printMemoryLeak(); - echo "<br>"; + echo "<br />"; } } } diff --git a/core/Session.php b/core/Session.php new file mode 100644 index 0000000000..49969f2066 --- /dev/null +++ b/core/Session.php @@ -0,0 +1,72 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Piwik + */ + +/** + * Session initialization. + * + * @package Piwik + */ +class Piwik_Session extends Zend_Session +{ + public static function start($options = false) + { + // don't use the default: PHPSESSID + $sessionName = defined('PIWIK_SESSION_NAME') ? PIWIK_SESSION_NAME : 'PIWIK_SESSID'; + @ini_set('session.name', $sessionName); + + // we consider this a misconfiguration (i.e., Piwik doesn't implement user-defined session handler functions) + if(ini_get('session.save_handler') == 'user') + { + @ini_set('session.save_handler', 'files'); + @ini_set('session.save_path', ''); + } + + // for "files", we want a writeable folder; + // for shared hosting, we assume the web server has been securely configured to prevent local session file hijacking + if(ini_get('session.save_handler') == 'files') + { + $sessionPath = ini_get('session.save_path'); + if(preg_match('/^[0-9]+;(.*)/', $sessionPath, $matches)) + { + $sessionPath = $matches[1]; + } + if(ini_get('safe_mode') || ini_get('open_basedir') || empty($sessionPath) || !@is_readable($sessionPath) || !@is_writable($sessionPath)) + { + $sessionPath = PIWIK_USER_PATH . '/tmp/sessions'; + $ok = true; + + if(!is_dir($sessionPath)) + { + @mkdir($sessionPath, 0755, true); + if(!is_dir($sessionPath)) + { + // Unable to mkdir $sessionPath + $ok = false; + } + } + else if(!@is_writable($sessionPath)) + { + // $sessionPath is not writable + $ok = false; + } + + if($ok) + { + @ini_set('session.save_path', $sessionPath); + } + // else rely on default setting (assuming it is configured to a writeable folder) + } + } + + Zend_Session::start(); + } +} diff --git a/core/Site.php b/core/Site.php index d692c470ae..d5915e30b3 100644 --- a/core/Site.php +++ b/core/Site.php @@ -25,13 +25,19 @@ class Piwik_Site $this->id = $idsite; if(!isset(self::$infoSites[$this->id])) { - self::$infoSites[$this->id] = Piwik_SitesManager_API::getSiteFromId($idsite); + self::$infoSites[$this->id] = Piwik_SitesManager_API::getInstance()->getSiteFromId($idsite); } } function __toString() { - return "site id=".$this->getId().", name=".$this->getName(); + return "site id=".$this->getId().", + name=".$this->getName() .", + url = ". $this->getMainUrl() .", + IPs excluded = ".$this->getExcludedIps().", + timezone = ".$this->getTimezone().", + currency = ".$this->getCurrency().", + creation date = ".$this->getCreationDate(); } function getName() @@ -54,6 +60,26 @@ class Piwik_Site $date = self::$infoSites[$this->id]['ts_created']; return Piwik_Date::factory($date); } + + function getTimezone() + { + return self::$infoSites[$this->id]['timezone']; + } + + function getCurrency() + { + return self::$infoSites[$this->id]['currency']; + } + + function getExcludedIps() + { + return self::$infoSites[$this->id]['excluded_ips']; + } + + function getExcludedQueryParameters() + { + return self::$infoSites[$this->id]['excluded_parameters']; + } /** * @param string comma separated idSite list @@ -70,4 +96,9 @@ class Piwik_Site } return $validIds; } + + static public function clearCache() + { + self::$infoSites = array(); + } } diff --git a/core/Smarty.php b/core/Smarty.php index a5b8165583..cc5c000bbf 100644 --- a/core/Smarty.php +++ b/core/Smarty.php @@ -10,9 +10,6 @@ * @package Piwik */ -// no direct access -defined('PIWIK_INCLUDE_PATH') or die; - /** * @see libs/Smarty/Smarty.class.php * @link http://smarty.net diff --git a/core/SmartyPlugins/function.ajaxErrorDiv.php b/core/SmartyPlugins/function.ajaxErrorDiv.php new file mode 100644 index 0000000000..b143fae09e --- /dev/null +++ b/core/SmartyPlugins/function.ajaxErrorDiv.php @@ -0,0 +1,30 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package SmartyPlugins + */ + +/** + * Outputs the generic Ajax error div (displayed when ajax requests are throwing exceptions and returning error messages) + * + * @param id=$ID_NAME ID of the HTML div, defaults to ajaxError + * @return string Html of the error message div, hidden by defayult + */ +function smarty_function_ajaxErrorDiv($params, &$smarty) +{ + if(empty($params['id'])) + { + $id = 'ajaxError'; + } + else + { + $id = $params['id']; + } + return '<div class="ajaxError" id="'.$id.'" style="display:none"></div>'; +} diff --git a/core/SmartyPlugins/function.ajaxLoadingDiv.php b/core/SmartyPlugins/function.ajaxLoadingDiv.php new file mode 100644 index 0000000000..86b624f7f6 --- /dev/null +++ b/core/SmartyPlugins/function.ajaxLoadingDiv.php @@ -0,0 +1,35 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package SmartyPlugins + */ + +/** + * Outputs the generic Ajax Loading div (displayed when ajax requests are triggered) + * + * @param id=$ID_NAME ID of the HTML div, defaults to ajaxLoading + * @return string Html of the Loading... div + */ +function smarty_function_ajaxLoadingDiv($params, &$smarty) +{ + if(empty($params['id'])) + { + $id = 'ajaxLoading'; + } + else + { + $id = $params['id']; + } + return '<div id="'.$id.'" style="display:none">'. + '<div id="loadingPiwik"><img src="themes/default/images/loading-blue.gif" alt="" /> '. + Piwik_Translate('General_LoadingData') . + ' </div>'. + '</div>'; + ; +} diff --git a/core/SmartyPlugins/function.ajaxRequestErrorDiv.php b/core/SmartyPlugins/function.ajaxRequestErrorDiv.php new file mode 100644 index 0000000000..d0878ae00c --- /dev/null +++ b/core/SmartyPlugins/function.ajaxRequestErrorDiv.php @@ -0,0 +1,22 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package SmartyPlugins + */ + +/** + * Outputs the generic Ajax request error div + * will be displayed when the ajax request fails (connectivity, server error, etc) + * + * @return string Html of the div + */ +function smarty_function_ajaxRequestErrorDiv() +{ + return '<div id="loadingError">'.Piwik_Translate('General_ErrorRequest').'</div>'; +} diff --git a/core/SmartyPlugins/function.assignTopBar.php b/core/SmartyPlugins/function.assignTopBar.php index 4d5beb44fc..e5313fa179 100644 --- a/core/SmartyPlugins/function.assignTopBar.php +++ b/core/SmartyPlugins/function.assignTopBar.php @@ -21,10 +21,11 @@ function smarty_function_assignTopBar($params, &$smarty) { $topBarElements = array(); $elements = array( - array('CoreHome', Piwik_Translate('General_YourDashboard'), array('module' => 'CoreHome', 'action' => 'index')), + array('CoreHome', Piwik_Translate('General_Dashboard'), array('module' => 'CoreHome', 'action' => 'index')), + array('MultiSites', Piwik_Translate('General_MultiSitesSummary'), array('module' => 'MultiSites', 'action' => 'index')), array('Widgetize', Piwik_Translate('General_Widgets'), array('module' => 'Widgetize', 'action' => 'index')), array('API', Piwik_Translate('General_API'), array('module' => 'API', 'action' => 'listAllAPI')), - array('Feedback', Piwik_Translate('General_GiveUsYourFeedback'), array('module' => 'Feedback', 'action' => 'index', 'keepThis' => 'true', 'TB_iframe' => 'true', 'height' => '400', 'width' => '350'), 'title="'.Piwik_Translate('General_GiveUsYourFeedback').'" class="thickbox"'), + array('Feedback', Piwik_Translate('General_GiveUsYourFeedback'), array('module' => 'Feedback', 'action' => 'index'), 'id="topbar-feedback"'), ); foreach($elements as $element) diff --git a/core/SmartyPlugins/modifier.inlineHelp.php b/core/SmartyPlugins/modifier.inlineHelp.php new file mode 100644 index 0000000000..bbd9892e61 --- /dev/null +++ b/core/SmartyPlugins/modifier.inlineHelp.php @@ -0,0 +1,26 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package SmartyPlugins + */ + +/** + * Displays inline help using the jquery UI CSS + */ +function smarty_modifier_inlineHelp($text) +{ + return + '<div class="ui-widget">'. + '<div class="ui-inline-help ui-state-highlight ui-corner-all">'. + '<p style="font-size:8pt;"><span class="ui-icon ui-icon-info" style="float:left;margin-right:.3em;"></span>'. + $text. + '</p>'. + '</div>'. + '</div>'; +} diff --git a/core/SmartyPlugins/modifier.money.php b/core/SmartyPlugins/modifier.money.php new file mode 100644 index 0000000000..52acffd4fd --- /dev/null +++ b/core/SmartyPlugins/modifier.money.php @@ -0,0 +1,27 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package SmartyPlugins + */ + +/** + * Prints money, given the currency symbol. + * + * @return string The amount with the currency symbol + */ +function smarty_modifier_money($amount) +{ + if(func_num_args() != 2) + { + throw new Exception('the smarty modifier money expects one parameter: the idSite.'); + } + $idSite = func_get_args(); + $idSite = $idSite[1]; + return Piwik::getPrettyMoney($amount, $idSite); +} diff --git a/core/SmartyPlugins/modifier.stripeol.php b/core/SmartyPlugins/modifier.stripeol.php new file mode 100644 index 0000000000..d9e70f810d --- /dev/null +++ b/core/SmartyPlugins/modifier.stripeol.php @@ -0,0 +1,32 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package SmartyPlugins + */ + +/** + * Smarty stripeol modifier plugin + * + * Type: modifier<br> + * Name: stripeol<br> + * Purpose: Replace all end-of-line characters with platform specific string.<br> + * Example: {$var|stripeol} + * Date: March 10th, 2010 + * @author anthon (at) piwik.org + * @version 1.0 + * @param string + * @param string + * @return string + */ +function smarty_modifier_stripeol($text) +{ + return preg_replace('!(\r\n|\r|\n)!', PHP_EOL, $text); +} + +/* vim: set expandtab: */ diff --git a/core/SmartyPlugins/modifier.translate.php b/core/SmartyPlugins/modifier.translate.php index 3c3a7346cc..09a2ecdf89 100644 --- a/core/SmartyPlugins/modifier.translate.php +++ b/core/SmartyPlugins/modifier.translate.php @@ -11,13 +11,19 @@ */ /** - * Read the translation string from the given index (read form the selected language in Piwik). - * The translations strings are located either in /lang/xx.php or within the plugin lang directory. + * Translates in the currently selected language the specified translation $stringToken + * Translations strings are located either in /lang/xx.php or within the plugin lang directory. * - * Example: + * Usage: * {'General_Unknown'|translate} will be translated as 'Unknown' (see the entry in /lang/en.php) * - * @return string The translated string + * Usage with multiple substrings to be replaced in the translation string: + * - in lang/en.php you would find: + * 'VisitorInterest_BetweenXYMinutes' => '%1s-%2s min', + * - in the smarty template you would then translate the string, passing the two parameters: + * {'VisitorInterest_BetweenXYMinutes'|translate:$min:$max} + * + * @return string The translated string, with optional substrings parameters replaced */ function smarty_modifier_translate($stringToken) { diff --git a/core/SmartyPlugins/modifier.urlRewriteWithParameters.php b/core/SmartyPlugins/modifier.urlRewriteWithParameters.php index e2077b493c..7f42333820 100644 --- a/core/SmartyPlugins/modifier.urlRewriteWithParameters.php +++ b/core/SmartyPlugins/modifier.urlRewriteWithParameters.php @@ -18,6 +18,7 @@ */ function smarty_modifier_urlRewriteWithParameters($parameters) { + $parameters['updated'] = null; $url = Piwik_Url::getCurrentQueryStringWithParametersModified($parameters); return htmlspecialchars($url); } diff --git a/core/SmartyPlugins/outputfilter.ajaxcdn.php b/core/SmartyPlugins/outputfilter.ajaxcdn.php index 7c2de1e65e..9d010f7475 100644 --- a/core/SmartyPlugins/outputfilter.ajaxcdn.php +++ b/core/SmartyPlugins/outputfilter.ajaxcdn.php @@ -39,14 +39,18 @@ function smarty_outputfilter_ajaxcdn($source, &$smarty) $swfobject_version = Zend_Registry::get('config')->General->swfobject_version; $pattern = array( + '~<link rel="stylesheet" type="text/css" href="libs/jquery/themes/([^"]*)" class="ui-theme" />~', '~<script type="text/javascript" src="libs/jquery/jquery\.js([^"]*)">~', '~<script type="text/javascript" src="libs/jquery/jquery-ui\.js([^"]*)">~', + '~<script type="text/javascript" src="libs/jquery/jquery-ui-18n\.js([^"]*)">~', '~<script type="text/javascript" src="libs/swfobject/swfobject\.js([^"]*)">~', ); $replace = array( + '<link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/jqueryui/'.$jqueryui_version.'/themes/\\1" class="ui-theme" />', '<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/'.$jquery_version.'/jquery.min.js">', '<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/'.$jqueryui_version.'/jquery-ui.min.js">', + '<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/'.$jqueryui_version.'/i18n/jquery-ui-18n.min.js">', '<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/swfobject/'.$swfobject_version.'/swfobject.js">', ); diff --git a/core/TablePartitioning.php b/core/TablePartitioning.php index 1edcbb4ef4..d74eefff3b 100644 --- a/core/TablePartitioning.php +++ b/core/TablePartitioning.php @@ -108,11 +108,7 @@ class Piwik_TablePartitioning_Monthly extends Piwik_TablePartitioning protected function generateTableName() { $config = Zend_Registry::get('config'); - $prefixTables = $config->database->tables_prefix; - - $date = date("Y_m", $this->timestamp); - - return $prefixTables . $this->tableName . "_" . $date; + return $config->database->tables_prefix . $this->tableName . "_" . date("Y_m", $this->timestamp); } } @@ -131,10 +127,6 @@ class Piwik_TablePartitioning_Daily extends Piwik_TablePartitioning protected function generateTableName() { $config = Zend_Registry::get('config'); - $prefixTables = $config->database->tables_prefix; - - $date = date("Y_m_d", $this->timestamp); - - return $prefixTables . $this->tableName . "_" . $date; + return $config->database->tables_prefix . $this->tableName . "_" . date("Y_m_d", $this->timestamp); } } diff --git a/core/Tracker.php b/core/Tracker.php index e49ecc06c2..d2284c4528 100644 --- a/core/Tracker.php +++ b/core/Tracker.php @@ -35,6 +35,7 @@ class Piwik_Tracker const STATE_LOGGING_DISABLE = 10; const STATE_EMPTY_REQUEST = 11; const STATE_TRACK_ONLY = 12; + const STATE_NOSCRIPT_REQUEST = 13; const COOKIE_INDEX_IDVISITOR = 1; const COOKIE_INDEX_TIMESTAMP_LAST_ACTION = 2; @@ -110,7 +111,7 @@ class Piwik_Tracker case self::STATE_EMPTY_REQUEST: printDebug("Empty request => Piwik page"); - echo "<a href='index.php'>Piwik</a> is a free open source <a href='http://piwik.org'>web analytics</a> alternative to Google analytics."; + echo "<a href='/'>Piwik</a> is a free open source <a href='http://piwik.org'>web analytics</a> alternative to Google analytics."; break; case self::STATE_TO_REDIRECT_URL: @@ -121,6 +122,7 @@ class Piwik_Tracker printDebug("Data push, tracking only"); break; + case self::STATE_NOSCRIPT_REQUEST: case self::STATE_NOTHING_TO_NOTICE: default: printDebug("Nothing to notice => default behaviour"); @@ -131,8 +133,10 @@ class Piwik_Tracker if($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) { - self::$db->recordProfiling(); - Piwik::printSqlProfilingReportTracker(self::$db); + if(isset(self::$db)) { + self::$db->recordProfiling(); + Piwik::printSqlProfilingReportTracker(self::$db); + } } self::disconnectDatabase(); @@ -237,7 +241,7 @@ class Piwik_Tracker if( !isset($GLOBALS['PIWIK_TRACKER_DEBUG']) || !$GLOBALS['PIWIK_TRACKER_DEBUG'] ) { $trans_gif_64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="; - header("Content-type: image/gif"); + header("Content-Type: image/gif"); print(base64_decode($trans_gif_64)); } } @@ -281,7 +285,7 @@ class Piwik_Tracker && count($pluginsTracker) != 0) { Piwik_PluginsManager::getInstance()->doNotLoadAlwaysActivatedPlugins(); - Piwik_PluginsManager::getInstance()->setPluginsToLoad( $pluginsTracker['Plugins_Tracker'] ); + Piwik_PluginsManager::getInstance()->loadPlugins( $pluginsTracker['Plugins_Tracker'] ); printDebug("Loading plugins: { ". implode(",", $pluginsTracker['Plugins_Tracker']) . "}"); } @@ -328,10 +332,15 @@ class Piwik_Tracker protected function handleEmptyRequest() { - if( count($this->request) == 0) + $countParameters = count($this->request); + if($countParameters == 0) { $this->setState(self::STATE_EMPTY_REQUEST); } + if($countParameters == 1 ) + { + $this->setState(self::STATE_NOSCRIPT_REQUEST); + } } protected function handleDisabledTracker() @@ -344,19 +353,22 @@ class Piwik_Tracker } } -function printDebug( $info = '' ) +if(!function_exists('printDebug')) { - if(isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) - { - if(is_array($info)) - { - print("<pre>"); - print(var_export($info,true)); - print("</pre>"); - } - else - { - print($info . "<br>\n"); - } - } + function printDebug( $info = '' ) + { + if(isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) + { + if(is_array($info)) + { + print("<pre>"); + print(var_export($info,true)); + print("</pre>"); + } + else + { + print($info . "<br />\n"); + } + } + } } diff --git a/core/Tracker/Action.php b/core/Tracker/Action.php index f7a9445058..e2641f35f1 100644 --- a/core/Tracker/Action.php +++ b/core/Tracker/Action.php @@ -65,6 +65,8 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface private $actionType; private $actionUrl; + static private $queryParametersToExclude = array('phpsessid', 'jsessionid', 'sessionid', 'aspsessionid'); + public function setRequest($requestArray) { $this->request = $requestArray; @@ -117,17 +119,54 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface protected function setActionName($name) { + $name = $this->truncate($name); $this->actionName = $name; } + protected function setActionType($type) { $this->actionType = $type; } + protected function setActionUrl($url) { + $url = self::excludeQueryParametersFromUrl($url, $this->idSite); + $url = $this->truncate($url); $this->actionUrl = $url; } + static public function excludeQueryParametersFromUrl($originalUrl, $idSite) + { + $website = Piwik_Common::getCacheWebsiteAttributes( $idSite ); + $originalUrl = Piwik_Common::unsanitizeInputValue($originalUrl); + $parsedUrl = @parse_url($originalUrl); + if(empty($parsedUrl['query'])) + { + return $originalUrl; + } + $excludedParameters = isset($website['excluded_parameters']) ? $website['excluded_parameters'] : array(); + $parametersToExclude = array_merge($excludedParameters, self::$queryParametersToExclude); + + $parametersToExclude = array_map('strtolower', $parametersToExclude); + $queryParameters = Piwik_Common::getArrayFromQueryString($parsedUrl['query']); + + $validQuery = ''; + $separator = '&'; + foreach($queryParameters as $name => $value) + { + if(!in_array(strtolower($name), $parametersToExclude)) + { + $validQuery .= $name.'='.$value.$separator; + } + } + $parsedUrl['query'] = substr($validQuery,0,-strlen($separator)); + $url = Piwik_Common::getParseUrlReverse($parsedUrl); + printDebug('Excluded parameters "'.implode(',',$excludedParameters).'" from URL. + Before was <br/><code>"'.$originalUrl.'"</code>, <br/> + After is <br/><code>"'.$url.'"</code>'); + return $url; + } + public function init() { $info = $this->extractUrlAndActionNameFromRequest(); @@ -136,6 +175,12 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface $this->setActionUrl($info['url']); } + protected function truncate( $label ) + { + $limit = Piwik_Tracker_Config::getInstance()->Tracker['page_maximum_length']; + return substr($label, 0, $limit); + } + /** * Loads the idaction of the current action name and the current action url. * These idactions are used in the visitor logging table to link the visit information @@ -288,8 +333,10 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface $actionType = self::TYPE_ACTION_URL; $url = Piwik_Common::getRequestVar( 'url', '', 'string', $this->request); - // get the delimiter, by default '/' - $actionCategoryDelimiter = Piwik_Tracker_Config::getInstance()->General['action_category_delimiter']; + // get the delimiter, by default '/'; BC, we read the old action_category_delimiter first (see #1067) + $actionCategoryDelimiter = isset(Piwik_Tracker_Config::getInstance()->General['action_category_delimiter']) + ? Piwik_Tracker_Config::getInstance()->General['action_category_delimiter'] + : Piwik_Tracker_Config::getInstance()->General['action_url_category_delimiter']; // create an array of the categories delimited by the delimiter $split = explode($actionCategoryDelimiter, $actionName); diff --git a/core/Tracker/Config.php b/core/Tracker/Config.php index 1fd8a74a19..8d9e6f7499 100644 --- a/core/Tracker/Config.php +++ b/core/Tracker/Config.php @@ -48,36 +48,39 @@ class Piwik_Tracker_Config * @var array */ public $config = array(); - protected $init = false; + protected $initialized = false; public function init($pathIniFileUser = null, $pathIniFileGlobal = null) { + if(is_null($pathIniFileGlobal)) + { + $pathIniFileGlobal = PIWIK_USER_PATH . '/config/global.ini.php'; + } + $this->configGlobal = _parse_ini_file($pathIniFileGlobal, true); + if(is_null($pathIniFileUser)) { $pathIniFileUser = PIWIK_USER_PATH . '/config/config.ini.php'; } - if(is_null($pathIniFileGlobal)) + $this->configUser = _parse_ini_file($pathIniFileUser, true); + if($this->configUser) { - $pathIniFileGlobal = PIWIK_USER_PATH . '/config/global.ini.php'; - } - $this->configUser = parse_ini_file($pathIniFileUser, true); - $this->configGlobal = parse_ini_file($pathIniFileGlobal, true); - - foreach($this->configUser as $section => &$sectionValues) - { - foreach($sectionValues as $name => &$value) - { - if(is_array($value)) - { - $value = array_map("html_entity_decode", $value); - } - else + foreach($this->configUser as $section => &$sectionValues) + { + foreach($sectionValues as $name => &$value) { - $value = html_entity_decode($value); + if(is_array($value)) + { + $value = array_map("html_entity_decode", $value); + } + else + { + $value = html_entity_decode($value); + } } } } - $this->init = true; + $this->initialized = true; } /** @@ -90,7 +93,7 @@ class Piwik_Tracker_Config */ public function __get( $name ) { - if(!$this->init) + if(!$this->initialized) { $this->init(); } diff --git a/core/Tracker/Db.php b/core/Tracker/Db.php index 01291712d1..331fb3593a 100644 --- a/core/Tracker/Db.php +++ b/core/Tracker/Db.php @@ -155,6 +155,25 @@ abstract class Piwik_Tracker_Db } /** + * This function is a proxy to fetch(), used to maintain compatibility with Zend_Db interface + * @see fetch() + */ + public function fetchOne( $query, $parameters = array() ) + { + $result = $this->fetch($query, $parameters); + return is_array($result) && !empty($result) ? reset($result) : false; + } + + /** + * This function is a proxy to fetch(), used to maintain compatibility with Zend_Db + PDO interface + * @see fetch() + */ + public function exec( $query, $parameters = array() ) + { + return $this->fetch($query, $parameters); + } + + /** * Return number of affected rows in last query * * @param mixed $queryResult Result from query() diff --git a/core/Tracker/Generator.php b/core/Tracker/Generator.php deleted file mode 100644 index 7a6dfdfe21..0000000000 --- a/core/Tracker/Generator.php +++ /dev/null @@ -1,621 +0,0 @@ -<?php -/** - * Piwik - Open source web analytics - * - * @link http://piwik.org - * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later - * @version $Id$ - * - * @category Piwik - * @package Piwik - */ - -/** - * Class used to generate fake visits. - * Useful to test performances, general functional testing, etc. - * - * Objective: - * Generate thousands of visits / actions per visitor using - * a single request to misc/generateVisits.php - * - * Requirements of the visits generator script. Fields that can be edited: - * - url => campaigns - * - campaign CPC - * - referer - * - search engine - * - misc site - * - same website - * - url => multiple directories, page names - * - multiple idsite - * - multiple settings configurations - * - action_name - * - HTML title - * - * @package Piwik - * @subpackage Piwik_Tracker - * - * "Le Generator, il est trop Fort!" - * - Random fan - */ -class Piwik_Tracker_Generator -{ - /** - * GET parameters array of values to be used for the current visit - * - * @var array ('res' => '1024x768', 'urlref' => 'http://google.com/search?q=piwik', ...) - */ - protected $currentget = array(); - - /** - * Array of all the potential values for the visit parameters - * Values of 'resolution', 'urlref', etc. will be randomly read from this array - * - * @var array ( - * 'res' => array('1024x768','800x600'), - * 'urlref' => array('google.com','intel.com','amazon.com'), - * ....) - */ - protected $allget = array(); - - /** - * See @see setMaximumUrlDepth - * - * @var int - */ - protected $maximumUrlDepth = 1; - - /** - * Unix timestamp to use for the generated visitor - * - * @var int Unix timestamp - */ - protected $timestampToUse; - - /** - * See @see disableProfiler() - * The profiler is enabled by default - * - * @var bool - */ - protected $profiling = true; - - /** - * If set to true, this will TRUNCATE the profiling tables at every new generated visit - * @see initProfiler() - * - * @var bool - */ - public $reinitProfilingAtEveryRequest = true; - - /** - * Hostname used to prefix all the generated URLs - * we could make this variable dynamic so that a visitor can make hit on several hosts and - * only the good ones should be kept (feature not yet implemented in piwik) - * - * @var string - */ - public $host = 'http://localhost'; - - /** - * IdSite to generate visits for (@see setIdSite()) - * - * @var int - */ - public $idSite = 1; - - /** - * Overwrite the global GET/POST/COOKIE variables and set the fake ones @see setFakeRequest() - * Reads the configuration file but disables write to this file - * Creates the database object & enable profiling by default (@see disableProfiler()) - * - */ - public function __construct() - { - $_COOKIE = $_GET = $_POST = array(); - - // init GET and REQUEST to the empty array - $this->setFakeRequest(); - - Piwik::createConfigObject(PIWIK_USER_PATH . '/config/config.ini.php'); - Zend_Registry::get('config')->disableSavingConfigurationFileUpdates(); - - // setup database - Piwik::createDatabaseObject(); - - Piwik_Tracker_Db::enableProfiling(); - - $this->timestampToUse = time(); - } - - /** - * Sets the depth level of the generated URLs - * value = 1 => path OR path/page1 - * value = 2 => path OR path/pageRand OR path/dir1/pageRand - * - * @param int Depth - */ - public function setMaximumUrlDepth($value) - { - $this->maximumUrlDepth = (int)$value; - } - - /** - * Set the timestamp to use as the starting time for the visitors times - * You have to call this method for every day you want to generate data - * - * @param int Unix timestamp - */ - public function setTimestampToUse($timestamp) - { - $this->timestampToUse = $timestamp; - } - - /** - * Returns the timestamp to be used as the visitor timestamp - * - * @return int Unix timestamp - */ - public function getTimestampToUse() - { - return $this->timestampToUse; - } - - /** - * Set the idsite to generate the visits for - * To be called before init() - * - * @param int idSite - */ - public function setIdSite($idSite) - { - $this->idSite = $idSite; - } - - /** - * Add a value to the GET global array. - * The generator script will then randomly read a value from this array. - * - * For example, $name = 'res' $aValue = '1024x768' - * - * @param string Name of the parameter _GET[$name] - * @param array|mixed Value of the parameter - */ - protected function addParam( $name, $aValue) - { - if(is_array($aValue)) - { - $this->allget[$name] = array_merge( $aValue, - (array)@$this->allget[$name]); - } - else - { - $this->allget[$name][] = $aValue; - } - } - - /** - * TRUNCATE all logs related tables to start a fresh logging database. - * Be careful, any data deleted this way is deleted forever - */ - public function emptyAllLogTables() - { - $db = Zend_Registry::get('db'); - $db->query('TRUNCATE TABLE '.Piwik::prefixTable('log_action')); - $db->query('TRUNCATE TABLE '.Piwik::prefixTable('log_visit')); - $db->query('TRUNCATE TABLE '.Piwik::prefixTable('log_link_visit_action')); - } - - /** - * Call this method to disable the SQL query profiler - */ - public function disableProfiler() - { - $this->profiling = false; - Piwik_Tracker_Db::disableProfiling(); - } - - /** - * This is called at the end of the Generator script. - * Calls the Profiler output if the profiler is enabled. - */ - public function end() - { - if($this->profiling) - { - Piwik::printSqlProfilingReportTracker(); - } - Piwik_Tracker::disconnectDatabase(); - } - - /** - * Init the Generator script: - * - init the SQL profiler - * - init the random generator - * - setup the different possible values for parameters such as 'resolution', - * 'color', 'hour', 'minute', etc. - * - load from DataFiles and setup values for the other parameters such as UserAgent, Referers, AcceptedLanguages, etc. - * @see misc/generateVisitsData/ - */ - public function init() - { - Piwik::createLogObject(); - - $this->initProfiler(); - - /* - * Init the random number generator - */ - function make_seed() - { - list($usec, $sec) = explode(' ', microtime()); - return (float) $sec + ((float) $usec * 100000); - } - mt_srand(make_seed()); - - /* - * Sets values for: resolutions, colors, idSite, times - */ - $common = array( - 'res' => array('1289x800','1024x768','800x600','564x644','200x100','50x2000',), - 'col' => array(24,32,16), - 'idsite'=> $this->idSite, - 'h' => range(0,23), - 'm' => range(0,59), - 's' => range(0,59), - ); - - foreach($common as $label => $values) - { - $this->addParam($label,$values); - } - - /* - * Sets values for: outlinks, downloads, campaigns - */ - // we get the name of the Download/outlink variables - $downloadOrOutlink = array('download', 'link'); - - // we have a 20% chance to add a download or outlink variable to the URL - $this->addParam('piwik_downloadOrOutlink', $downloadOrOutlink); - $this->addParam('piwik_downloadOrOutlink', array_fill(0,8,'')); - - // we get the variables name for the campaign parameters - $campaigns = array( - Piwik_Tracker_Config::getInstance()->Tracker['campaign_var_name'] - ); - // we generate a campaign in the URL in 3/18 % of the generated URls - $this->addParam('piwik_vars_campaign', $campaigns); - $this->addParam('piwik_vars_campaign', array_fill(0,15,'')); - - - /* - * Sets values for: Referers, user agents, accepted languages - */ - // we load some real referers to be used by the generator - $referers = array(); - require_once PIWIK_INCLUDE_PATH . '/misc/generateVisitsData/Referers.php'; - - $this->addParam('urlref',$referers); - - // and we add 2000 empty referers so that some visitors don't come using a referer (direct entry) - $this->addParam('urlref',array_fill(0,2000,'')); - - // load some user agent and accept language - $userAgent = $acceptLanguages = array(); - require_once PIWIK_INCLUDE_PATH . '/misc/generateVisitsData/UserAgent.php'; - require_once PIWIK_INCLUDE_PATH . '/misc/generateVisitsData/AcceptLanguage.php'; - $this->userAgents=$userAgent; - $this->acceptLanguage=$acceptLanguages; - } - - /** - * If the SQL profiler is enabled and if the reinit at every request is set to true, - * then we TRUNCATE the profiling information so that we only profile one visitor at a time - */ - protected function initProfiler() - { - /* - * Inits the profiler - */ - if($this->profiling) - { - if($this->reinitProfilingAtEveryRequest) - { - $all = Piwik_Query('TRUNCATE TABLE '.Piwik::prefixTable('log_profiling').'' ); - } - } - } - /** - * Launches the process and generates an exact number of nbVisitors - * For each visit, we setup the timestamp to the common timestamp - * Then we generate between 1 and nbActionsMaxPerVisit actions for this visit - * The generated actions will have a growing timestamp so it looks like a real visit - * - * @param int The number of visits to generate - * @param int The maximum number of actions to generate per visit - * - * @return int The number of total actions generated - */ - public function generate( $nbVisitors, $nbActionsMaxPerVisit ) - { - $nbActionsTotal = 0; - for($i = 0; $i < $nbVisitors; $i++) - { - $nbActions = mt_rand(1, $nbActionsMaxPerVisit); - Piwik_Tracker_Generator_Visit::setTimestampToUse($this->getTimestampToUse()); - - $this->generateNewVisit(); - for($j = 1; $j <= $nbActions; $j++) - { - $this->generateActionVisit(); - $this->saveVisit(); - } - $nbActionsTotal += $nbActions; - } - return $nbActionsTotal; - } - - /** - * Generates a new visitor. - * Loads random values for all the necessary parameters (resolution, local time, referers, etc.) from the fake GET array. - * Also generates a random IP. - * - * We change the superglobal values of HTTP_USER_AGENT, HTTP_CLIENT_IP, HTTP_ACCEPT_LANGUAGE to the generated value. - */ - protected function generateNewVisit() - { - $this->setCurrentRequest( 'urlref' , $this->getRandom('urlref')); - $this->setCurrentRequest( 'idsite', $this->getRandom('idsite')); - $this->setCurrentRequest( 'res' ,$this->getRandom('res')); - $this->setCurrentRequest( 'col' ,$this->getRandom('col')); - $this->setCurrentRequest( 'h' ,$this->getRandom('h')); - $this->setCurrentRequest( 'm' ,$this->getRandom('m')); - $this->setCurrentRequest( 's' ,$this->getRandom('s')); - $this->setCurrentRequest( 'fla' ,$this->getRandom01()); - $this->setCurrentRequest( 'java' ,$this->getRandom01()); - $this->setCurrentRequest( 'dir' ,$this->getRandom01()); - $this->setCurrentRequest( 'qt' ,$this->getRandom01()); - $this->setCurrentRequest( 'realp' ,$this->getRandom01()); - $this->setCurrentRequest( 'pdf' ,$this->getRandom01()); - $this->setCurrentRequest( 'wma' ,$this->getRandom01()); - $this->setCurrentRequest( 'gears' ,$this->getRandom01()); - $this->setCurrentRequest( 'ag' ,$this->getRandom01()); - $this->setCurrentRequest( 'cookie',$this->getRandom01()); - - $_SERVER['HTTP_CLIENT_IP'] = mt_rand(0,255).".".mt_rand(0,255).".".mt_rand(0,255).".".mt_rand(0,255); - $_SERVER['HTTP_USER_AGENT'] = $this->userAgents[mt_rand(0,count($this->userAgents)-1)]; - $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $this->acceptLanguage[mt_rand(0,count($this->acceptLanguage)-1)]; - } - - /** - * Generates a new action for the current visitor. - * We random generate some campaigns, action names, download or outlink clicks, etc. - * We generate a new Referer, that would be read in the case the visit last page is older than 30 minutes. - * - * This function tries to generate actions that use the features of Piwik (campaigns, downloads, outlinks, action_name set in the JS tag, etc.) - */ - protected function generateActionVisit() - { - // we don't keep the previous action values - // reinit them to empty string - $this->setCurrentRequest( 'download', ''); - $this->setCurrentRequest( 'link', ''); - $this->setCurrentRequest( 'action_name', ''); - - // generate new url referer ; case the visitor stays more than 30min - // (when the visit is known this value will simply be ignored) - $this->setCurrentRequest( 'urlref' , $this->getRandom('urlref')); - - // generates the current URL - $url = $this->getRandomUrlFromHost($this->host); - - // we generate a campaign - $urlVars = $this->getRandom('piwik_vars_campaign'); - - // if we actually generated a campaign - if(!empty($urlVars)) - { - // campaign name - $urlValue = $this->getRandomString(5,3,'lower'); - - // add the parameter to the url - $url .= '?'. $urlVars . '=' . $urlValue; - - // for a campaign of the CPC kind, we sometimes generate a keyword - if($urlVars == Piwik_Tracker_Config::getInstance()->Tracker['campaign_var_name'] - && mt_rand(0,1)==0) - { - $url .= '&'. Piwik_Tracker_Config::getInstance()->Tracker['campaign_keyword_var_name'] - . '=' . $this->getRandomString(6,3,'ALL');; - } - } - else - { - // we generate a download Or Outlink parameter in the GET request so that - // the current action is counted as a download action OR a outlink click action - $GETParamToAdd = $this->getRandom('piwik_downloadOrOutlink'); - if(!empty($GETParamToAdd)) - { - - $possibleDownloadHosts = array('http://piwik.org/',$this->host); - $nameDownload = $this->getRandomUrlFromHost($possibleDownloadHosts[mt_rand(0,1)]); - $extensions = array('.zip','.tar.gz'); - $nameDownload .= $extensions[mt_rand(0,1)]; - $urlValue = $nameDownload; - - // add the parameter to the url - $this->setCurrentRequest( $GETParamToAdd , $urlValue); - } - } - - $this->setCurrentRequest( 'url' ,$url); - - // setup the title of the page - $this->setCurrentRequest( 'action_name',$this->getRandomString(15,5)); - } - - /** - * Returns a random URL using the $host as the URL host. - * Depth level depends on @see setMaximumUrlDepth() - * - * @param string Hostname of the URL to generate, eg. http://example.com/ - * - * @return string The generated URL - */ - protected function getRandomUrlFromHost( $host ) - { - $url = $host; - - $deep = mt_rand(0,$this->maximumUrlDepth); - for($i=0;$i<$deep;$i++) - { - $name = $this->getRandomString(1,1,'alnum'); - - $url .= '/'.$name; - } - return $url; - } - - /** - * Generates a random string from minLength to maxLength using a specified set of characters - * - * Taken from php.net and then badly hacked by some unknown monkey - * - * @param int (optional) Maximum length of the string to generate - * @param int (optional) Minimum length of the string to generate - * @param string (optional) Characters set to use, 'ALL' or 'lower' or 'upper' or 'numeric' or 'ALPHA' or 'ALNUM' - * - * @return string The generated random string - */ - protected function getRandomString($maxLength = 15, $minLength = 5, $type = 'ALL') - { - $len = mt_rand($minLength, $maxLength); - - // Register the lower case alphabet array - $alpha = array('a', 'd', 'e', 'f', 'g'); - - // Register the upper case alphabet array - $ALPHA = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', - 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'); - - // Register the numeric array - $num = array('1', '2', '3', '8', '9', '0'); - - // Register the strange array - $strange = array('/', '?', '!','"','£','$','%','^','&','*','(',')',' '); - - // Initialize the keyVals array for use in the for loop - $keyVals = array(); - - // Initialize the key array to register each char - $key = array(); - - // Loop through the choices and register - // The choice to keyVals array - switch ($type) - { - case 'lower' : - $keyVals = $alpha; - break; - case 'upper' : - $keyVals = $ALPHA; - break; - case 'numeric' : - $keyVals = $num; - break; - case 'ALPHA' : - $keyVals = array_merge($alpha, $ALPHA); - break; - case 'alnum' : - $keyVals = array_merge($alpha, $num); - break; - case 'ALNUM' : - $keyVals = array_merge($alpha, $ALPHA, $num); - break; - case 'ALL' : - $keyVals = array_merge($alpha, $ALPHA, $num, $strange); - break; - } - - // Loop as many times as specified - // Register each value to the key array - for($i = 0; $i <= $len-1; $i++) - { - $r = mt_rand(0,count($keyVals)-1); - $key[$i] = $keyVals[$r]; - } - - // Glue the key array into a string and return it - return join("", $key); - } - - /** - * Sets the _GET and _REQUEST superglobal to the current generated array of values. - * @see setCurrentRequest() - * This method is called once the current action parameters array has been generated from - * the global parameters array - */ - protected function setFakeRequest() - { - $_GET = $this->currentget; - } - - /** - * Sets a value in the current action request array. - * - * @param string Name of the parameter to set - * @param string Value of the parameter - */ - protected function setCurrentRequest($name,$value) - { - $this->currentget[$name] = $value; - } - - /** - * Returns a value for the given parameter $name read randomly from the global parameter array. - * @see init() - * - * @param string Name of the parameter value to randomly load and return - * @return mixed Random value for the parameter named $name - * @throws Exception if the parameter asked for has never been set - * - */ - protected function getRandom( $name ) - { - if(!isset($this->allget[$name])) - { - throw new exception("You are asking for $name which doesnt exist"); - } - else - { - $index = mt_rand(0,count($this->allget[$name])-1); - $value =$this->allget[$name][$index]; - return $value; - } - } - - /** - * Returns either 0 or 1 - * - * @return int 0 or 1 - */ - protected function getRandom01() - { - return mt_rand(0,1); - } - - /** - * Saves the visit - * - replaces GET and REQUEST by the fake generated request - * - load the Tracker class and call the method to launch the recording - * - * This will save the visit in the database - */ - protected function saveVisit() - { - $this->setFakeRequest(); - $process = new Piwik_Tracker_Generator_Tracker(); - $process->main(); - unset($process); - } -} diff --git a/core/Tracker/Generator/Tracker.php b/core/Tracker/Generator/Tracker.php deleted file mode 100644 index 92bd002d59..0000000000 --- a/core/Tracker/Generator/Tracker.php +++ /dev/null @@ -1,53 +0,0 @@ -<?php -/** - * Piwik - Open source web analytics - * - * @link http://piwik.org - * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later - * @version $Id$ - * - * @category Piwik - * @package Piwik - */ - -/** - * Fake Piwik_Tracker that: - * - overwrite the sendHeader method so that no headers are sent. - * - doesn't print the 1pixel transparent GIF at the end of the visit process - * - overwrite the Tracker Visit object to use so we use our own Tracker_visit @see Piwik_Tracker_Generator_Visit - * - * @package Piwik - * @subpackage Piwik_Tracker - */ -class Piwik_Tracker_Generator_Tracker extends Piwik_Tracker -{ - /** - * Does nothing instead of sending headers - */ - protected function sendHeader($header) - { - } - - /** - * Does nothing instead of displaying a 1x1 transparent pixel GIF - */ - protected function end() - { - } - - /** - * Returns our 'generator home made' Piwik_Tracker_Generator_Visit object. - * - * @return Piwik_Tracker_Generator_Visit - */ - protected function getNewVisitObject() - { - $visit = new Piwik_Tracker_Generator_Visit(); - return $visit; - } - - static function disconnectDatabase() - { - return; - } -} diff --git a/core/Tracker/Generator/Visit.php b/core/Tracker/Generator/Visit.php deleted file mode 100644 index 2937ebfe0d..0000000000 --- a/core/Tracker/Generator/Visit.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php -/** - * Piwik - Open source web analytics - * - * @link http://piwik.org - * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later - * @version $Id$ - * - * @category Piwik - * @package Piwik - */ - -/** - * Fake Piwik_Tracker_Visit class that overwrite all the Time related method to be able - * to setup a given timestamp for the generated visitor and actions. - * - * @package Piwik - * @subpackage Piwik_Tracker - */ -class Piwik_Tracker_Generator_Visit extends Piwik_Tracker_Visit -{ - static protected $timestampToUse; - - static public function setTimestampToUse($time) - { - self::$timestampToUse = $time; - } - protected function getCurrentDate( $format = "Y-m-d") - { - return date($format, $this->getCurrentTimestamp() ); - } - - protected function getCurrentTimestamp() - { - self::$timestampToUse = max(@$this->visitorInfo['visit_last_action_time'],self::$timestampToUse); - self::$timestampToUse += mt_rand(4,1840); - return self::$timestampToUse; - } - - protected function updateCookie() - { - @parent::updateCookie(); - } -} diff --git a/core/Tracker/GoalManager.php b/core/Tracker/GoalManager.php index f1ff810891..29e6d508db 100644 --- a/core/Tracker/GoalManager.php +++ b/core/Tracker/GoalManager.php @@ -52,7 +52,7 @@ class Piwik_Tracker_GoalManager return $goal; } } - throw new Exception("The goal id = $idGoal couldn't be found."); + throw new Exception(Piwik_TranslateException('General_ExceptionGoalNotFound', array($idGoal))); } static public function getGoalIds( $idSite ) @@ -71,14 +71,14 @@ class Piwik_Tracker_GoalManager return Piwik_PluginsManager::getInstance()->isPluginActivated('Goals'); } - //TODO does this code work for manually triggered goals, with custom revenue? function detectGoalsMatchingUrl($idSite, $action) { if(!$this->isGoalPluginEnabled()) { return false; } - $url = $action->getActionUrl(); + $sanitizedUrl = $action->getActionUrl(); + $url = htmlspecialchars_decode($sanitizedUrl); $actionType = $action->getActionType(); $goals = $this->getGoalDefinitions($idSite); foreach($goals as $goal) @@ -129,12 +129,12 @@ class Piwik_Tracker_GoalManager $match = ($matched == 0); break; default: - throw new Exception("Pattern type $pattern_type not valid."); + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidGoalPattern', array($pattern_type))); break; } if($match) { - $goal['url'] = $url; + $goal['url'] = $sanitizedUrl; $this->convertedGoals[] = $goal; } } @@ -154,7 +154,9 @@ class Piwik_Tracker_GoalManager return false; } $goal = $goals[$idGoal]; - $goal['url'] = Piwik_Common::getRequestVar( 'url', '', 'string', $request); + + $url = Piwik_Common::getRequestVar( 'url', '', 'string', $request); + $goal['url'] = Piwik_Tracker_Action::excludeQueryParametersFromUrl($url, $idSite); $goal['revenue'] = Piwik_Common::getRequestVar('revenue', $goal['revenue'], 'float', $request); $this->convertedGoals[] = $goal; return true; @@ -162,15 +164,22 @@ class Piwik_Tracker_GoalManager function recordGoals($visitorInformation, $action) { - $location_country = isset($visitorInformation['location_country']) ? $visitorInformation['location_country'] : Piwik_Common::getCountry(Piwik_Common::getBrowserLanguage(), $enableLanguageToCountryGuess = Piwik_Tracker_Config::getInstance()->Tracker['enable_language_to_country_guess']); - $location_continent = isset($visitorInformation['location_continent']) ? $visitorInformation['location_continent'] : Piwik_Common::getContinent($location_country); + $location_country = isset($visitorInformation['location_country']) + ? $visitorInformation['location_country'] + : Piwik_Common::getCountry( + Piwik_Common::getBrowserLanguage(), + $enableLanguageToCountryGuess = Piwik_Tracker_Config::getInstance()->Tracker['enable_language_to_country_guess'], $visitorInformation['location_ip'] + ); + + $location_continent = isset($visitorInformation['location_continent']) + ? $visitorInformation['location_continent'] + : Piwik_Common::getContinent($location_country); $goal = array( 'idvisit' => $visitorInformation['idvisit'], 'idsite' => $visitorInformation['idsite'], 'visitor_idcookie' => $visitorInformation['visitor_idcookie'], 'server_time' => Piwik_Tracker::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time']), - 'visit_server_date' => $visitorInformation['visit_server_date'], 'location_country' => $location_country, 'location_continent'=> $location_continent, 'visitor_returning' => $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_VISITOR_RETURNING ), @@ -207,14 +216,14 @@ class Piwik_Tracker_GoalManager try { Piwik_Tracker::getDatabase()->query( - "INSERT INTO " . Piwik_Common::prefixTable('log_conversion') . " ($fields) + "INSERT IGNORE INTO " . Piwik_Common::prefixTable('log_conversion') . " ($fields) VALUES ($bindFields) ", array_values($newGoal) ); } catch( Exception $e) { - if(Piwik_Tracker::isErrNo($e, '1062')) + if(Piwik_Tracker::getDatabase()->isErrNo($e, '1062')) { // integrity violation when same visit converts to the same goal twice - printDebug("--> Goal already recorded for this (idvisit, idgoal)"); + printDebug("--> Goal already recorded for this (idvisit, idgoal)"); } else { diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php index 18c636f483..b216f742e7 100644 --- a/core/Tracker/Visit.php +++ b/core/Tracker/Visit.php @@ -59,9 +59,10 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $this->request = $requestArray; $idsite = Piwik_Common::getRequestVar('idsite', 0, 'int', $this->request); + Piwik_PostEvent('Tracker.setRequest.idSite', $idsite); if($idsite <= 0) { - throw new Exception("The 'idsite' in the request is invalid."); + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidIdsite')); } $this->idsite = $idsite; } @@ -88,6 +89,9 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface */ public function handle() { + // the IP is needed by isExcluded() and GoalManager->recordGoals() + $this->visitorInfo['location_ip'] = Piwik_Common::getIp(); + if($this->isExcluded()) { return; @@ -106,6 +110,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface // if we find a idgoal in the URL, but then the goal is not valid, this is most likely a fake request if(!$someGoalsConverted) { + printDebug('Invalid goal tracking request for goal id = '.$idGoal); unset($goalManager); return; } @@ -127,9 +132,10 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $isLastActionInTheSameVisit = $this->isLastActionInTheSameVisit(); // Known visit when: - // - the visitor has the Piwik cookie with the idcookie ID used by Piwik to match the visitor - // OR - // - the visitor doesn't have the Piwik cookie but could be match using heuristics @see recognizeTheVisitor() + // ( - the visitor has the Piwik cookie with the idcookie ID used by Piwik to match the visitor + // OR + // - the visitor doesn't have the Piwik cookie but could be match using heuristics @see recognizeTheVisitor() + // ) // AND // - the last page view for this visitor was less than 30 minutes ago @see isLastActionInTheSameVisit() if( $this->isVisitorKnown() @@ -232,26 +238,31 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface visit_total_actions = visit_total_actions + 1, "; $this->visitorInfo['visit_exit_idaction_url'] = $actionUrlId; } - $result = Piwik_Tracker::getDatabase()->query("/* SHARDING_ID_SITE = ". $this->idsite ." */ + $sqlQuery = "/* SHARDING_ID_SITE = ". $this->idsite ." */ UPDATE ". Piwik_Common::prefixTable('log_visit')." SET $sqlActionIdUpdate $sqlUpdateGoalConverted visit_last_action_time = ?, - visit_total_time = UNIX_TIMESTAMP(visit_last_action_time) - UNIX_TIMESTAMP(visit_first_action_time) - WHERE idvisit = ? + visit_total_time = ? + WHERE idsite = ? + AND idvisit = ? AND visitor_idcookie = ? - LIMIT 1", - array( $datetimeServer, - $this->visitorInfo['idvisit'], - $this->visitorInfo['visitor_idcookie'] ) - ); + LIMIT 1"; + $sqlBind = array( $datetimeServer, + $visitTotalTime = $this->getCurrentTimestamp() - $this->visitorInfo['visit_first_action_time'], + $this->idsite, + $this->visitorInfo['idvisit'], + $this->visitorInfo['visitor_idcookie'] ); + + $result = Piwik_Tracker::getDatabase()->query($sqlQuery, $sqlBind); + + printDebug('Updating visitor with idvisit='.$this->visitorInfo['idvisit'].', setting visit_last_action_time='.$datetimeServer.' and visit_total_time='.$visitTotalTime); if(Piwik_Tracker::getDatabase()->rowCount($result) == 0) { throw new Piwik_Tracker_Visit_VisitorNotFoundInDatabase("The visitor with visitor_idcookie=".$this->visitorInfo['visitor_idcookie']." and idvisit=".$this->visitorInfo['idvisit']." wasn't found in the DB, we fallback to a new visitor"); } $this->visitorInfo['idsite'] = $this->idsite; - $this->visitorInfo['visit_server_date'] = $this->getCurrentDate(); // will be updated in cookie $this->visitorInfo['time_spent_ref_action'] = $serverTime - $this->visitorInfo['visit_last_action_time']; @@ -275,7 +286,6 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface .':'. Piwik_Common::getRequestVar( 'm', $this->getCurrentDate("i"), 'int', $this->request) .':'. Piwik_Common::getRequestVar( 's', $this->getCurrentDate("s"), 'int', $this->request); $serverTime = $this->getCurrentTimestamp(); - $serverDate = $this->getCurrentDate(); $idcookie = $this->getVisitorIdcookie(); $returningVisitor = $this->isVisitorKnown() ? 1 : 0; @@ -283,47 +293,49 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $defaultTimeOnePageVisit = Piwik_Tracker_Config::getInstance()->Tracker['default_time_one_page_visit']; $userInfo = $this->getUserSettingsInformation(); - $country = Piwik_Common::getCountry($userInfo['location_browser_lang'], $enableLanguageToCountryGuess = Piwik_Tracker_Config::getInstance()->Tracker['enable_language_to_country_guess']); + $country = Piwik_Common::getCountry($userInfo['location_browser_lang'], + $enableLanguageToCountryGuess = Piwik_Tracker_Config::getInstance()->Tracker['enable_language_to_country_guess'], + $this->getVisitorIp()); $refererInfo = $this->getRefererInformation(); /** * Save the visitor */ $this->visitorInfo = array( - 'idsite' => $this->idsite, - 'visitor_localtime' => $localTime, - 'visitor_idcookie' => $idcookie, - 'visitor_returning' => $returningVisitor, - 'visit_first_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($serverTime), - 'visit_last_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($serverTime), - 'visit_server_date' => $serverDate, - 'visit_entry_idaction_url' => $actionUrlId, - 'visit_exit_idaction_url' => $actionUrlId, - 'visit_total_actions' => 1, - 'visit_total_time' => $defaultTimeOnePageVisit, - 'visit_goal_converted' => $someGoalsConverted ? 1: 0, - 'referer_type' => $refererInfo['referer_type'], - 'referer_name' => $refererInfo['referer_name'], - 'referer_url' => $refererInfo['referer_url'], - 'referer_keyword' => $refererInfo['referer_keyword'], - 'config_md5config' => $userInfo['config_md5config'], - 'config_os' => $userInfo['config_os'], - 'config_browser_name' => $userInfo['config_browser_name'], - 'config_browser_version' => $userInfo['config_browser_version'], - 'config_resolution' => $userInfo['config_resolution'], - 'config_pdf' => $userInfo['config_pdf'], - 'config_flash' => $userInfo['config_flash'], - 'config_java' => $userInfo['config_java'], - 'config_director' => $userInfo['config_director'], - 'config_quicktime' => $userInfo['config_quicktime'], - 'config_realplayer' => $userInfo['config_realplayer'], - 'config_windowsmedia' => $userInfo['config_windowsmedia'], - 'config_gears' => $userInfo['config_gears'], - 'config_silverlight' => $userInfo['config_silverlight'], - 'config_cookie' => $userInfo['config_cookie'], - 'location_ip' => $userInfo['location_ip'], - 'location_browser_lang' => $userInfo['location_browser_lang'], - 'location_country' => $country, + 'idsite' => $this->idsite, + 'visitor_localtime' => $localTime, + 'visitor_idcookie' => $idcookie, + 'visitor_returning' => $returningVisitor, + 'visit_server_date' => $this->getCurrentDate(), + 'visit_first_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($serverTime), + 'visit_last_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($serverTime), + 'visit_entry_idaction_url' => $actionUrlId, + 'visit_exit_idaction_url' => $actionUrlId, + 'visit_total_actions' => 1, + 'visit_total_time' => $defaultTimeOnePageVisit, + 'visit_goal_converted' => $someGoalsConverted ? 1: 0, + 'referer_type' => $refererInfo['referer_type'], + 'referer_name' => $refererInfo['referer_name'], + 'referer_url' => $refererInfo['referer_url'], + 'referer_keyword' => $refererInfo['referer_keyword'], + 'config_md5config' => $userInfo['config_md5config'], + 'config_os' => $userInfo['config_os'], + 'config_browser_name' => $userInfo['config_browser_name'], + 'config_browser_version' => $userInfo['config_browser_version'], + 'config_resolution' => $userInfo['config_resolution'], + 'config_pdf' => $userInfo['config_pdf'], + 'config_flash' => $userInfo['config_flash'], + 'config_java' => $userInfo['config_java'], + 'config_director' => $userInfo['config_director'], + 'config_quicktime' => $userInfo['config_quicktime'], + 'config_realplayer' => $userInfo['config_realplayer'], + 'config_windowsmedia' => $userInfo['config_windowsmedia'], + 'config_gears' => $userInfo['config_gears'], + 'config_silverlight' => $userInfo['config_silverlight'], + 'config_cookie' => $userInfo['config_cookie'], + 'location_ip' => $this->getVisitorIp(), + 'location_browser_lang' => $userInfo['location_browser_lang'], + 'location_country' => $country, ); Piwik_PostEvent('Tracker.newVisitorInformation', $this->visitorInfo); @@ -389,8 +401,9 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface */ protected function getVisitorIp() { - return Piwik_Common::getIp(); + return $this->visitorInfo['location_ip']; } + /** * Returns the visitor's browser (user agent) @@ -433,20 +446,62 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface */ protected function isExcluded() { + $excluded = false; + $ip = $this->getVisitorIp(); $ua = $this->getUserAgent(); /* - * Live/Bing bot and Googlebot are evolving to detect cloaked websites. + * Live/Bing/MSN bot and Googlebot are evolving to detect cloaked websites. * As a result, these sophisticated bots exhibit characteristics of * browsers (cookies enabled, executing JavaScript, etc). */ - $excluded = preg_match('/65\.55/', long2ip($ip)) // Live/Bing - || preg_match('/Googlebot/', $ua); // Googlebot - - /* custom filters can override the built-in filter above */ + $dotIp = long2ip($ip); + if (strpos($dotIp, '65.55') === 0 // Live/Bing + || strpos($dotIp, '207.46') === 0 // MSN + || strpos($ua, 'Googlebot') !== false) // Googlebot + { + printDebug('Search bot detected, visit excluded'); + $excluded = true; + } + + + /* + * Requests built with piwik.js will contain a rec=1 parameter. This is used as + * an indication that the request is made by a JS enabled device. By default, Piwik + * doesn't track non-JS visitors. + */ + if(!$excluded) + { + $parameterForceRecord = 'rec'; + $toRecord = Piwik_Common::getRequestVar($parameterForceRecord, false, 'int'); + if(!$toRecord) + { + printDebug('GET parameter '.$parameterForceRecord.' not found in URL, request excluded'); + $excluded = true; + } + } + + /* custom filters can override the built-in filters above */ Piwik_PostEvent('Tracker.Visit.isExcluded', $excluded); - + + /* + * Following exclude operations happen after the hook. + * These are of higher priority and should not be overwritten by plugins. + */ + + // Checking if the Piwik ignore cookie is set + if(!$excluded) + { + $excluded = $this->isIgnoreCookieFound(); + } + + // Checking for excluded IPs + if(!$excluded) + { + $excluded = $this->isVisitorIpExcluded($ip); + } + if($excluded) { printDebug("Visitor excluded."); @@ -455,8 +510,47 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface return false; } + + /** + * Looks for the ignore cookie that users can set in the Piwik admin screen. + * @return bool + */ + protected function isIgnoreCookieFound() + { + $cookie = new Piwik_Cookie($this->getIgnoreVisitsCookieName()); + if($cookie->isCookieFound()) + { + printDebug('Piwik ignore cookie was found, visit not tracked.'); + return true; + } + return false; + } /** + * Checks if the visitor ip is in the excluded list + * + * @param $ip Long IP + * @return bool + */ + protected function isVisitorIpExcluded($ip) + { + $websiteAttributes = Piwik_Common::getCacheWebsiteAttributes( $this->idsite ); + if(!empty($websiteAttributes['excluded_ips'])) + { + foreach($websiteAttributes['excluded_ips'] as $ipRange) + { + if($ip >= $ipRange[0] + && $ip <= $ipRange[1]) + { + printDebug('Visitor IP '.long2ip($ip).' is excluded from being tracked'); + return true; + } + } + } + return false; + } + + /** * Returns the cookie name used for the Piwik Tracker cookie * * @return string @@ -467,13 +561,33 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface } /** + * Returns the cookie name used to ignore/exclude webmaster visits + * + * @return string + */ + protected function getIgnoreVisitsCookieName() + { + return Piwik_Tracker_Config::getInstance()->Tracker['ignore_visits_cookie_name']; + } + + /** * Returns the cookie expiration date. * * @return int */ protected function getCookieExpire() { - return time() + Piwik_Tracker_Config::getInstance()->Tracker['cookie_expire']; + return $this->getCurrentTimestamp() + Piwik_Tracker_Config::getInstance()->Tracker['cookie_expire']; + } + + /** + * Returns cookie path + * + * @return string + */ + protected function getCookiePath() + { + return Piwik_Tracker_Config::getInstance()->Tracker['cookie_path']; } /** @@ -508,7 +622,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface protected function recognizeTheVisitor() { $this->visitorKnown = false; - $this->setCookie( new Piwik_Cookie( $this->getCookieName(), $this->getCookieExpire() ) ); + $this->setCookie( new Piwik_Cookie( $this->getCookieName(), $this->getCookieExpire(), $this->getCookiePath() ) ); /* * Case the visitor has the piwik cookie. @@ -551,8 +665,8 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $visitRow = Piwik_Tracker::getDatabase()->fetch( " SELECT visitor_idcookie, - UNIX_TIMESTAMP(visit_last_action_time) as visit_last_action_time, - UNIX_TIMESTAMP(visit_first_action_time) as visit_first_action_time, + visit_last_action_time, + visit_first_action_time, idvisit, visit_exit_idaction_url FROM ".Piwik_Common::prefixTable('log_visit'). @@ -566,14 +680,14 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface && count($visitRow) > 0) { $this->visitorInfo['visitor_idcookie'] = $visitRow['visitor_idcookie']; - $this->visitorInfo['visit_last_action_time'] = $visitRow['visit_last_action_time']; - $this->visitorInfo['visit_first_action_time'] = $visitRow['visit_first_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']); $this->visitorInfo['idvisit'] = $visitRow['idvisit']; $this->visitorInfo['visit_exit_idaction_url'] = $visitRow['visit_exit_idaction_url']; $this->visitorKnown = true; - printDebug("The visitor is known because of his userSettings+IP (idcookie = {$visitRow['visitor_idcookie']}, idvisit = {$this->visitorInfo['idvisit']}, last action = ".date("r", $this->visitorInfo['visit_last_action_time']).") "); + printDebug("The visitor is known because of his userSettings+IP (idcookie = {$visitRow['visitor_idcookie']}, idvisit = {$this->visitorInfo['idvisit']}, last action = ".date("r", $this->visitorInfo['visit_last_action_time']).", first action = ".date("r", $this->visitorInfo['visit_first_action_time']) .")"); } } } @@ -614,8 +728,6 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $resolution = Piwik_Common::getRequestVar('res', 'unknown', 'string', $this->request); - $ip = $this->getVisitorIp(); - $browserLang = Piwik_Common::getBrowserLanguage(); $configurationHash = $this->getConfigHash( @@ -633,7 +745,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $plugin_Gears, $plugin_Silverlight, $plugin_Cookie, - $ip, + $this->getVisitorIp(), $browserLang); $this->userSettingsInformation = array( @@ -652,7 +764,6 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface 'config_gears' => $plugin_Gears, 'config_silverlight' => $plugin_Silverlight, 'config_cookie' => $plugin_Cookie, - 'location_ip' => $ip, 'location_browser_lang' => $browserLang, ); diff --git a/core/Tracker/javascriptTag.tpl b/core/Tracker/javascriptTag.tpl index 6730f1b1b7..f0370aca3d 100644 --- a/core/Tracker/javascriptTag.tpl +++ b/core/Tracker/javascriptTag.tpl @@ -8,5 +8,5 @@ try { piwikTracker.trackPageView(); piwikTracker.enableLinkTracking(); } catch( err ) {} -</script><noscript><p><img src="http://{$piwikUrl}piwik.php?idsite={$idSite}" style="border:0" alt=""/></p></noscript> +</script><noscript><p><img src="http://{$piwikUrl}piwik.php?idsite={$idSite}" style="border:0" alt="" /></p></noscript> <!-- End Piwik Tag --> diff --git a/core/Translate.php b/core/Translate.php index 9d4b3a3efb..adc3379cf4 100644 --- a/core/Translate.php +++ b/core/Translate.php @@ -47,8 +47,12 @@ class Piwik_Translate { return; } - - require PIWIK_INCLUDE_PATH . '/lang/' . $language . '.php'; + $path = PIWIK_INCLUDE_PATH . '/lang/' . $language . '.php'; + if(!is_readable($path)) + { + throw new Exception(Piwik_TranslateException('General_ExceptionLanguageFileNotFound', array($language))); + } + require $path; $this->mergeTranslationArray($translations); $this->setLocale(); } @@ -74,9 +78,11 @@ class Piwik_Translate { return $language; } + Piwik_PostEvent('Translate.getLanguageToLoad', $language); - if(is_null($language) || empty($language)) + $language = Piwik_Common::getRequestVar('language', is_null($language) ? '' : $language, 'string'); + if(empty($language)) { $language = Zend_Registry::get('config')->General->default_language; } @@ -110,7 +116,7 @@ class Piwik_Translate $moduleRegex .= $module.'|'; } $moduleRegex = substr($moduleRegex, 0, -1); - $moduleRegex .= ')_([^_]+)_js$#i'; + $moduleRegex .= ')_.*_js$#i'; foreach($GLOBALS['Piwik_translations'] as $key => $value) { @@ -124,14 +130,22 @@ class Piwik_Translate 'for(var i in translations) { piwik_translations[i] = translations[i];} '; $js .= 'function _pk_translate(translationStringId) { '. 'if( typeof(piwik_translations[translationStringId]) != \'undefined\' ){ return piwik_translations[translationStringId]; }'. - 'return "The string "+translationStringId+" was not loaded in javascript. Make sure it is prefixed with _js";}'; + 'return "The string "+translationStringId+" was not loaded in javascript. Make sure it is suffixed with _js and that you called {loadJavascriptTranslations plugins=\'\$YOUR_PLUGIN_NAME\'} before your javascript code.";}'; return $js; } + /** + * Set locale + * + * @see http://php.net/setlocale + */ private function setLocale() { - setlocale(LC_ALL, $GLOBALS['Piwik_translations']['General_Locale']); + $locale = $GLOBALS['Piwik_translations']['General_Locale']; + $locale_variant = str_replace('UTF-8', 'UTF8', $locale); + setlocale(LC_ALL, $locale, $locale_variant); + setlocale(LC_CTYPE, ''); } } diff --git a/core/UpdateCheck.php b/core/UpdateCheck.php index c5ebe07874..8e7b09b581 100644 --- a/core/UpdateCheck.php +++ b/core/UpdateCheck.php @@ -17,7 +17,7 @@ */ class Piwik_UpdateCheck { - const CHECK_INTERVAL = 86400; + const CHECK_INTERVAL = 28800; // every 8 hours const LAST_TIME_CHECKED = 'UpdateCheck_LastTimeChecked'; const LATEST_VERSION = 'UpdateCheck_LatestVersion'; const PIWIK_HOST = 'http://api.piwik.org/1.0/getLatestVersion/'; @@ -25,29 +25,34 @@ class Piwik_UpdateCheck /** * Check for a newer version + * + * @param bool $force Force check */ - public static function check() + public static function check($force = false) { $lastTimeChecked = Piwik_GetOption(self::LAST_TIME_CHECKED); - if($lastTimeChecked === false + if($force || $lastTimeChecked === false || time() - self::CHECK_INTERVAL > $lastTimeChecked ) { + // set the time checked first, so that parallel Piwik requests don't all trigger the http requests + Piwik_SetOption(self::LAST_TIME_CHECKED, time(), $autoload = 1); $parameters = array( 'piwik_version' => Piwik_Version::VERSION, 'php_version' => phpversion(), 'url' => Piwik_Url::getCurrentUrlWithoutQueryString(), 'trigger' => Piwik_Common::getRequestVar('module','','string'), + 'timezone' => Piwik_SitesManager_API::getInstance()->getDefaultTimezone(), ); $url = self::PIWIK_HOST . "?" . http_build_query($parameters, '', '&'); $timeout = self::SOCKET_TIMEOUT; try { - $latestVersion = Piwik::sendHttpRequest($url, $timeout); + $latestVersion = Piwik_Http::sendHttpRequest($url, $timeout); Piwik_SetOption(self::LATEST_VERSION, $latestVersion); } catch(Exception $e) { // e.g., disable_functions = fsockopen; allow_url_open = Off + Piwik_SetOption(self::LATEST_VERSION, ''); } - Piwik_SetOption(self::LAST_TIME_CHECKED, time(), $autoload = 1); } } diff --git a/core/Updater.php b/core/Updater.php index 695826daa5..b45a16436f 100644 --- a/core/Updater.php +++ b/core/Updater.php @@ -10,9 +10,6 @@ * @package Piwik */ -// no direct access -defined('PIWIK_INCLUDE_PATH') or die; - /** * @see core/Option.php */ @@ -61,13 +58,23 @@ class Piwik_Updater public function recordComponentSuccessfullyUpdated($name, $version) { try { - Piwik_SetOption('version_'.$name, $version, $autoload = 1); + Piwik_SetOption($this->getNameInOptionTable($name), $version, $autoload = 1); } catch(Exception $e) { // case when the option table is not yet created (before 0.2.10) } } /** + * Returns the flag name to use in the option table to record current schema version + * @param string $name + * @return string + */ + private function getNameInOptionTable($name) + { + return 'version_'.$name; + } + + /** * Returns a list of components (core | plugin) that need to run through the upgrade process. * * @return array( componentName => array( file1 => version1, [...]), [...]) @@ -80,34 +87,79 @@ class Piwik_Updater } /** + * Component has a new version? + * + * @param string $componentName + * @return bool TRUE if compoment is to be updated; FALSE if not + */ + public function hasNewVersion($componentName) + { + return isset($this->componentsWithNewVersion) && + isset($this->componentsWithNewVersion[$componentName]); + } + + /** + * Returns the list of SQL queries that would be executed during the update + * + * @return array of SQL queries + */ + public function getSqlQueriesToExecute() + { + $queries = array(); + foreach($this->componentsWithUpdateFile as $componentName => $componentUpdateInfo) + { + foreach($componentUpdateInfo as $file => $fileVersion) + { + require_once $file; // prefixed by PIWIK_INCLUDE_PATH + + $className = $this->getUpdateClassName($componentName, $fileVersion); + if(class_exists($className, false)) + { + $queriesForComponent = call_user_func( array($className, 'getSql')); + foreach($queriesForComponent as $query => $error) { + $queries[] = $query.';'; + } + } + } + // unfortunately had to extract this query from the Piwik_Option class + $queries[] = 'UPDATE '.Piwik_Common::prefixTable('option').' + SET option_value = "' .$fileVersion.'" + WHERE option_name = "'. $this->getNameInOptionTable($componentName).'";'; + } + return $queries; + } + + private function getUpdateClassName($componentName, $fileVersion) + { + $suffix = strtolower(str_replace(array('-','.'), '_', $fileVersion)); + if($componentName == 'core') + { + return 'Piwik_Updates_' . $suffix; + } + return 'Piwik_'. $componentName .'_Updates_' . $suffix; + } + + /** * Update the named component * - * @param string $name + * @param string $componentName 'core', or plugin name * @return array of warning strings if applicable */ - public function update($name) + public function update($componentName) { $warningMessages = array(); - foreach($this->componentsWithUpdateFile[$name] as $file => $fileVersion) + foreach($this->componentsWithUpdateFile[$componentName] as $file => $fileVersion) { try { require_once $file; // prefixed by PIWIK_INCLUDE_PATH - if($name == 'core') - { - $className = 'Piwik_Updates_' . str_replace('.', '_', $fileVersion); - } - else - { - $className = 'Piwik_'. $name .'_Updates_' . str_replace('.', '_', $fileVersion); - } - + $className = $this->getUpdateClassName($componentName, $fileVersion); if(class_exists($className, false)) { call_user_func( array($className, 'update') ); } - $this->recordComponentSuccessfullyUpdated($name, $fileVersion); + $this->recordComponentSuccessfullyUpdated($componentName, $fileVersion); } catch( Piwik_Updater_UpdateErrorException $e) { throw $e; } catch( Exception $e) { @@ -116,7 +168,7 @@ class Piwik_Updater } // to debug, create core/Updates/X.php, update the core/Version.php, throw an Exception in the try, and comment the following line - $this->recordComponentSuccessfullyUpdated($name, $this->componentsWithNewVersion[$name][self::INDEX_NEW_VERSION]); + $this->recordComponentSuccessfullyUpdated($componentName, $this->componentsWithNewVersion[$componentName][self::INDEX_NEW_VERSION]); return $warningMessages; } @@ -145,12 +197,16 @@ class Piwik_Updater $files = glob( $pathToUpdates ); if($files === false) { - continue; + $files = array(); } + foreach( $files as $file) { $fileVersion = basename($file, '.php'); - if(version_compare($currentVersion, $fileVersion) == -1) + if( // if the update is from a newer version + version_compare($currentVersion, $fileVersion) == -1 + // but we don't execute updates from non existing future releases + && version_compare($fileVersion, $newVersion) <= 0) { $componentsWithUpdateFile[$name][$file] = $fileVersion; } @@ -246,7 +302,8 @@ class Piwik_Updater try { Piwik_Exec( $update ); } catch(Exception $e) { - if(($ignoreError === false) || !Zend_Registry::get('db')->isErrNo($e, $ignoreError)) + if(($ignoreError === false) + || !Zend_Registry::get('db')->isErrNo($e, $ignoreError)) { $message = $file .":\nError trying to execute the query '". $update ."'.\nThe error was: ". $e->getMessage(); throw new Piwik_Updater_UpdateErrorException($message); diff --git a/core/iUpdate.php b/core/Updates.php index 5b9888558c..3d5e4df537 100644 --- a/core/iUpdate.php +++ b/core/Updates.php @@ -11,15 +11,26 @@ */ /** - * Interface to be implemented by update scripts + * Abstract class for update scripts * * @example core/Updates/0.4.2.php * @package Piwik */ -interface Piwik_iUpdate +abstract class Piwik_Updates { /** + * Return SQL to be executed in this update + * + * @param string Adapter name + * @return array + */ + static function getSql($adapter = 'PDO_MYSQL') + { + return array(); + } + + /** * Incremental version update */ - static function update(); + abstract static function update(); } diff --git a/core/Updates/0.2.10.php b/core/Updates/0.2.10.php index ef64070237..6caf21ae46 100644 --- a/core/Updates/0.2.10.php +++ b/core/Updates/0.2.10.php @@ -13,14 +13,46 @@ /** * @package Updates */ -class Piwik_Updates_0_2_10 implements Piwik_iUpdate +class Piwik_Updates_0_2_10 extends Piwik_Updates { - static function update() + static function getSql($adapter = 'PDO_MYSQL') { $tables = Piwik::getTablesCreateSql(); - Piwik_Updater::updateDatabase(__FILE__, array( + + return array( $tables['option'] => false, - )); + + // 0.1.7 [463] + 'ALTER IGNORE TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + CHANGE `location_provider` `location_provider` VARCHAR( 100 ) DEFAULT NULL' => '1054', + + // 0.1.7 [470] + 'ALTER TABLE `'. Piwik_Common::prefixTable('logger_api_call') .'` + CHANGE `parameter_names_default_values` `parameter_names_default_values` TEXT, + CHANGE `parameter_values` `parameter_values` TEXT, + CHANGE `returned_value` `returned_value` TEXT' => false, + 'ALTER TABLE `'. Piwik_Common::prefixTable('logger_error') .'` + CHANGE `message` `message` TEXT' => false, + 'ALTER TABLE `'. Piwik_Common::prefixTable('logger_exception') .'` + CHANGE `message` `message` TEXT' => false, + 'ALTER TABLE `'. Piwik_Common::prefixTable('logger_message') .'` + CHANGE `message` `message` TEXT' => false, + + // 0.2.2 [489] + 'ALTER IGNORE TABLE `'. Piwik_Common::prefixTable('site') .'` + CHANGE `feedburnerName` `feedburnerName` VARCHAR( 100 ) DEFAULT NULL' => '1054', + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + + $obsoleteFile = '/plugins/ExamplePlugin/API.php'; + if(file_exists(PIWIK_INCLUDE_PATH . $obsoleteFile)) + { + @unlink(PIWIK_INCLUDE_PATH . $obsoleteFile); + } $obsoleteDirectories = array( '/plugins/AdminHome', diff --git a/core/Updates/0.2.12.php b/core/Updates/0.2.12.php index 5227f68586..3a4b33ac8d 100644 --- a/core/Updates/0.2.12.php +++ b/core/Updates/0.2.12.php @@ -13,15 +13,24 @@ /** * @package Updates */ -class Piwik_Updates_0_2_12 implements Piwik_iUpdate +class Piwik_Updates_0_2_12 extends Piwik_Updates { - static function update() + static function getSql($adapter = 'PDO_MYSQL') { - Piwik_Updater::updateDatabase(__FILE__, array( - 'ALTER TABLE `'. Piwik::prefixTable('site') .'` + return array( + 'ALTER TABLE `'. Piwik_Common::prefixTable('site') .'` CHANGE `ts_created` `ts_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL' => false, - 'ALTER TABLE `'. Piwik::prefixTable('log_visit') .'` + 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` DROP `config_color_depth`' => false, - )); + + // 0.2.12 [673] + // Note: requires INDEX privilege + 'DROP INDEX index_idaction ON `'. Piwik_Common::prefixTable('log_action') .'`' => '1091', + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.2.13.php b/core/Updates/0.2.13.php index fb8a875f54..11f49ff709 100644 --- a/core/Updates/0.2.13.php +++ b/core/Updates/0.2.13.php @@ -13,14 +13,20 @@ /** * @package Updates */ -class Piwik_Updates_0_2_13 implements Piwik_iUpdate +class Piwik_Updates_0_2_13 extends Piwik_Updates { - static function update() + static function getSql($adapter = 'PDO_MYSQL') { $tables = Piwik::getTablesCreateSql(); - Piwik_Updater::updateDatabase(__FILE__, array( - 'DROP TABLE IF EXISTS `'. Piwik::prefixTable('option') .'`' => false, + + return array( + 'DROP TABLE IF EXISTS `'. Piwik_Common::prefixTable('option') .'`' => false, $tables['option'] => false, - )); + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.2.24.php b/core/Updates/0.2.24.php index a5a19837e6..81a0e4df22 100644 --- a/core/Updates/0.2.24.php +++ b/core/Updates/0.2.24.php @@ -13,17 +13,22 @@ /** * @package Updates */ -class Piwik_Updates_0_2_24 implements Piwik_iUpdate +class Piwik_Updates_0_2_24 extends Piwik_Updates { - static function update() + static function getSql($adapter = 'PDO_MYSQL') { - Piwik_Updater::updateDatabase(__FILE__, array( + return array( 'CREATE INDEX index_type_name - ON '. Piwik::prefixTable('log_action') .' (type, name(15))' => false, + ON '. Piwik_Common::prefixTable('log_action') .' (type, name(15))' => false, 'CREATE INDEX index_idsite_date - ON '. Piwik::prefixTable('log_visit') .' (idsite, visit_server_date)' => false, - 'DROP INDEX index_idsite ON '. Piwik::prefixTable('log_visit') => false, - 'DROP INDEX index_visit_server_date ON '. Piwik::prefixTable('log_visit') => false, - )); + ON '. Piwik_Common::prefixTable('log_visit') .' (idsite, visit_server_date)' => false, + 'DROP INDEX index_idsite ON '. Piwik_Common::prefixTable('log_visit') => false, + 'DROP INDEX index_visit_server_date ON '. Piwik_Common::prefixTable('log_visit') => false, + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.2.27.php b/core/Updates/0.2.27.php index 3424216245..57cf3465d6 100644 --- a/core/Updates/0.2.27.php +++ b/core/Updates/0.2.27.php @@ -13,12 +13,17 @@ /** * @package Updates */ -class Piwik_Updates_0_2_27 implements Piwik_iUpdate +class Piwik_Updates_0_2_27 extends Piwik_Updates { - static function update() + static function getSql($adapter = 'PDO_MYSQL') { - $sqlarray[ 'ALTER TABLE `'. Piwik::prefixTable('log_visit') .'` - ADD `visit_goal_converted` VARCHAR( 1 ) NOT NULL AFTER `visit_total_time`' ] = false; + $sqlarray = array( + 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + ADD `visit_goal_converted` VARCHAR( 1 ) NOT NULL AFTER `visit_total_time`' => false, + // 0.2.27 [826] + 'ALTER IGNORE TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + CHANGE `visit_goal_converted` `visit_goal_converted` TINYINT(1) NOT NULL' => false, + ); $tables = Piwik::getTablesCreateSql(); $sqlarray[ $tables['log_conversion'] ] = false; @@ -33,6 +38,11 @@ class Piwik_Updates_0_2_27 implements Piwik_iUpdate } } - Piwik_Updater::updateDatabase(__FILE__, $sqlarray); + return $sqlarray; + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.2.32.php b/core/Updates/0.2.32.php new file mode 100644 index 0000000000..5594b6f521 --- /dev/null +++ b/core/Updates/0.2.32.php @@ -0,0 +1,37 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Updates + */ + +/** + * @package Updates + */ +class Piwik_Updates_0_2_32 extends Piwik_Updates +{ + static function getSql($adapter = 'PDO_MYSQL') + { + return array( + // 0.2.32 [941] + 'ALTER TABLE `'. Piwik_Common::prefixTable('access') .'` + CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => false, + 'ALTER TABLE `'. Piwik_Common::prefixTable('user') .'` + CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => false, + 'ALTER TABLE `'. Piwik_Common::prefixTable('user_dashboard') .'` + CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => '1146', + 'ALTER TABLE `'. Piwik_Common::prefixTable('user_language') .'` + CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => '1146', + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } +} diff --git a/core/Updates/0.2.33.php b/core/Updates/0.2.33.php index ce936f5731..ed94e78fb8 100644 --- a/core/Updates/0.2.33.php +++ b/core/Updates/0.2.33.php @@ -13,10 +13,18 @@ /** * @package Updates */ -class Piwik_Updates_0_2_33 implements Piwik_iUpdate +class Piwik_Updates_0_2_33 extends Piwik_Updates { - static function update() + static function getSql($adapter = 'PDO_MYSQL') { + $sqlarray = array( + // 0.2.33 [1020] + 'ALTER TABLE `'. Piwik_Common::prefixTable('user_dashboard') .'` + CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci ' => '1146', + 'ALTER TABLE `'. Piwik_Common::prefixTable('user_language') .'` + CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci ' => '1146', + ); + // alter table to set the utf8 collation $tablesToAlter = Piwik::getTablesInstalled(true); foreach($tablesToAlter as $table) { @@ -24,6 +32,11 @@ class Piwik_Updates_0_2_33 implements Piwik_iUpdate CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci ' ] = false; } - Piwik_Updater::updateDatabase(__FILE__, $sqlarray); + return $sqlarray; + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.2.34.php b/core/Updates/0.2.34.php index a36b284589..ca7ae16211 100644 --- a/core/Updates/0.2.34.php +++ b/core/Updates/0.2.34.php @@ -13,13 +13,13 @@ /** * @package Updates */ -class Piwik_Updates_0_2_34 implements Piwik_iUpdate +class Piwik_Updates_0_2_34 extends Piwik_Updates { static function update() { // force regeneration of cache files following #648 Piwik::setUserIsSuperUser(); - $allSiteIds = Piwik_SitesManager_API::getAllSitesId(); + $allSiteIds = Piwik_SitesManager_API::getInstance()->getAllSitesId(); Piwik_Common::regenerateCacheWebsiteAttributes($allSiteIds); } } diff --git a/core/Updates/0.2.35.php b/core/Updates/0.2.35.php index 3fbeb4ab87..4ceadcf30d 100644 --- a/core/Updates/0.2.35.php +++ b/core/Updates/0.2.35.php @@ -13,13 +13,18 @@ /** * @package Updates */ -class Piwik_Updates_0_2_35 implements Piwik_iUpdate +class Piwik_Updates_0_2_35 extends Piwik_Updates { - static function update() + static function getSql($adapter = 'PDO_MYSQL') { - Piwik_Updater::updateDatabase(__FILE__, array( - 'ALTER TABLE `'. Piwik::prefixTable('user_dashboard') .'` + return array( + 'ALTER TABLE `'. Piwik_Common::prefixTable('user_dashboard') .'` CHANGE `layout` `layout` TEXT NOT NULL' => false, - )); + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.2.37.php b/core/Updates/0.2.37.php index ed1a36160d..51d77a6c32 100644 --- a/core/Updates/0.2.37.php +++ b/core/Updates/0.2.37.php @@ -13,14 +13,19 @@ /** * @package Updates */ -class Piwik_Updates_0_2_37 implements Piwik_iUpdate +class Piwik_Updates_0_2_37 extends Piwik_Updates { - static function update() + static function getSql($adapter = 'PDO_MYSQL') { - Piwik_Updater::updateDatabase(__FILE__, array( - 'DELETE FROM `'. Piwik::prefixTable('user_dashboard') ."` + return array( + 'DELETE FROM `'. Piwik_Common::prefixTable('user_dashboard') ."` WHERE layout LIKE '%.getLastVisitsGraph%' OR layout LIKE '%.getLastVisitsReturningGraph%'" => false, - )); + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.4.1.php b/core/Updates/0.4.1.php index 93fe25064c..ec1995e399 100644 --- a/core/Updates/0.4.1.php +++ b/core/Updates/0.4.1.php @@ -13,15 +13,20 @@ /** * @package Updates */ -class Piwik_Updates_0_4_1 implements Piwik_iUpdate +class Piwik_Updates_0_4_1 extends Piwik_Updates { - static function update() + static function getSql($adapter = 'PDO_MYSQL') { - Piwik_Updater::updateDatabase(__FILE__, array( - 'ALTER TABLE `'. Piwik::prefixTable('log_conversion') .'` + return array( + 'ALTER TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` CHANGE `idlink_va` `idlink_va` INT(11) DEFAULT NULL' => false, - 'ALTER TABLE `'. Piwik::prefixTable('log_conversion') .'` - CHANGE `idaction` `idaction` INT(11) DEFAULT NULL' => false, - )); + 'ALTER TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` + CHANGE `idaction` `idaction` INT(11) DEFAULT NULL' => '1054', + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.4.2.php b/core/Updates/0.4.2.php index ef91786033..9be470e46a 100644 --- a/core/Updates/0.4.2.php +++ b/core/Updates/0.4.2.php @@ -13,19 +13,24 @@ /** * @package Updates */ -class Piwik_Updates_0_4_2 implements Piwik_iUpdate +class Piwik_Updates_0_4_2 extends Piwik_Updates { - // when restoring (possibly) previousy dropped columns, ignore mysql code error 1060: duplicate column - static function update() + static function getSql($adapter = 'PDO_MYSQL') { - Piwik_Updater::updateDatabase(__FILE__, array( - 'ALTER TABLE `'. Piwik::prefixTable('log_visit') .'` + return array( + 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` ADD `config_java` TINYINT(1) NOT NULL AFTER `config_flash`' => '1060', - 'ALTER TABLE `'. Piwik::prefixTable('log_visit') .'` + 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` ADD `config_quicktime` TINYINT(1) NOT NULL AFTER `config_director`' => '1060', - 'ALTER TABLE `'. Piwik::prefixTable('log_visit') .'` + 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` ADD `config_gears` TINYINT(1) NOT NULL AFTER `config_windowsmedia`, ADD `config_silverlight` TINYINT(1) NOT NULL AFTER `config_gears`' => false, - )); + ); + } + + // when restoring (possibly) previousy dropped columns, ignore mysql code error 1060: duplicate column + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.4.3.php b/core/Updates/0.4.3.php deleted file mode 100644 index b4524186a1..0000000000 --- a/core/Updates/0.4.3.php +++ /dev/null @@ -1,65 +0,0 @@ -<?php -/** - * Piwik - Open source web analytics - * - * @link http://piwik.org - * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later - * @version $Id$ - * - * @category Piwik - * @package Updates - */ - -/** - * @package Updates - */ -class Piwik_Updates_0_4_3 implements Piwik_iUpdate -{ - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, array( - // 0.1.7 [463] - 'ALTER IGNORE TABLE `'. Piwik::prefixTable('log_visit') .'` - CHANGE `location_provider` `location_provider` VARCHAR( 100 ) DEFAULT NULL' => '1054', - // 0.1.7 [470] - 'ALTER TABLE `'. Piwik::prefixTable('logger_api_call') .'` - CHANGE `parameter_names_default_values` `parameter_names_default_values` TEXT, - CHANGE `parameter_values` `parameter_values` TEXT, - CHANGE `returned_value` `returned_value` TEXT' => false, - 'ALTER TABLE `'. Piwik::prefixTable('logger_error') .'` - CHANGE `message` `message` TEXT' => false, - 'ALTER TABLE `'. Piwik::prefixTable('logger_exception') .'` - CHANGE `message` `message` TEXT' => false, - 'ALTER TABLE `'. Piwik::prefixTable('logger_message') .'` - CHANGE `message` `message` TEXT' => false, - // 0.2.2 [489] - 'ALTER IGNORE TABLE `'. Piwik::prefixTable('site') .'` - CHANGE `feedburnerName` `feedburnerName` VARCHAR( 100 ) DEFAULT NULL' => '1054', - // 0.2.12 [673] - // Note: requires INDEX privilege - 'DROP INDEX index_idaction ON `'. Piwik::prefixTable('log_action') .'`' => '1091', - // 0.2.27 [826] - 'ALTER IGNORE TABLE `'. Piwik::prefixTable('log_visit') .'` - CHANGE `visit_goal_converted` `visit_goal_converted` TINYINT(1) NOT NULL' => false, - // 0.2.32 [941] - 'ALTER TABLE `'. Piwik::prefixTable('access') .'` - CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => false, - 'ALTER TABLE `'. Piwik::prefixTable('user') .'` - CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => false, - 'ALTER TABLE `'. Piwik::prefixTable('user_dashboard') .'` - CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => '1146', - 'ALTER TABLE `'. Piwik::prefixTable('user_language') .'` - CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => '1146', - // 0.2.33 [1020] - 'ALTER TABLE `'. Piwik::prefixTable('user_dashboard') .'` - CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci ' => '1146', - 'ALTER TABLE `'. Piwik::prefixTable('user_language') .'` - CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci ' => '1146', - // 0.4 [1140] - 'ALTER TABLE `'. Piwik::prefixTable('log_visit') .'` - CHANGE `location_ip` `location_ip` BIGINT UNSIGNED NOT NULL' => false, - 'ALTER TABLE `'. Piwik::prefixTable('logger_api_call') .'` - CHANGE `caller_ip` `caller_ip` BIGINT UNSIGNED' => false, - )); - } -} diff --git a/core/Updates/0.4.4.php b/core/Updates/0.4.4.php index e6c986a0b4..e29c07eed8 100644 --- a/core/Updates/0.4.4.php +++ b/core/Updates/0.4.4.php @@ -13,14 +13,18 @@ /** * @package Updates */ -class Piwik_Updates_0_4_4 implements Piwik_iUpdate +class Piwik_Updates_0_4_4 extends Piwik_Updates { static function update() { - $obsoleteFile = '/libs/open-flash-chart/php-ofc-library/ofc_upload_image.php'; - if(file_exists(PIWIK_DOCUMENT_ROOT . $obsoleteFile)) + $obsoleteFile = PIWIK_DOCUMENT_ROOT . '/libs/open-flash-chart/php-ofc-library/ofc_upload_image.php'; + if(file_exists($obsoleteFile)) { - @unlink(PIWIK_DOCUMENT_ROOT . $obsoleteFile); + $rc = @unlink($obsoleteFile); + if(!$rc) + { + throw new Exception(Piwik_TranslateException('General_ExceptionUndeletableFile', array($obsoleteFile))); + } } } } diff --git a/core/Updates/0.4.php b/core/Updates/0.4.php index a0ac958c11..cdd9911bdd 100644 --- a/core/Updates/0.4.php +++ b/core/Updates/0.4.php @@ -13,19 +13,25 @@ /** * @package Updates */ -class Piwik_Updates_0_4 implements Piwik_iUpdate +class Piwik_Updates_0_4 extends Piwik_Updates { - static function update() + static function getSql($adapter = 'PDO_MYSQL') { - Piwik_Updater::updateDatabase(__FILE__, array( - 'UPDATE `'. Piwik::prefixTable('log_visit') .'` + return array( + // 0.4 [1140] + 'UPDATE `'. Piwik_Common::prefixTable('log_visit') .'` SET location_ip=location_ip+CAST(POW(2,32) AS UNSIGNED) WHERE location_ip < 0' => false, - 'ALTER TABLE `'. Piwik::prefixTable('log_visit') .'` + 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` CHANGE `location_ip` `location_ip` BIGINT UNSIGNED NOT NULL' => false, - 'UPDATE `'. Piwik::prefixTable('logger_api_call') .'` + 'UPDATE `'. Piwik_Common::prefixTable('logger_api_call') .'` SET caller_ip=caller_ip+CAST(POW(2,32) AS UNSIGNED) WHERE caller_ip < 0' => false, - 'ALTER TABLE `'. Piwik::prefixTable('logger_api_call') .'` + 'ALTER TABLE `'. Piwik_Common::prefixTable('logger_api_call') .'` CHANGE `caller_ip` `caller_ip` BIGINT UNSIGNED' => false, - )); + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.5.4.php b/core/Updates/0.5.4.php new file mode 100644 index 0000000000..859301e3ff --- /dev/null +++ b/core/Updates/0.5.4.php @@ -0,0 +1,75 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Updates + */ + +/** + * @package Updates + */ +class Piwik_Updates_0_5_4 extends Piwik_Updates +{ + static function getSql($adapter = 'PDO_MYSQL') + { + return array( + 'ALTER TABLE `'. Piwik_Common::prefixTable('log_action') .'` + CHANGE `name` `name` TEXT' => false, + ); + } + + static function update() + { + $config = Zend_Registry::get('config'); + $salt = Piwik_Common::generateUniqId(); + if(!isset($config->superuser->salt)) + { + try { + if(is_writable( Piwik_Config::getDefaultUserConfigPath() )) + { + $superuser_info = $config->superuser->toArray(); + $superuser_info['salt'] = $salt; + $config->superuser = $superuser_info; + + $config->__destruct(); + Piwik::createConfigObject(); + } + else + { + throw new Exception('mandatory update failed'); + } + } catch(Exception $e) { + throw new Piwik_Updater_UpdateErrorException("Please edit your config/config.ini.php file and add below <code>[superuser]</code> the following line: <br /><code>salt = $salt</code>"); + } + } + + $config = Zend_Registry::get('config'); + $plugins = $config->Plugins->toArray(); + if(!in_array('MultiSites', $plugins)) + { + try { + if(is_writable( Piwik_Config::getDefaultUserConfigPath() )) + { + $plugins[] = 'MultiSites'; + $config->Plugins = $plugins; + + $config->__destruct(); + Piwik::createConfigObject(); + } + else + { + throw new Exception('optional update failed'); + } + } catch(Exception $e) { + throw new Exception("You can now enable the new MultiSites plugin in the Plugins screen in the Piwik admin!"); + } + } + + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } +} diff --git a/core/Updates/0.5.5.php b/core/Updates/0.5.5.php new file mode 100644 index 0000000000..a82cf44cd7 --- /dev/null +++ b/core/Updates/0.5.5.php @@ -0,0 +1,46 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Updates + */ + +/** + * @package Updates + */ +class Piwik_Updates_0_5_5 extends Piwik_Updates +{ + static function getSql($adapter = 'PDO_MYSQL') + { + $sqlarray = array( + 'DROP INDEX index_idsite_date ON ' . Piwik_Common::prefixTable('log_visit') => '1091', + 'CREATE INDEX index_idsite_date_config ON ' . Piwik_Common::prefixTable('log_visit') . ' (idsite, visit_server_date, config_md5config(8))' => '1061', + ); + + $tables = Piwik::getTablesInstalled(); + foreach($tables as $tableName) + { + if(preg_match('/archive_/', $tableName) == 1) + { + $sqlarray[ 'DROP INDEX index_all ON '. $tableName ] = '1091'; + } + if(preg_match('/archive_numeric_/', $tableName) == 1) + { + $sqlarray[ 'CREATE INDEX index_idsite_dates_period ON '. $tableName .' (idsite, date1, date2, period)' ] = '1061'; + } + } + + return $sqlarray; + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + + } +} diff --git a/core/Updates/0.5.php b/core/Updates/0.5.php index 108e651617..5d23f8a811 100644 --- a/core/Updates/0.5.php +++ b/core/Updates/0.5.php @@ -13,21 +13,26 @@ /** * @package Updates */ -class Piwik_Updates_0_5 implements Piwik_iUpdate +class Piwik_Updates_0_5 extends Piwik_Updates { + static function getSql($adapter = 'PDO_MYSQL') + { + return array( + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_action') . ' ADD COLUMN `hash` INTEGER(10) UNSIGNED NOT NULL AFTER `name`;' => '1060', + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_visit') . ' CHANGE visit_exit_idaction visit_exit_idaction_url INTEGER(11) NOT NULL;' => '1054', + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_visit') . ' CHANGE visit_entry_idaction visit_entry_idaction_url INTEGER(11) NOT NULL;' => '1054', + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_link_visit_action') . ' CHANGE `idaction_ref` `idaction_url_ref` INTEGER(10) UNSIGNED NOT NULL;' => '1054', + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_link_visit_action') . ' CHANGE `idaction` `idaction_url` INTEGER(10) UNSIGNED NOT NULL;' => '1054', + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_link_visit_action') . ' ADD COLUMN `idaction_name` INTEGER(10) UNSIGNED AFTER `idaction_url_ref`;' => '1060', + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_conversion') . ' CHANGE `idaction` `idaction_url` INTEGER(11) UNSIGNED NOT NULL;' => '1054', + 'UPDATE ' . Piwik_Common::prefixTable('log_action') . ' SET `hash` = CRC32(name);' => false, + 'CREATE INDEX index_type_hash ON ' . Piwik_Common::prefixTable('log_action') . ' (type, hash);' => '1061', + 'DROP INDEX index_type_name ON ' . Piwik_Common::prefixTable('log_action') . ';' => '1091', + ); + } + static function update() { - Piwik_Updater::updateDatabase(__FILE__, array( - 'ALTER TABLE ' . Piwik::prefixTable('log_action'). ' ADD COLUMN `hash` INTEGER(10) UNSIGNED NOT NULL AFTER `name`;' => false, - 'UPDATE '. Piwik::prefixTable('log_action'). ' SET `hash` = CRC32(name);' => false, - 'CREATE INDEX index_type_hash ON '. Piwik::prefixTable('log_action') .' (type, hash);' => false, - 'DROP INDEX index_type_name ON '. Piwik::prefixTable('log_action') .';' => false, - 'ALTER TABLE '. Piwik::prefixTable('log_visit') .' CHANGE visit_exit_idaction visit_exit_idaction_url INTEGER(11) NOT NULL;' => false, - 'ALTER TABLE '. Piwik::prefixTable('log_visit') .' CHANGE visit_entry_idaction visit_entry_idaction_url INTEGER(11) NOT NULL;' => false, - 'ALTER TABLE ' . Piwik::prefixTable('log_link_visit_action'). ' CHANGE `idaction_ref` `idaction_url_ref` INTEGER(10) UNSIGNED NOT NULL;' => false, - 'ALTER TABLE ' . Piwik::prefixTable('log_link_visit_action'). ' CHANGE `idaction` `idaction_url` INTEGER(10) UNSIGNED NOT NULL;' => false, - 'ALTER TABLE ' . Piwik::prefixTable('log_link_visit_action'). ' ADD COLUMN `idaction_name` INTEGER(10) UNSIGNED AFTER `idaction_url_ref`;' => false, - 'ALTER TABLE ' . Piwik::prefixTable('log_conversion'). ' CHANGE `idaction` `idaction_url` INTEGER(11) UNSIGNED NOT NULL;' => false, - )); + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); } } diff --git a/core/Updates/0.6-rc1.php b/core/Updates/0.6-rc1.php new file mode 100644 index 0000000000..14929092ee --- /dev/null +++ b/core/Updates/0.6-rc1.php @@ -0,0 +1,68 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Updates + */ + +/** + * @package Updates + */ +class Piwik_Updates_0_6_rc1 extends Piwik_Updates +{ + static function getSql($adapter = 'PDO_MYSQL') + { + $defaultTimezone = 'UTC'; + $defaultCurrency = 'USD'; + return array( + 'ALTER TABLE ' . Piwik_Common::prefixTable('user') . ' CHANGE date_registered date_registered TIMESTAMP NULL' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('site') . ' CHANGE ts_created ts_created TIMESTAMP NULL' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('site') . ' ADD `timezone` VARCHAR( 50 ) NOT NULL AFTER `ts_created` ;' => false, + 'UPDATE ' . Piwik_Common::prefixTable('site') . ' SET `timezone` = "'.$defaultTimezone.'";' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('site') . ' ADD currency CHAR( 3 ) NOT NULL AFTER `timezone` ;' => false, + 'UPDATE ' . Piwik_Common::prefixTable('site') . ' SET `currency` = "'.$defaultCurrency.'";' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('site') . ' ADD `excluded_ips` TEXT NOT NULL AFTER `currency` ;' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('site') . ' ADD excluded_parameters VARCHAR( 255 ) NOT NULL AFTER `excluded_ips` ;' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_visit') . ' ADD INDEX `index_idsite_datetime_config` ( `idsite` , `visit_last_action_time` , `config_md5config` ( 8 ) ) ;' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_visit') . ' ADD INDEX index_idsite_idvisit (idsite, idvisit) ;' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_conversion') . ' DROP INDEX index_idsite_date' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_conversion') . ' DROP visit_server_date;' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_conversion') . ' ADD INDEX index_idsite_datetime ( `idsite` , `server_time` )' => false, + ); + } + + static function update() + { + // first we disable the plugins and keep an array of warnings messages + $pluginsToDisableMessage = array( + 'SearchEnginePosition' => "SearchEnginePosition plugin was disabled, because it is not compatible with the new Piwik 0.6. \n You can download the latest version of the plugin, compatible with Piwik 0.6.\n<a target='_blank' href='misc/redirectToUrl.php?url=http://dev.piwik.org/trac/ticket/502'>Click here.</a>", + 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 0.6. \nYou can download the latest version of the plugin, compatible with Piwik 0.6.\n<a target='_blank' href='misc/redirectToUrl.php?url=http://dev.piwik.org/trac/ticket/45'>Click here.</a>" + ); + $disabledPlugins = array(); + foreach($pluginsToDisableMessage as $pluginToDisable => $warningMessage) + { + if(Piwik_PluginsManager::getInstance()->isPluginActivated($pluginToDisable)) + { + Piwik_PluginsManager::getInstance()->deactivatePlugin($pluginToDisable); + $disabledPlugins[] = $warningMessage; + } + } + + // Run the SQL + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + + // Outputs warning message, pointing users to the plugin download page + if(!empty($disabledPlugins)) + { + throw new Exception("The following plugins were disabled during the upgrade:" + ."<ul><li>" . + implode('</li><li>', $disabledPlugins) . + "</li></ul>"); + } + } +} diff --git a/core/Updates/0.6.2.php b/core/Updates/0.6.2.php new file mode 100644 index 0000000000..5c564df099 --- /dev/null +++ b/core/Updates/0.6.2.php @@ -0,0 +1,47 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Updates + */ + +/** + * @package Updates + */ +class Piwik_Updates_0_6_2 extends Piwik_Updates +{ + static function update() + { + $obsoleteFiles = array( + PIWIK_INCLUDE_PATH . '/core/Db/Mysqli.php', + ); + foreach($obsoleteFiles as $obsoleteFile) + { + if(file_exists($obsoleteFile)) + { + @unlink($obsoleteFile); + } + } + + $obsoleteDirectories = array( + PIWIK_INCLUDE_PATH . '/core/Db/Pdo', + ); + foreach($obsoleteDirectories as $dir) + { + if(file_exists($dir)) + { + Piwik::unlinkRecursive($dir, true); + } + } + + // force regeneration of cache files + Piwik::setUserIsSuperUser(); + $allSiteIds = Piwik_SitesManager_API::getInstance()->getAllSitesId(); + Piwik_Common::regenerateCacheWebsiteAttributes($allSiteIds); + } +} diff --git a/core/Updates/0.6.3.php b/core/Updates/0.6.3.php new file mode 100644 index 0000000000..d4e5a8b38a --- /dev/null +++ b/core/Updates/0.6.3.php @@ -0,0 +1,54 @@ +<?php +/** + * Piwik - Open source web analytics + * + * @link http://piwik.org + * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later + * @version $Id$ + * + * @category Piwik + * @package Updates + */ + +/** + * @package Updates + */ +class Piwik_Updates_0_6_3 extends Piwik_Updates +{ + static function getSql($adapter = 'PDO_MYSQL') + { + return array( + 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + CHANGE `location_ip` `location_ip` INT UNSIGNED NOT NULL' => false, + 'ALTER TABLE `'. Piwik_Common::prefixTable('logger_api_call') .'` + CHANGE `caller_ip` `caller_ip` INT UNSIGNED' => false, + ); + } + + static function update() + { + $config = Zend_Registry::get('config'); + $dbInfos = $config->database->toArray(); + if(!isset($dbInfos['schema'])) + { + try { + if(is_writable( Piwik_Config::getDefaultUserConfigPath() )) + { + $dbInfos['schema'] = 'Myisam'; + $config->database = $dbInfos; + + $config->__destruct(); + Piwik::createConfigObject(); + } + else + { + throw new Exception('mandatory update failed'); + } + } catch(Exception $e) { + throw new Piwik_Updater_UpdateErrorException("Please edit your config/config.ini.php file and add below <code>[database]</code> the following line: <br /><code>schema = Myisam</code>"); + } + } + + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } +} diff --git a/core/Url.php b/core/Url.php index 7ba48bbc99..cc6164ac26 100644 --- a/core/Url.php +++ b/core/Url.php @@ -1,11 +1,11 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html Gpl v3 or later * @version $Id$ - * + * * @category Piwik * @package Piwik */ @@ -26,11 +26,12 @@ class Piwik_Url */ static public function getCurrentUrl() { - return self::getCurrentHost() - . self::getCurrentScriptName() - . self::getCurrentQueryString(); + return self::getCurrentScheme() . '://' + . self::getCurrentHost() + . self::getCurrentScriptName() + . self::getCurrentQueryString(); } - + /** * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" * will return "http://example.org/dir1/dir2/index.php" @@ -39,10 +40,11 @@ class Piwik_Url */ static public function getCurrentUrlWithoutQueryString() { - return self::getCurrentHost() - . self::getCurrentScriptName() ; + return self::getCurrentScheme() . '://' + . self::getCurrentHost() + . self::getCurrentScriptName(); } - + /** * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" * will return "http://example.org/dir1/dir2/" @@ -51,9 +53,9 @@ class Piwik_Url */ static public function getCurrentUrlWithoutFileName() { - $host = self::getCurrentHost(); - $urlDir = self::getCurrentScriptPath(); - return $host.$urlDir; + return self::getCurrentScheme() . '://' + . self::getCurrentHost() + . self::getCurrentScriptPath(); } /** @@ -76,7 +78,7 @@ class Piwik_Url } return $urlDir; } - + /** * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" * will return "/dir1/dir2/index.php" @@ -106,41 +108,56 @@ class Piwik_Url { $url = $_SERVER['SCRIPT_NAME']; } + + if($url[0] !== '/') + { + $url = '/' . $url; + } return $url; } /** - * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * will return "http://example.org" + * If the current URL is 'http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * will return 'http' * - * @return string + * @return string 'https' or 'http' */ - static public function getCurrentHost() + static public function getCurrentScheme() { if(isset($_SERVER['HTTPS']) - && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true) + && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true) ) { - $url = 'https'; + $scheme = 'https'; } - else + else { - $url = 'http'; + $scheme = 'http'; } - - $url .= '://'; - - if(isset($_SERVER['HTTP_HOST'])) + return $scheme; + } + + /** + * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * will return "http://example.org" + * + * @return string + */ + static public function getCurrentHost() + { + if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) { - $url .= $_SERVER['HTTP_HOST']; + return Piwik_Common::getFirstIpFromList($_SERVER['HTTP_X_FORWARDED_HOST']); } - else + + if(isset($_SERVER['HTTP_HOST'])) { - $url .= 'unknown'; + return $_SERVER['HTTP_HOST']; } - return $url; + + return 'unknown'; } - + /** * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" * will return "?param1=value1¶m2=value2" @@ -157,7 +174,7 @@ class Piwik_Url } return $url; } - + /** * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" * will return @@ -173,7 +190,7 @@ class Piwik_Url $urlValues = Piwik_Common::getArrayFromQueryString($queryString); return $urlValues; } - + /** * Given an array of name-values, it will return the current query string * with the new requested parameter key-values; @@ -196,7 +213,7 @@ class Piwik_Url } return ''; } - + /** * Given an array of parameters name->value, returns the query string. * Also works with array values using the php array syntax for GET parameters. @@ -228,7 +245,7 @@ class Piwik_Url $query = substr($query, 0, -1); return $query; } - + /** * Redirects the user to the Referer if found. * If the user doesn't have a referer set, it redirects to the current URL without query string. @@ -242,7 +259,7 @@ class Piwik_Url } self::redirectToUrl(self::getCurrentUrlWithoutQueryString()); } - + /** * Redirects the user to the specified URL * @@ -253,7 +270,7 @@ class Piwik_Url header("Location: $url"); exit; } - + /** * Returns the HTTP_REFERER header, false if not found. * @@ -267,4 +284,46 @@ class Piwik_Url } return false; } + + /** + * Is the URL on the same host and in the same script path? + * + * @param string $url + * @return bool True if local; false otherwise. + */ + static public function isLocalUrl($url) + { + // handle case-sensitivity differences + $pathContains = strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' ? 'stripos' : 'strpos'; + + // test the scheme/protocol portion of the reconstructed "current" URL + if(stripos($url, 'http://') === 0 || stripos($url, 'https://') === 0) + { + // determine the offset to begin the comparison + $offset = strpos($url, '://'); + $current = strstr(self::getCurrentUrlWithoutFileName(), '://'); + if($pathContains($url, $current, $offset) === $offset) + { + return true; + } + } + + return false; + } + + /** + * Get local referer, i.e., on the same host and in the same script path. + * + * @return string|false + */ + static public function getLocalReferer() + { + // verify that the referer contains the current URL (minus the filename & query parameters), http://example.org/dir1/dir2/ + $referer = self::getReferer(); + if($referer !== false && self::isLocalUrl($referer)) { + return $referer; + } + + return false; + } } diff --git a/core/Version.php b/core/Version.php index 26e24c4dfc..a6031b12a7 100644 --- a/core/Version.php +++ b/core/Version.php @@ -17,5 +17,5 @@ */ final class Piwik_Version { - const VERSION = '0.5'; + const VERSION = '0.6.3'; } diff --git a/core/View.php b/core/View.php index 24b7339738..6757a101dd 100644 --- a/core/View.php +++ b/core/View.php @@ -9,7 +9,6 @@ * @category Piwik * @package Piwik */ - /* * Transition for pre-Piwik 0.4.4 * @todo Remove this post-1.0 @@ -34,7 +33,8 @@ class Piwik_View implements Piwik_iView private $template = ''; private $smarty = false; private $variables = array(); - + private $contentType = 'text/html; charset=utf-8'; + public function __construct( $templateFile, $smConf = array(), $filter = true ) { $this->template = $templateFile; @@ -50,7 +50,7 @@ class Piwik_View implements Piwik_iView } $this->smarty->template_dir = $smConf->template_dir->toArray(); - array_walk($this->smarty->template_dir, array("Piwik_View","addPiwikPath"), PIWIK_USER_PATH); + array_walk($this->smarty->template_dir, array("Piwik_View","addPiwikPath"), PIWIK_INCLUDE_PATH); $this->smarty->plugins_dir = $smConf->plugins_dir->toArray(); array_walk($this->smarty->plugins_dir, array("Piwik_View","addPiwikPath"), PIWIK_INCLUDE_PATH); @@ -61,8 +61,12 @@ class Piwik_View implements Piwik_iView $this->smarty->cache_dir = $smConf->cache_dir; Piwik_View::addPiwikPath($this->smarty->cache_dir, null, PIWIK_USER_PATH); - $this->smarty->error_reporting = $smConf->debugging; - $this->smarty->error_reporting = $smConf->error_reporting; + $error_reporting = $smConf->error_reporting; + if($error_reporting != (string)(int)$error_reporting) + { + $error_reporting = self::bitwise_eval($error_reporting); + } + $this->smarty->error_reporting = $error_reporting; $this->smarty->assign('tag', 'piwik=' . Piwik_Version::VERSION); if($filter) @@ -100,17 +104,20 @@ class Piwik_View implements Piwik_iView return $this->smarty->get_template_vars($key); } + /** + * Render view + */ public function render() { try { $this->currentModule = Piwik::getModule(); - $this->currentPluginName = Piwik::getCurrentPlugin()->getName(); + $this->currentPluginName = Piwik::getCurrentPlugin()->getClassName(); $this->userLogin = Piwik::getCurrentUserLogin(); $showWebsiteSelectorInUserInterface = Zend_Registry::get('config')->General->show_website_selector_in_user_interface; if($showWebsiteSelectorInUserInterface) { - $sites = Piwik_SitesManager_API::getSitesWithAtLeastViewAccess(); + $sites = Piwik_SitesManager_API::getInstance()->getSitesWithAtLeastViewAccess(); usort($sites, create_function('$site1, $site2', 'return strcasecmp($site1["name"], $site2["name"]);')); $this->sites = $sites; } @@ -122,7 +129,7 @@ class Piwik_View implements Piwik_iView $this->piwik_version = Piwik_Version::VERSION; $this->latest_version_available = Piwik_UpdateCheck::isNewestVersionAvailable(); - $this->loginModule = Zend_Registry::get('auth')->getName(); + $this->loginModule = Piwik::getLoginPluginName(); } catch(Exception $e) { // can fail, for example at installation (no plugin loaded yet) } @@ -135,13 +142,28 @@ class Piwik_View implements Piwik_iView $this->totalNumberOfQueries = 0; } - @header('Content-Type: text/html; charset=utf-8'); + @header('Content-Type: '.$this->contentType); @header("Pragma: "); @header("Cache-Control: no-store, must-revalidate"); return $this->smarty->fetch($this->template); } - + + /** + * Set Content-Type field in HTTP response + * + * @param string $contentType + */ + public function setContentType( $contentType ) + { + $this->contentType = $contentType; + } + + /** + * Add form to view + * + * @param Piwik_Form $form + */ public function addForm( $form ) { // Create the renderer object @@ -154,7 +176,13 @@ class Piwik_View implements Piwik_iView $this->smarty->assign('form_data', $renderer->toArray()); $this->smarty->assign('element_list', $form->getElementList()); } - + + /** + * Assign value to a variable for use in Smarty template + * + * @param string|array $var + * @param mixed $value + */ public function assign($var, $value=null) { if (is_string($var)) @@ -170,6 +198,9 @@ class Piwik_View implements Piwik_iView } } + /** + * Clear compiled Smarty templates + */ public function clearCompiledTemplates() { $this->smarty->clear_compiled_tpl(); @@ -192,6 +223,13 @@ class Piwik_View implements Piwik_iView } */ + /** + * Prepend relative paths with absolute Piwik path + * + * @param string $value relative path (pass by reference) + * @param int $key (don't care) + * @param string $path Piwik root + */ static public function addPiwikPath(&$value, $key, $path) { if($value[0] != '/' && $value[0] != DIRECTORY_SEPARATOR) @@ -201,6 +239,33 @@ class Piwik_View implements Piwik_iView } /** + * Evaluate expression containing only bitwise operators. + * Replaces defined constants with corresponding values. + * Does not use eval() or create_function(). + * + * @param string $expression Expression. + * @return string + */ + static public function bitwise_eval($expression) + { + // replace defined constants + $buf = get_defined_constants(true); + + // use only the 'Core' PHP constants, e.g., E_ALL, E_STRICT, ... + $consts = isset($buf['Core']) ? $buf['Core'] : (isset($buf['mhash']) ? $buf['mhash'] : $buf['internal']); + $expression = str_replace(' ', '', strtr($expression, $consts)); + + // bitwise operators in order of precedence (highest to lowest) + // @todo: boolean ! (NOT) and parentheses aren't handled + $expression = preg_replace_callback('/~(-?[0-9]+)/', create_function('$matches', 'return (string)((~(int)$matches[1]));'), $expression); + $expression = preg_replace_callback('/(-?[0-9]+)&(-?[0-9]+)/', create_function('$matches', 'return (string)((int)$matches[1]&(int)$matches[2]);'), $expression); + $expression = preg_replace_callback('/(-?[0-9]+)\^(-?[0-9]+)/', create_function('$matches', 'return (string)((int)$matches[1]^(int)$matches[2]);'), $expression); + $expression = preg_replace_callback('/(-?[0-9]+)\|(-?[0-9]+)/', create_function('$matches', 'return (string)((int)$matches[1]|(int)$matches[2]);'), $expression); + + return (string)((int)$expression & PHP_INT_MAX); + } + + /** * View factory method * * @param $templateName Template name (e.g., 'index') @@ -211,10 +276,10 @@ class Piwik_View implements Piwik_iView Piwik_PostEvent('View.getViewType', $viewType); // get caller - $bt = debug_backtrace(); - if(!isset($bt[0])) + $bt = @debug_backtrace(); + if($bt === null || !isset($bt[0])) { - throw new Exception("View factory cannot be invoked directly"); + throw new Exception("View factory cannot be invoked"); } $path = dirname($bt[0]['file']); diff --git a/core/ViewDataTable.php b/core/ViewDataTable.php index 5608dd152f..703044d78b 100644 --- a/core/ViewDataTable.php +++ b/core/ViewDataTable.php @@ -82,6 +82,14 @@ abstract class Piwik_ViewDataTable */ protected $dataTable = null; + + /** + * List of filters to apply after the data has been loaded from the API + * + * @var array + */ + protected $queuedFilters = array(); + /** * @see init() * @var string @@ -121,8 +129,19 @@ abstract class Piwik_ViewDataTable */ protected $columnsTranslations = array(); - + /** + * Array of columns set to display + * + * @var array + */ protected $columnsToDisplay = array(); + + /** + * Variable that is used as the DIV ID in the rendered HTML + * + * @var string + */ + protected $uniqIdTable = null; /** * Method to be implemented by the ViewDataTable_*. @@ -251,7 +270,7 @@ abstract class Piwik_ViewDataTable $this->viewProperties['show_all_views_icons'] = Piwik_Common::getRequestVar('show_all_views_icons', true); $this->viewProperties['show_export_as_image_icon'] = Piwik_Common::getRequestVar('show_export_as_image_icon', false); $this->viewProperties['show_exclude_low_population'] = Piwik_Common::getRequestVar('show_exclude_low_population', true); - $this->viewProperties['show_offset_information'] = Piwik_Common::getRequestVar('show_offset_information', true);; + $this->viewProperties['show_offset_information'] = Piwik_Common::getRequestVar('show_offset_information', true); $this->viewProperties['show_footer'] = Piwik_Common::getRequestVar('show_footer', true); $this->viewProperties['show_footer_icons'] = ($this->idSubtable == false); $this->viewProperties['apiMethodToRequestDataTable'] = $this->apiMethodToRequestDataTable; @@ -344,6 +363,21 @@ abstract class Piwik_ViewDataTable } /** + * Hook called after the dataTable has been loaded from the API + * Can be used to add, delete or modify the data freshly loaded + */ + protected function postDataTableLoadedFromAPI() + { + // Apply datatable filters that were queued by the controllers + foreach($this->queuedFilters as $filter) + { + $filterName = $filter[0]; + $filterParameters = $filter[1]; + $this->dataTable->filter($filterName, $filterParameters); + } + } + + /** * @return string URL to call the API, eg. "method=Referers.getKeywords&period=day&date=yesterday"... */ protected function getRequestString() @@ -415,7 +449,7 @@ abstract class Piwik_ViewDataTable * @see datatable.js * @return string */ - protected function getUniqueIdViewDataTable() + protected function loadUniqueIdViewDataTable() { // if we request a subDataTable the $this->currentControllerAction DIV ID is already there in the page // we make the DIV ID really unique by appending the ID of the subtable requested @@ -434,6 +468,27 @@ abstract class Piwik_ViewDataTable } return $uniqIdTable; } + + /** + * Sets the $uniqIdTable variable that is used as the DIV ID in the rendered HTML + */ + public function setUniqueIdViewDataTable($uniqIdTable) + { + $this->viewProperties['uniqueId'] = $uniqIdTable; + $this->uniqIdTable = $uniqIdTable; + } + + /** + * Returns current value of $uniqIdTable variable that is used as the DIV ID in the rendered HTML + */ + public function getUniqueIdViewDataTable() + { + if( $this->uniqIdTable == null ) + { + $this->uniqIdTable = $this->loadUniqueIdViewDataTable(); + } + return $this->uniqIdTable; + } /** * Returns array of properties, eg. "show_footer", "show_search", etc. @@ -721,7 +776,7 @@ abstract class Piwik_ViewDataTable */ public function setLimit( $limit ) { - if($limit != 0) + if($limit !== 0) { $this->variablesDefault['filter_limit'] = $limit; } @@ -739,6 +794,15 @@ abstract class Piwik_ViewDataTable $this->variablesDefault['filter_sort_order'] = $order; } + /** + * Returns the column name on which the table will be sorted + * + * @return string + */ + public function getSortedColumn() + { + return $this->variablesDefault['filter_sort_column']; + } /** * Sets translation string for given column @@ -746,9 +810,10 @@ abstract class Piwik_ViewDataTable * @param string $columnName column name * @param string $columnTranslation column name translation */ - public function setColumnTranslation( $columnName, $columnTranslation ) + public function setColumnTranslation( $columnName, $columnTranslation, $columnDescription = false ) { $this->columnsTranslations[$columnName] = $columnTranslation; + $this->columnsDescriptions[$columnName] = $columnDescription; } /** @@ -762,10 +827,20 @@ abstract class Piwik_ViewDataTable { return html_entity_decode($this->columnsTranslations[$columnName], ENT_COMPAT, 'UTF-8'); } - else + return $columnName; + } + + /** + * Returns column description, or false + * @param string $columnName column name + */ + public function getColumnDescription( $columnName ) + { + if( !empty($this->columnsDescriptions[$columnName]) ) { - return $columnName; + return html_entity_decode($this->columnsDescriptions[$columnName], ENT_COMPAT, 'UTF-8'); } + return false; } /** @@ -830,4 +905,18 @@ abstract class Piwik_ViewDataTable } $this->variablesDefault[$parameter] = $value; } + + /** + * Queues a Datatable filter, that will be applied once the datatable is loaded from the API. + * Useful when the controller needs to add columns, or decorate existing columns, when these filters don't + * necessarily make sense directly in the API. + * + * @param $filterName + * @param $parameters + * @return void + */ + public function queueFilter($filterName, $parameters) + { + $this->queuedFilters[] = array($filterName, $parameters); + } } diff --git a/core/ViewDataTable/Cloud.php b/core/ViewDataTable/Cloud.php index f819ef6c3c..c7d68be64a 100644 --- a/core/ViewDataTable/Cloud.php +++ b/core/ViewDataTable/Cloud.php @@ -36,11 +36,13 @@ class Piwik_ViewDataTable_Cloud extends Piwik_ViewDataTable */ function init($currentControllerName, $currentControllerAction, - $apiMethodToRequestDataTable ) + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable = null) { parent::init($currentControllerName, $currentControllerAction, - $apiMethodToRequestDataTable ); + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable); $this->dataTableTemplate = 'CoreHome/templates/cloud.tpl'; $this->disableOffsetInformation(); $this->disableExcludeLowPopulation(); diff --git a/core/ViewDataTable/GenerateGraphData.php b/core/ViewDataTable/GenerateGraphData.php index d3b1251f73..4a2fcce75e 100644 --- a/core/ViewDataTable/GenerateGraphData.php +++ b/core/ViewDataTable/GenerateGraphData.php @@ -87,8 +87,14 @@ abstract class Piwik_ViewDataTable_GenerateGraphData extends Piwik_ViewDataTable } $this->mainAlreadyExecuted = true; - @header( "Content-type: application/json" ); + if (!Zend_Registry::get('config')->General->serve_widget_and_data) + { + @header( "Content-Type: application/json" ); + } + // Graphs require the full dataset, setting limit to null (same as 'no limit') + $this->setLimit(null); + // the queued filters will be manually applied later. This is to ensure that filtering using search // will be done on the table before the labels are enhanced (see ReplaceColumnNames) $this->disableQueuedFilters(); diff --git a/core/ViewDataTable/GenerateGraphData/ChartEvolution.php b/core/ViewDataTable/GenerateGraphData/ChartEvolution.php index 5ed804c3ad..8c8d961ade 100644 --- a/core/ViewDataTable/GenerateGraphData/ChartEvolution.php +++ b/core/ViewDataTable/GenerateGraphData/ChartEvolution.php @@ -28,11 +28,11 @@ class Piwik_ViewDataTable_GenerateGraphData_ChartEvolution extends Piwik_ViewDat $this->view = new Piwik_Visualization_Chart_Evolution(); } - protected function guessUnitFromRequestedColumnNames($requestedColumnNames) + protected function guessUnitFromRequestedColumnNames($requestedColumnNames, $idSite) { $nameToUnit = array( '_rate' => '%', - '_revenue' => Piwik::getCurrency(), + '_revenue' => Piwik::getCurrency($idSite), ); foreach($requestedColumnNames as $columnName) { @@ -119,11 +119,12 @@ class Piwik_ViewDataTable_GenerateGraphData_ChartEvolution extends Piwik_ViewDat $yAxisLabelToValueCleaned[$yAxisLabel][] = $columnValue; } } + $idSite = Piwik_Common::getRequestVar('idSite'); $unit = $this->yAxisUnit; if(empty($unit)) { - $unit = $this->guessUnitFromRequestedColumnNames($requestedColumnNames); + $unit = $this->guessUnitFromRequestedColumnNames($requestedColumnNames, $idSite); } $this->view->setAxisXLabels($xLabels); @@ -145,24 +146,63 @@ class Piwik_ViewDataTable_GenerateGraphData_ChartEvolution extends Piwik_ViewDat if($this->isLinkEnabled()) { $axisXOnClick = array(); + $queryStringAsHash = $this->getQueryStringAsHash(); foreach($this->dataTable->metadata as $idDataTable => $metadataDataTable) { $period = $metadataDataTable['period']; $dateInUrl = $period->getDateStart(); + $parameters = array( + 'idSite' => $idSite, + 'period' => $period->getLabel(), + 'date' => $dateInUrl->toString() + ); + $hash = ''; + if(!empty($queryStringAsHash)) + { + $hash = '#' . Piwik_Url::getQueryStringFromParameters( $queryStringAsHash + $parameters); + } $link = Piwik_Url::getCurrentUrlWithoutQueryString() . '?' . Piwik_Url::getQueryStringFromParameters( array( 'module' => 'CoreHome', 'action' => 'index', - 'idSite' => Piwik_Common::getRequestVar('idSite'), - 'period' => $period->getLabel(), - 'date' => $dateInUrl, - )); + ) + $parameters) + . $hash; $axisXOnClick[] = $link; } $this->view->setAxisXOnClick($axisXOnClick); } } + + /** + * We link the graph dots to the same report as currently being displayed (only the date would change). + * + * In some cases the widget is loaded within a report that doesn't exist as such. + * For example, the dashboards loads the 'Last visits graph' widget which can't be directly linked to. + * Instead, the graph must link back to the dashboard. + * + * In other cases, like Visitors>Overview or the Goals graphs, we can link the graph clicks to the same report. + * + * To detect whether or not we can link to a report, we simply check if the current URL from which it was loaded + * belongs to the menu or not. If it doesn't belong to the menu, we do not append the hash to the URL, + * which results in loading the dashboard. + * + * @return array Query string array to append to the URL hash or false if the dashboard should be displayed + */ + private function getQueryStringAsHash() + { + $queryString = Piwik_Url::getArrayFromCurrentQueryString(); + $piwikParameters = array('idSite', 'date', 'period', 'XDEBUG_SESSION_START', 'KEY'); + foreach($piwikParameters as $parameter) + { + unset($queryString[$parameter]); + } + if(Piwik_IsMenuUrlFound($queryString)) + { + return $queryString; + } + return false; + } private function isLinkEnabled() { diff --git a/core/ViewDataTable/GenerateGraphHTML.php b/core/ViewDataTable/GenerateGraphHTML.php index 1c3bad57f3..be7e774a0a 100644 --- a/core/ViewDataTable/GenerateGraphHTML.php +++ b/core/ViewDataTable/GenerateGraphHTML.php @@ -28,12 +28,13 @@ abstract class Piwik_ViewDataTable_GenerateGraphHTML extends Piwik_ViewDataTable */ function init($currentControllerName, $currentControllerAction, - $apiMethodToRequestDataTable ) + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable = null) { parent::init($currentControllerName, $currentControllerAction, - $apiMethodToRequestDataTable ); - + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable); $this->dataTableTemplate = 'CoreHome/templates/graph.tpl'; $this->disableOffsetInformation(); @@ -67,7 +68,8 @@ abstract class Piwik_ViewDataTable_GenerateGraphHTML extends Piwik_ViewDataTable /** * We persist the parametersToModify values in the javascript footer. - * This is used by the "export links" that use the "date" attribute from the json properties array in the datatable footer. + * This is used by the "export links" that use the "date" attribute + * from the json properties array in the datatable footer. */ protected function getJavascriptVariablesToSet() { @@ -125,9 +127,19 @@ abstract class Piwik_ViewDataTable_GenerateGraphHTML extends Piwik_ViewDataTable foreach($this->parametersToModify as $key => $val) { - if (is_array($val)) { + // We do not forward filter data to the graph controller. + // This would cause the graph to have filter_limit=5 set by default, + // which would break them (graphs need the full dataset to build the "Others" aggregate value) + if(strpos($key, 'filter_') !== false) + { + continue; + } + if (is_array($val)) + { $_GET[$key] = unserialize(serialize($val)); - } else { + } + else + { $_GET[$key] = $val; } } @@ -135,7 +147,7 @@ abstract class Piwik_ViewDataTable_GenerateGraphHTML extends Piwik_ViewDataTable $_GET = $saveGet; - return str_replace(array("\r", "\n", "'"), array('', '', "\\'"), $content); + return str_replace(array("\r", "\n", "'", '\"'), array('', '', "\\'", '\\\"'), $content); } protected function getFlashParameters() diff --git a/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php b/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php index fae0235bfb..78b990e62b 100644 --- a/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php +++ b/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php @@ -38,17 +38,34 @@ class Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution extends Piwik_ViewDat function init($currentControllerName, $currentControllerAction, - $apiMethodToRequestDataTable ) + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable = null) { parent::init($currentControllerName, $currentControllerAction, - $apiMethodToRequestDataTable ); + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable); $this->setParametersToModify(array('date' => Piwik_Common::getRequestVar('date', 'last30', 'string'))); $this->disableShowAllViewsIcons(); $this->disableShowTable(); } + + /** + * We ensure that the graph for a given Goal has a different ID than the 'Goals Overview' graph + * so that both can display on the dashboard at the same time + */ + public function getUniqueIdViewDataTable() + { + $id = parent::getUniqueIdViewDataTable(); + if(isset($this->parametersToModify['idGoal'])) + { + $id .= $this->parametersToModify['idGoal']; + } + return $id; + } + /** * Sets the columns that will be displayed on output evolution chart * By default all columns are displayed ($columnsNames = array() will display all columns) diff --git a/core/ViewDataTable/HtmlTable.php b/core/ViewDataTable/HtmlTable.php index d40ed84696..cac953024c 100644 --- a/core/ViewDataTable/HtmlTable.php +++ b/core/ViewDataTable/HtmlTable.php @@ -41,7 +41,7 @@ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable function init($currentControllerName, $currentControllerAction, $apiMethodToRequestDataTable, - $controllerActionCalledWhenRequestSubTable = null ) + $controllerActionCalledWhenRequestSubTable = null) { parent::init($currentControllerName, $currentControllerAction, @@ -82,14 +82,6 @@ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable } /** - * Hook called after the dataTable has been loaded from the API - * Can be used to add, delete or modify the data freshly loaded - */ - protected function postDataTableLoadedFromAPI() - { - } - - /** * @return Piwik_View with all data set */ protected function buildView() @@ -103,10 +95,11 @@ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable else { $columns = $this->getColumnsToDisplay(); - $columnTranslations = array(); + $columnTranslations = $columnDescriptions = array(); foreach($columns as $columnName) { $columnTranslations[$columnName] = $this->getColumnTranslation($columnName); + $columnDescriptions[$columnName] = $this->getColumnDescription($columnName); } $nbColumns = count($columns); // case no data in the array we use the number of columns set to be displayed @@ -118,6 +111,7 @@ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable $view->arrayDataTable = $this->getPHPArrayFromDataTable(); $view->dataTableColumns = $columns; $view->columnTranslations = $columnTranslations; + $view->columnDescriptions = $columnDescriptions; $view->nbColumns = $nbColumns; $view->defaultWhenColumnValueNotDefined = '-'; } diff --git a/core/ViewDataTable/HtmlTable/Goals.php b/core/ViewDataTable/HtmlTable/Goals.php index a4627e0cd5..8bac47b4b1 100644 --- a/core/ViewDataTable/HtmlTable/Goals.php +++ b/core/ViewDataTable/HtmlTable/Goals.php @@ -23,14 +23,35 @@ class Piwik_ViewDataTable_HtmlTable_Goals extends Piwik_ViewDataTable_HtmlTable public function main() { + $this->idSite = Piwik_Common::getRequestVar('idSite', null, 'int'); + $this->processOnlyIdGoal = Piwik_Common::getRequestVar('filter_only_display_idgoal', 0, 'int'); $this->viewProperties['show_exclude_low_population'] = true; $this->viewProperties['show_goals'] = true; - $this->setColumnsToDisplay( array( 'label', - 'nb_visits', - 'goals_conversion_rate', - 'goal_%s_conversion_rate', - 'revenue_per_visit', - )); + + $this->setColumnsTranslations( array( + 'goal_%s_conversion_rate' => '%s conversion rate', + 'goal_%s_nb_conversions' => '%s conversions', + 'goal_%s_revenue_per_visit' => '%s revenue per visit', + )); + + $this->setColumnsToDisplay( array( + 'label', + 'nb_visits', + 'goal_%s_nb_conversions', + 'goal_%s_conversion_rate', + 'goal_%s_revenue_per_visit', + 'goals_conversion_rate', + 'revenue_per_visit', + )); + + // We ensure that the 'Sort by' column is actually displayed in the table + // eg. most daily reports sort by nb_uniq_visitors but this column is not displayed in the Goals table + $columnsToDisplay = $this->getColumnsToDisplay(); + $columnToSortBy = $this->getSortedColumn(); + if(!in_array($columnToSortBy, $columnsToDisplay)) + { + $this->setSortedColumn('nb_visits', 'desc'); + } parent::main(); } @@ -39,35 +60,49 @@ class Piwik_ViewDataTable_HtmlTable_Goals extends Piwik_ViewDataTable_HtmlTable $this->controllerActionCalledWhenRequestSubTable = null; } - protected function getRequestString() - { - $requestString = parent::getRequestString(); - return $requestString . '&filter_update_columns_when_show_all_goals=1'; - } - - protected $columnsToPercentageFilter = array(); - - private function getIdSite() - { - return Piwik_Common::getRequestVar('idSite', null, 'int'); - } - public function setColumnsToDisplay($columnsNames) { $newColumnsNames = array(); + $goals = array(); + $idSite = $this->getIdSite(); + if($idSite) + { + $goals = Piwik_Goals_API::getInstance()->getGoals( $idSite ); + } foreach($columnsNames as $columnName) { - if($columnName == 'goal_%s_conversion_rate') + if(in_array($columnName, array('goal_%s_conversion_rate', 'goal_%s_nb_conversions', 'goal_%s_revenue_per_visit'))) { - $goals = Piwik_Goals_API::getGoals( $this->getIdSite() ); foreach($goals as $goal) { $idgoal = $goal['idgoal']; - $name = $goal['name']; - $columnName = 'goal_'.$idgoal.'_conversion_rate'; - $newColumnsNames[] = $columnName; - $this->setColumnTranslation($columnName, $name); - $this->columnsToPercentageFilter[] = $columnName; + if($this->processOnlyIdGoal > Piwik_DataTable_Filter_UpdateColumnsWhenShowAllGoals::GOALS_FULL_TABLE + && $this->processOnlyIdGoal != $idgoal) + { + continue; + } + $name = Piwik_Translate($this->getColumnTranslation($columnName), $goal['name']); + $columnNameGoal = str_replace('%s', $idgoal, $columnName); + $this->setColumnTranslation($columnNameGoal, $name); + if(strstr($columnNameGoal, '_rate') !== false) + { + $this->columnsToPercentageFilter[] = $columnNameGoal; + } + // For the goal table (when the flag icon is clicked), we only display the per Goal Conversion rate + elseif($this->processOnlyIdGoal == Piwik_DataTable_Filter_UpdateColumnsWhenShowAllGoals::GOALS_OVERVIEW) + { + continue; + } + + if(strstr($columnNameGoal, '_revenue') !== false) + { + $this->columnsToRevenueFilter[] = $columnNameGoal; + } + else + { + $this->columnsToConversionFilter[] = $columnNameGoal; + } + $newColumnsNames[] = $columnNameGoal; } } else @@ -78,14 +113,45 @@ class Piwik_ViewDataTable_HtmlTable_Goals extends Piwik_ViewDataTable_HtmlTable parent::setColumnsToDisplay($newColumnsNames); } + protected function getRequestString() + { + $requestString = parent::getRequestString(); + if($this->processOnlyIdGoal > Piwik_DataTable_Filter_UpdateColumnsWhenShowAllGoals::GOALS_FULL_TABLE) + { + $requestString .= "&filter_only_display_idgoal=".$this->processOnlyIdGoal; + } + return $requestString . '&filter_update_columns_when_show_all_goals=1'; + } + + protected $columnsToPercentageFilter = array(); + protected $columnsToRevenueFilter = array(); + protected $columnsToConversionFilter = array(); + protected $idSite = false; + + private function getIdSite() + { + return $this->idSite; + } + protected function postDataTableLoadedFromAPI() { parent::postDataTableLoadedFromAPI(); $this->columnsToPercentageFilter[] = 'goals_conversion_rate'; foreach($this->columnsToPercentageFilter as $columnName) { - $this->dataTable->filter('ColumnCallbackReplace', array($columnName, create_function('$rate', 'return $rate."%";'))); + $this->dataTable->filter('ColumnCallbackReplace', array($columnName, create_function('$rate', 'return sprintf("%.1f",$rate)."%";'))); + } + $this->columnsToRevenueFilter[] = 'revenue_per_visit'; + foreach($this->columnsToRevenueFilter as $columnName) + { + $this->dataTable->filter('ColumnCallbackReplace', array($columnName, create_function('$value', 'return sprintf("%.1f",$value);'))); + $this->dataTable->filter('ColumnCallbackReplace', array($columnName, array("Piwik", "getPrettyMoney"), array($this->getIdSite()))); + } + + foreach($this->columnsToConversionFilter as $columnName) + { + // this ensures that the value is set to zero for all rows where the value was not set (no conversion) + $this->dataTable->filter('ColumnCallbackReplace', array($columnName, create_function('$value', 'return $value;'))); } - $this->dataTable->filter('ColumnCallbackReplace', array('revenue_per_visit', array("Piwik", "getPrettyMoney"))); } } diff --git a/core/Visualization/Chart.php b/core/Visualization/Chart.php index 9efd164aa9..c795522d8d 100644 --- a/core/Visualization/Chart.php +++ b/core/Visualization/Chart.php @@ -10,9 +10,6 @@ * @package Piwik */ -// no direct access -defined('PIWIK_INCLUDE_PATH') or die; - /** * @see libs/open-flash-chart/php-ofc-library/open-flash-chart.php * @link http://teethgrinder.co.uk/open-flash-chart-2/ @@ -128,8 +125,12 @@ abstract class Piwik_Visualization_Chart implements Piwik_iView public function render() { - @header("Pragma: "); - @header("Cache-Control: no-store, must-revalidate"); + if(Piwik_Url::getCurrentScheme() == 'https' || + Zend_Registry::get('config')->General->reverse_proxy) + { + @header("Pragma: "); + @header("Cache-Control: must-revalidate"); + } return $this->chart->toPrettyString(); } diff --git a/core/Visualization/Sparkline.php b/core/Visualization/Sparkline.php index 53110906f4..3b17e8fceb 100644 --- a/core/Visualization/Sparkline.php +++ b/core/Visualization/Sparkline.php @@ -10,9 +10,6 @@ * @package Piwik */ -// no direct access -defined('PIWIK_INCLUDE_PATH') or die; - /** * @see libs/sparkline/lib/Sparkline_Line.php * @link http://sparkline.org @@ -52,18 +49,22 @@ class Piwik_Visualization_Sparkline implements Piwik_iView $width = self::getWidth(); $height = self::getHeight(); - $data = $this->values; $sparkline = new Sparkline_Line(); $sparkline->SetColor('lineColor', 22, 44, 74); // dark blue $sparkline->SetColorHtml('red', '#FF7F7F'); $sparkline->SetColorHtml('blue', '#55AAFF'); $sparkline->SetColorHtml('green', '#75BF7C'); - $data = array_reverse($data); $min = $max = $last = null; $i = 0; foreach($this->values as $value) { + // 50% should be plotted as 50 + $toRemove = '%'; + if(strpos($value, $toRemove) !== false) + { + $value = str_replace($toRemove, '', $value); + } $sparkline->SetData($i, $value); if( null == $min || $value <= $min[1]) { @@ -77,14 +78,12 @@ class Piwik_Visualization_Sparkline implements Piwik_iView $i++; } $sparkline->SetYMin(0); - $sparkline->setYMax($max[1] + 0.5); // the +0.5 seems to be mandatory to not lose some pixels when value = max - $sparkline->SetPadding( 3, 0, 2, 0); - $font = FONT_2; - // the -0.5 is a hack as the sparkline samping rendering is obviously slightly bugged - // (see also fix marked as //FIX FROM PIWIK in libs/sparkline/lib/Sparkline.php) - $sparkline->SetFeaturePoint($min[0] -1, $min[1], 'red', 5); - $sparkline->SetFeaturePoint($max[0] -1, $max[1], 'green', 5); - $sparkline->SetFeaturePoint($last[0] -1, $last[1], 'blue', 5); + $sparkline->SetYMax($max[1]); + $sparkline->SetPadding( 3, 0, 2, 0 ); // top, right, bottom, left +// $font = FONT_2; + $sparkline->SetFeaturePoint($min[0], $min[1], 'red', 5); + $sparkline->SetFeaturePoint($max[0], $max[1], 'green', 5); + $sparkline->SetFeaturePoint($last[0], $last[1], 'blue', 5); $sparkline->SetLineSize(3); // for renderresampled, linesize is on virtual image $ratio = 1; // var_dump($min);var_dump($max);var_dump($lasts);exit; diff --git a/core/testMinimumPhpVersion.php b/core/testMinimumPhpVersion.php index 36a6ac1f1f..bd28c3fe72 100644 --- a/core/testMinimumPhpVersion.php +++ b/core/testMinimumPhpVersion.php @@ -10,9 +10,6 @@ * @package Piwik */ -// no direct access -defined('PIWIK_INCLUDE_PATH') or die; - /** * This file is executed before anything else. * It checks the minimum PHP version required to run Piwik. @@ -48,7 +45,7 @@ function Piwik_ExitWithMessage($message, $optionalTrace = false, $optionalLinks { if($optionalTrace) { - $optionalTrace = '<font color="#888888">Backtrace:<br/><pre>'.$optionalTrace.'</pre></font>'; + $optionalTrace = '<font color="#888888">Backtrace:<br /><pre>'.$optionalTrace.'</pre></font>'; } if($optionalLinks) { @@ -69,6 +66,7 @@ function Piwik_ExitWithMessage($message, $optionalTrace = false, $optionalLinks exit; } +// added in PHP 4.3.0 if (!function_exists('file_get_contents')) { function file_get_contents($filename) |