diff options
author | mattab <matthieu.aubry@gmail.com> | 2013-03-28 03:42:39 +0400 |
---|---|---|
committer | mattab <matthieu.aubry@gmail.com> | 2013-03-28 03:42:40 +0400 |
commit | ae4b03163792f0b6e933933e5d37df87dc3fd566 (patch) | |
tree | d1d7510a9728f587d3d63ebd03e4ecf3d904838b /core | |
parent | 158c2150f5f2e13ece459b8d131244c11b763997 (diff) |
Mass conversion of all files to the newly agreed coding standard: PSR 1/2
Converting Piwik core source files, PHP, JS, TPL, CSS
More info: http://piwik.org/participate/coding-standards/
Diffstat (limited to 'core')
267 files changed, 42581 insertions, 45367 deletions
diff --git a/core/API/DataTableGenericFilter.php b/core/API/DataTableGenericFilter.php index ffdd4de044..02e2b31a24 100644 --- a/core/API/DataTableGenericFilter.php +++ b/core/API/DataTableGenericFilter.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,146 +15,136 @@ */ class Piwik_API_DataTableGenericFilter { - private static $genericFiltersInfo = null; + private static $genericFiltersInfo = null; - /** - * Constructor - * - * @param $request - */ - function __construct( $request ) - { - $this->request = $request; - } + /** + * Constructor + * + * @param $request + */ + function __construct($request) + { + $this->request = $request; + } - /** - * Filters the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - $this->applyGenericFilters($table); - } - - /** - * Returns an array containing the information of the generic Piwik_DataTable_Filter - * to be applied automatically to the data resulting from the API calls. - * - * Order to apply the filters: - * 1 - Filter that remove filtered rows - * 2 - Filter that sort the remaining rows - * 3 - Filter that keep only a subset of the results - * 4 - Presentation filters - * - * @return array See the code for spec - */ - public static function getGenericFiltersInformation() - { - if (is_null(self::$genericFiltersInfo)) - { - self::$genericFiltersInfo = array( - 'Pattern' => array( - 'filter_column' => array('string', 'label'), - 'filter_pattern' => array('string'), - ), - 'PatternRecursive' => array( - 'filter_column_recursive' => array('string', 'label'), - 'filter_pattern_recursive' => array('string'), - ), - 'ExcludeLowPopulation' => array( - 'filter_excludelowpop' => array('string'), - 'filter_excludelowpop_value'=> array('float', '0'), - ), - 'AddColumnsProcessedMetrics' => array( - 'filter_add_columns_when_show_all_columns' => array('integer') - ), - 'AddColumnsProcessedMetricsGoal' => array( - 'filter_update_columns_when_show_all_goals' => array('integer'), - 'idGoal' => array('string', Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW), - ), - 'Sort' => array( - 'filter_sort_column' => array('string'), - 'filter_sort_order' => array('string', 'desc'), - ), - 'Truncate' => array( - 'filter_truncate' => array('integer'), - ), - 'Limit' => array( - 'filter_offset' => array('integer', '0'), - 'filter_limit' => array('integer'), - 'keep_summary_row' => array('integer', '0'), - ), - ); - } + /** + * Filters the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + $this->applyGenericFilters($table); + } - return self::$genericFiltersInfo; - } - - /** - * Apply generic filters to the DataTable object resulting from the API Call. - * Disable this feature by setting the parameter disable_generic_filters to 1 in the API call request. - * - * @param Piwik_DataTable $datatable - * @return bool - */ - protected function applyGenericFilters($datatable) - { - if($datatable instanceof Piwik_DataTable_Array ) - { - $tables = $datatable->getArray(); - $filterWasApplied = false; - foreach($tables as $table) - { - $filterWasApplied = $this->applyGenericFilters($table); - } - return; - } - - $genericFilters = self::getGenericFiltersInformation(); - - $filterApplied = false; - foreach($genericFilters as $filterName => $parameters) - { - $filterParameters = array(); - $exceptionRaised = false; - foreach($parameters as $name => $info) - { - // parameter type to cast to - $type = $info[0]; - - // default value if specified, when the parameter doesn't have a value - $defaultValue = null; - if(isset($info[1])) - { - $defaultValue = $info[1]; - } - - // third element in the array, if it exists, overrides the name of the request variable - $varName = $name; - if(isset($info[2])) - { - $varName = $info[2]; - } - - try { - $value = Piwik_Common::getRequestVar($name, $defaultValue, $type, $this->request); - settype($value, $type); - $filterParameters[] = $value; - } - catch(Exception $e) - { - $exceptionRaised = true; - break; - } - } + /** + * Returns an array containing the information of the generic Piwik_DataTable_Filter + * to be applied automatically to the data resulting from the API calls. + * + * Order to apply the filters: + * 1 - Filter that remove filtered rows + * 2 - Filter that sort the remaining rows + * 3 - Filter that keep only a subset of the results + * 4 - Presentation filters + * + * @return array See the code for spec + */ + public static function getGenericFiltersInformation() + { + if (is_null(self::$genericFiltersInfo)) { + self::$genericFiltersInfo = array( + 'Pattern' => array( + 'filter_column' => array('string', 'label'), + 'filter_pattern' => array('string'), + ), + 'PatternRecursive' => array( + 'filter_column_recursive' => array('string', 'label'), + 'filter_pattern_recursive' => array('string'), + ), + 'ExcludeLowPopulation' => array( + 'filter_excludelowpop' => array('string'), + 'filter_excludelowpop_value' => array('float', '0'), + ), + 'AddColumnsProcessedMetrics' => array( + 'filter_add_columns_when_show_all_columns' => array('integer') + ), + 'AddColumnsProcessedMetricsGoal' => array( + 'filter_update_columns_when_show_all_goals' => array('integer'), + 'idGoal' => array('string', Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW), + ), + 'Sort' => array( + 'filter_sort_column' => array('string'), + 'filter_sort_order' => array('string', 'desc'), + ), + 'Truncate' => array( + 'filter_truncate' => array('integer'), + ), + 'Limit' => array( + 'filter_offset' => array('integer', '0'), + 'filter_limit' => array('integer'), + 'keep_summary_row' => array('integer', '0'), + ), + ); + } - if(!$exceptionRaised) - { - $datatable->filter($filterName, $filterParameters); - $filterApplied = true; - } - } - return $filterApplied; - } + return self::$genericFiltersInfo; + } + + /** + * Apply generic filters to the DataTable object resulting from the API Call. + * Disable this feature by setting the parameter disable_generic_filters to 1 in the API call request. + * + * @param Piwik_DataTable $datatable + * @return bool + */ + protected function applyGenericFilters($datatable) + { + if ($datatable instanceof Piwik_DataTable_Array) { + $tables = $datatable->getArray(); + $filterWasApplied = false; + foreach ($tables as $table) { + $filterWasApplied = $this->applyGenericFilters($table); + } + return; + } + + $genericFilters = self::getGenericFiltersInformation(); + + $filterApplied = false; + foreach ($genericFilters as $filterName => $parameters) { + $filterParameters = array(); + $exceptionRaised = false; + foreach ($parameters as $name => $info) { + // parameter type to cast to + $type = $info[0]; + + // default value if specified, when the parameter doesn't have a value + $defaultValue = null; + if (isset($info[1])) { + $defaultValue = $info[1]; + } + + // third element in the array, if it exists, overrides the name of the request variable + $varName = $name; + if (isset($info[2])) { + $varName = $info[2]; + } + + try { + $value = Piwik_Common::getRequestVar($name, $defaultValue, $type, $this->request); + settype($value, $type); + $filterParameters[] = $value; + } catch (Exception $e) { + $exceptionRaised = true; + break; + } + } + + if (!$exceptionRaised) { + $datatable->filter($filterName, $filterParameters); + $filterApplied = true; + } + } + return $filterApplied; + } } diff --git a/core/API/DataTableManipulator.php b/core/API/DataTableManipulator.php index b7f1f79a2e..dee8d80316 100644 --- a/core/API/DataTableManipulator.php +++ b/core/API/DataTableManipulator.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,7 +12,7 @@ /** * Base class for manipulating data tables. * It provides generic mechanisms like iteration and loading subtables. - * + * * The manipulators are used in Piwik_API_ResponseBuilder and are triggered by * API parameters. They are not filters because they don't work on the pre- * fetched nested data tables. Instead, they load subtables using this base @@ -20,156 +20,145 @@ * of using expanded=1. Another difference between manipulators and filters * is that filters keep the overall structure of the table intact while * manipulators can change the entire thing. - * + * * @package Piwik * @subpackage Piwik_API */ abstract class Piwik_API_DataTableManipulator { - protected $apiModule; - protected $apiMethod; - protected $request; - - private $apiMethodForSubtable; - - /** - * Constructor - * - * @param bool $apiModule - * @param bool $apiMethod - * @param array $request - */ - public function __construct($apiModule=false, $apiMethod=false, $request=array()) { - $this->apiModule = $apiModule; - $this->apiMethod = $apiMethod; - $this->request = $request; - } - - /** - * This method can be used by subclasses to iterate over data tables that might be - * data table arrays. It calls back the template method self::doManipulate for each table. - * This way, data table arrays can be handled in a transparent fashion. - * - * @param Piwik_DataTable_Array|Piwik_DataTable $dataTable - * @throws Exception - * @return Piwik_DataTable_Array|Piwik_DataTable - */ - protected function manipulate($dataTable) - { - if ($dataTable instanceof Piwik_DataTable_Array) - { - return $this->manipulateDataTableArray($dataTable); - } - else if ($dataTable instanceof Piwik_DataTable) - { - return $this->manipulateDataTable($dataTable); - } - else - { - return $dataTable; - } - } - - /** - * Manipulates child DataTables of a DataTable_Array. See @manipulate for more info. - */ - protected function manipulateDataTableArray( $dataTable ) - { - $result = $dataTable->getEmptyClone(); - foreach ($dataTable->getArray() as $tableLabel => $childTable) - { - $newTable = $this->manipulate($childTable); - $result->addTable($newTable, $tableLabel); - } - return $result; - } - - /** - * Manipulates a single Piwik_DataTable instance. Derived classes must define - * this function. - */ - protected abstract function manipulateDataTable( $dataTable ); - - /** - * Load the subtable for a row. - * Returns null if none is found. - * - * @param Piwik_Datatable_Row $row - * @throws Exception - * @return Piwik_DataTable - */ - protected function loadSubtable($dataTable, $row) { - if (!($this->apiModule && $this->apiMethod && count($this->request))) { - return null; - } - - $request = $this->request; - + protected $apiModule; + protected $apiMethod; + protected $request; + + private $apiMethodForSubtable; + + /** + * Constructor + * + * @param bool $apiModule + * @param bool $apiMethod + * @param array $request + */ + public function __construct($apiModule = false, $apiMethod = false, $request = array()) + { + $this->apiModule = $apiModule; + $this->apiMethod = $apiMethod; + $this->request = $request; + } + + /** + * This method can be used by subclasses to iterate over data tables that might be + * data table arrays. It calls back the template method self::doManipulate for each table. + * This way, data table arrays can be handled in a transparent fashion. + * + * @param Piwik_DataTable_Array|Piwik_DataTable $dataTable + * @throws Exception + * @return Piwik_DataTable_Array|Piwik_DataTable + */ + protected function manipulate($dataTable) + { + if ($dataTable instanceof Piwik_DataTable_Array) { + return $this->manipulateDataTableArray($dataTable); + } else if ($dataTable instanceof Piwik_DataTable) { + return $this->manipulateDataTable($dataTable); + } else { + return $dataTable; + } + } + + /** + * Manipulates child DataTables of a DataTable_Array. See @manipulate for more info. + */ + protected function manipulateDataTableArray($dataTable) + { + $result = $dataTable->getEmptyClone(); + foreach ($dataTable->getArray() as $tableLabel => $childTable) { + $newTable = $this->manipulate($childTable); + $result->addTable($newTable, $tableLabel); + } + return $result; + } + + /** + * Manipulates a single Piwik_DataTable instance. Derived classes must define + * this function. + */ + protected abstract function manipulateDataTable($dataTable); + + /** + * Load the subtable for a row. + * Returns null if none is found. + * + * @param Piwik_Datatable_Row $row + * @throws Exception + * @return Piwik_DataTable + */ + protected function loadSubtable($dataTable, $row) + { + if (!($this->apiModule && $this->apiMethod && count($this->request))) { + return null; + } + + $request = $this->request; + $idSubTable = $row->getIdSubDataTable(); - if ($idSubTable === null) - { - return null; - } - - $request['idSubtable'] = $idSubTable; - if ($dataTable) - { - $request['date'] = $dataTable->metadata['period']->getDateStart()->toString(); - } - - $class = 'Piwik_'.$this->apiModule.'_API'; + if ($idSubTable === null) { + return null; + } + + $request['idSubtable'] = $idSubTable; + if ($dataTable) { + $request['date'] = $dataTable->metadata['period']->getDateStart()->toString(); + } + + $class = 'Piwik_' . $this->apiModule . '_API'; $method = $this->getApiMethodForSubtable(); - + $this->manipulateSubtableRequest($request); $request['serialize'] = 0; - $request['expanded'] = 0; - - // don't want to run recursive filters on the subtables as they are loaded, - // otherwise the result will be empty in places (or everywhere). instead we - // run it on the flattened table. - unset($request['filter_pattern_recursive']); - - $dataTable = Piwik_API_Proxy::getInstance()->call($class, $method, $request); - $response = new Piwik_API_ResponseBuilder($format = 'original', $request); - $dataTable = $response->getResponse($dataTable); - if (method_exists($dataTable, 'applyQueuedFilters')) - { - $dataTable->applyQueuedFilters(); - } - - return $dataTable; - } - - /** - * In this method, subclasses can clean up the request array for loading subtables - * in order to make Piwik_API_ResponseBuilder behave correctly (e.g. not trigger the - * manipulator again). - * - * @param $request - * @return - */ - protected abstract function manipulateSubtableRequest(&$request); - - /** - * Extract the API method for loading subtables from the meta data - * - * @return string - */ - private function getApiMethodForSubtable() - { - if (!$this->apiMethodForSubtable) - { - $meta = Piwik_API_API::getInstance()->getMetadata('all', $this->apiModule, $this->apiMethod); - if (isset($meta[0]['actionToLoadSubTables'])) - { - $this->apiMethodForSubtable = $meta[0]['actionToLoadSubTables']; - } - else - { - $this->apiMethodForSubtable = $this->apiMethod; - } - } - return $this->apiMethodForSubtable; - } - + $request['expanded'] = 0; + + // don't want to run recursive filters on the subtables as they are loaded, + // otherwise the result will be empty in places (or everywhere). instead we + // run it on the flattened table. + unset($request['filter_pattern_recursive']); + + $dataTable = Piwik_API_Proxy::getInstance()->call($class, $method, $request); + $response = new Piwik_API_ResponseBuilder($format = 'original', $request); + $dataTable = $response->getResponse($dataTable); + if (method_exists($dataTable, 'applyQueuedFilters')) { + $dataTable->applyQueuedFilters(); + } + + return $dataTable; + } + + /** + * In this method, subclasses can clean up the request array for loading subtables + * in order to make Piwik_API_ResponseBuilder behave correctly (e.g. not trigger the + * manipulator again). + * + * @param $request + * @return + */ + protected abstract function manipulateSubtableRequest(&$request); + + /** + * Extract the API method for loading subtables from the meta data + * + * @return string + */ + private function getApiMethodForSubtable() + { + if (!$this->apiMethodForSubtable) { + $meta = Piwik_API_API::getInstance()->getMetadata('all', $this->apiModule, $this->apiMethod); + if (isset($meta[0]['actionToLoadSubTables'])) { + $this->apiMethodForSubtable = $meta[0]['actionToLoadSubTables']; + } else { + $this->apiMethodForSubtable = $this->apiMethod; + } + } + return $this->apiMethodForSubtable; + } + } diff --git a/core/API/DataTableManipulator/Flattener.php b/core/API/DataTableManipulator/Flattener.php index e6cd3c0991..a3360d5618 100644 --- a/core/API/DataTableManipulator/Flattener.php +++ b/core/API/DataTableManipulator/Flattener.php @@ -1,136 +1,126 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * This class is responsible for flattening data tables. - * + * * It loads subtables and combines them into a single table by concatenating the labels. * This manipulator is triggered by using flat=1 in the API request. - * + * * @package Piwik * @subpackage Piwik_API */ class Piwik_API_DataTableManipulator_Flattener extends Piwik_API_DataTableManipulator { - - private $includeAggregateRows = false; - - /** - * If the flattener is used after calling this method, aggregate rows will - * be included in the result. This can be useful when they contain data that - * the leafs don't have (e.g. conversion stats in some cases). - */ - public function includeAggregateRows() - { - $this->includeAggregateRows = true; - } - - /** - * Separator for building recursive labels (or paths) - * @var string - */ - public $recursiveLabelSeparator = ' - '; - /** - * @param Piwik_DataTable $dataTable - * @return Piwik_DataTable|Piwik_DataTable_Array - */ - public function flatten($dataTable) - { - if ($this->apiModule == 'Actions' || $this->apiMethod == 'getWebsites') - { - $this->recursiveLabelSeparator = '/'; - } - - return $this->manipulate($dataTable); - } + private $includeAggregateRows = false; - /** - * Template method called from self::manipulate. - * Flatten each data table. - * - * @param Piwik_DataTable $dataTable - * @return Piwik_DataTable - */ - protected function manipulateDataTable($dataTable) - { - $newDataTable = $dataTable->getEmptyClone(); - foreach ($dataTable->getRows() as $row) - { - $this->flattenRow($row, $newDataTable); - } - return $newDataTable; - } + /** + * If the flattener is used after calling this method, aggregate rows will + * be included in the result. This can be useful when they contain data that + * the leafs don't have (e.g. conversion stats in some cases). + */ + public function includeAggregateRows() + { + $this->includeAggregateRows = true; + } - /** - * @param Piwik_DataTable_Row $row - * @param Piwik_DataTable $dataTable - * @param string $labelPrefix - * @param bool $parentLogo - */ - private function flattenRow(Piwik_DataTable_Row $row, Piwik_DataTable $dataTable, - $labelPrefix = '', $parentLogo = false) { - - $label = $row->getColumn('label'); - if ($label !== false) - { - $label = trim($label); - if (substr($label, 0, 1) == '/' && $this->recursiveLabelSeparator == '/') - { - $label = substr($label, 1); - } - $label = $labelPrefix . $label; - $row->setColumn('label', $label); - } - - $logo = $row->getMetadata('logo'); - if ($logo === false && $parentLogo !== false) - { - $logo = $parentLogo; - $row->setMetadata('logo', $logo); - } - - $subTable = $this->loadSubtable($dataTable, $row); - $row->removeSubtable(); - - if ($subTable === null) - { - if ($this->includeAggregateRows) - { - $row->setMetadata('is_aggregate', 0); - } - $dataTable->addRow($row); - } - else - { - if ($this->includeAggregateRows) - { - $row->setMetadata('is_aggregate', 1); - $dataTable->addRow($row); - } - $prefix = $label . $this->recursiveLabelSeparator; - foreach ($subTable->getRows() as $row) - { - $this->flattenRow($row, $dataTable, $prefix, $logo); - } - } - } + /** + * Separator for building recursive labels (or paths) + * @var string + */ + public $recursiveLabelSeparator = ' - '; - /** - * Remove the flat parameter from the subtable request - * - * @param array $request - */ - protected function manipulateSubtableRequest(&$request) - { - unset($request['flat']); - } + /** + * @param Piwik_DataTable $dataTable + * @return Piwik_DataTable|Piwik_DataTable_Array + */ + public function flatten($dataTable) + { + if ($this->apiModule == 'Actions' || $this->apiMethod == 'getWebsites') { + $this->recursiveLabelSeparator = '/'; + } + + return $this->manipulate($dataTable); + } + + /** + * Template method called from self::manipulate. + * Flatten each data table. + * + * @param Piwik_DataTable $dataTable + * @return Piwik_DataTable + */ + protected function manipulateDataTable($dataTable) + { + $newDataTable = $dataTable->getEmptyClone(); + foreach ($dataTable->getRows() as $row) { + $this->flattenRow($row, $newDataTable); + } + return $newDataTable; + } + + /** + * @param Piwik_DataTable_Row $row + * @param Piwik_DataTable $dataTable + * @param string $labelPrefix + * @param bool $parentLogo + */ + private function flattenRow(Piwik_DataTable_Row $row, Piwik_DataTable $dataTable, + $labelPrefix = '', $parentLogo = false) + { + + $label = $row->getColumn('label'); + if ($label !== false) { + $label = trim($label); + if (substr($label, 0, 1) == '/' && $this->recursiveLabelSeparator == '/') { + $label = substr($label, 1); + } + $label = $labelPrefix . $label; + $row->setColumn('label', $label); + } + + $logo = $row->getMetadata('logo'); + if ($logo === false && $parentLogo !== false) { + $logo = $parentLogo; + $row->setMetadata('logo', $logo); + } + + $subTable = $this->loadSubtable($dataTable, $row); + $row->removeSubtable(); + + if ($subTable === null) { + if ($this->includeAggregateRows) { + $row->setMetadata('is_aggregate', 0); + } + $dataTable->addRow($row); + } else { + if ($this->includeAggregateRows) { + $row->setMetadata('is_aggregate', 1); + $dataTable->addRow($row); + } + $prefix = $label . $this->recursiveLabelSeparator; + foreach ($subTable->getRows() as $row) { + $this->flattenRow($row, $dataTable, $prefix, $logo); + } + } + } + + /** + * Remove the flat parameter from the subtable request + * + * @param array $request + */ + protected function manipulateSubtableRequest(&$request) + { + unset($request['flat']); + } } diff --git a/core/API/DataTableManipulator/LabelFilter.php b/core/API/DataTableManipulator/LabelFilter.php index 246a4b6ebc..f2aa0020da 100644 --- a/core/API/DataTableManipulator/LabelFilter.php +++ b/core/API/DataTableManipulator/LabelFilter.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -13,164 +13,156 @@ * This class is responsible for handling the label parameter that can be * added to every API call. If the parameter is set, only the row with the matching * label is returned. - * + * * The labels passed to this class should be urlencoded. * Some reports use recursive labels (e.g. action reports). Use > to join them. - * + * * This filter does not work when expanded=1 is set because it is designed to load * only the subtables on the path, not all existing subtables (which would happen with * expanded=1). Also, the aim of this filter is to return only the row matching the * label. With expanded=1, the subtables of the matching row would be returned as well. - * + * * @package Piwik * @subpackage Piwik_API */ class Piwik_API_DataTableManipulator_LabelFilter extends Piwik_API_DataTableManipulator { - const SEPARATOR_RECURSIVE_LABEL = '>'; - - private $labels; - private $addEmptyRows; - - /** - * Filter a data table by label. - * The filtered table is returned, which might be a new instance. - * - * $apiModule, $apiMethod and $request are needed load sub-datatables - * for the recursive search. If the label is not recursive, these parameters - * are not needed. - * - * @param string $labels the labels to search for - * @param Piwik_DataTable $dataTable the data table to be filtered - * @param bool $addEmptyRows Whether to add empty rows when a row isn't found - * for a label, or not. - * @return Piwik_DataTable - */ - public function filter($labels, $dataTable, $addEmptyRows = false) - { - if (!is_array($labels)) - { - $labels = array($labels); - } - - $this->labels = $labels; - $this->addEmptyRows = (bool)$addEmptyRows; - return $this->manipulate($dataTable); - } - - /** - * Method for the recursive descend - * - * @param array $labelParts - * @param Piwik_DataTable $dataTable - * @return Piwik_DataTable_Row|false - */ - private function doFilterRecursiveDescend($labelParts, $dataTable) - { - // search for the first part of the tree search + const SEPARATOR_RECURSIVE_LABEL = '>'; + + private $labels; + private $addEmptyRows; + + /** + * Filter a data table by label. + * The filtered table is returned, which might be a new instance. + * + * $apiModule, $apiMethod and $request are needed load sub-datatables + * for the recursive search. If the label is not recursive, these parameters + * are not needed. + * + * @param string $labels the labels to search for + * @param Piwik_DataTable $dataTable the data table to be filtered + * @param bool $addEmptyRows Whether to add empty rows when a row isn't found + * for a label, or not. + * @return Piwik_DataTable + */ + public function filter($labels, $dataTable, $addEmptyRows = false) + { + if (!is_array($labels)) { + $labels = array($labels); + } + + $this->labels = $labels; + $this->addEmptyRows = (bool)$addEmptyRows; + return $this->manipulate($dataTable); + } + + /** + * Method for the recursive descend + * + * @param array $labelParts + * @param Piwik_DataTable $dataTable + * @return Piwik_DataTable_Row|false + */ + private function doFilterRecursiveDescend($labelParts, $dataTable) + { + // search for the first part of the tree search $labelPart = array_shift($labelParts); - - foreach ($this->getLabelVariations($labelPart) as $labelPart) - { - $row = $dataTable->getRowFromLabel($labelPart); - if ($row !== false) - { - break; - } - } - - if ($row === false) - { - // not found - return false; - } - - // end of tree search reached - if (count($labelParts) == 0) - { - return $row; - } - - $subTable = $this->loadSubtable($dataTable, $row); - if ($subTable === null) - { - // no more subtables but label parts left => no match found - return false; - } - - return $this->doFilterRecursiveDescend($labelParts, $subTable); - } - - /** - * Clean up request for Piwik_API_ResponseBuilder to behave correctly - * - * @param $request - */ - protected function manipulateSubtableRequest(&$request) - { - unset($request['label']); - } - - /** - * Use variations of the label to make it easier to specify the desired label - * - * Note: The HTML Encoded version must be tried first, since in Piwik_API_ResponseBuilder the $label is unsanitized - * via Piwik_Common::unsanitizeInputValue. - * - * @param string $label - * @return array - */ - private function getLabelVariations($label) - { - $variations = array(); - $label = trim($label); - - $sanitizedLabel = Piwik_Common::sanitizeInputValue($label); - $variations[] = $sanitizedLabel; - - if ($this->apiModule == 'Actions' - && $this->apiMethod == 'getPageTitles') - { - // special case: the Actions.getPageTitles report prefixes some labels with a blank. - // the blank might be passed by the user but is removed in Piwik_API_Request::getRequestArrayFromString. - $variations[] = ' '.$sanitizedLabel; - $variations[] = ' '.$label; - } - $variations[] = $label; - - return $variations; - } - - /** - * Filter a Piwik_DataTable instance. See @filter for more info. - */ - protected function manipulateDataTable( $dataTable ) - { - $result = $dataTable->getEmptyClone(); - foreach ($this->labels as $labelIdx => $label) - { - $row = null; - foreach ($this->getLabelVariations($label) as $labelVariation) - { - $labelVariation = explode(self::SEPARATOR_RECURSIVE_LABEL, $labelVariation); - $labelVariation = array_map('urldecode', $labelVariation); - - $row = $this->doFilterRecursiveDescend($labelVariation, $dataTable); - if ($row) - { - $result->addRow($row); - break; - } - } - - if (empty($row) - && $this->addEmptyRows) // if no row has been found, add an empty one - { - $row = new Piwik_DataTable_Row(); - $row->setColumn('label', $label); - $result->addRow($row); - } - } - return $result; - } + + foreach ($this->getLabelVariations($labelPart) as $labelPart) { + $row = $dataTable->getRowFromLabel($labelPart); + if ($row !== false) { + break; + } + } + + if ($row === false) { + // not found + return false; + } + + // end of tree search reached + if (count($labelParts) == 0) { + return $row; + } + + $subTable = $this->loadSubtable($dataTable, $row); + if ($subTable === null) { + // no more subtables but label parts left => no match found + return false; + } + + return $this->doFilterRecursiveDescend($labelParts, $subTable); + } + + /** + * Clean up request for Piwik_API_ResponseBuilder to behave correctly + * + * @param $request + */ + protected function manipulateSubtableRequest(&$request) + { + unset($request['label']); + } + + /** + * Use variations of the label to make it easier to specify the desired label + * + * Note: The HTML Encoded version must be tried first, since in Piwik_API_ResponseBuilder the $label is unsanitized + * via Piwik_Common::unsanitizeInputValue. + * + * @param string $label + * @return array + */ + private function getLabelVariations($label) + { + $variations = array(); + $label = trim($label); + + $sanitizedLabel = Piwik_Common::sanitizeInputValue($label); + $variations[] = $sanitizedLabel; + + if ($this->apiModule == 'Actions' + && $this->apiMethod == 'getPageTitles' + ) { + // special case: the Actions.getPageTitles report prefixes some labels with a blank. + // the blank might be passed by the user but is removed in Piwik_API_Request::getRequestArrayFromString. + $variations[] = ' ' . $sanitizedLabel; + $variations[] = ' ' . $label; + } + $variations[] = $label; + + return $variations; + } + + /** + * Filter a Piwik_DataTable instance. See @filter for more info. + */ + protected function manipulateDataTable($dataTable) + { + $result = $dataTable->getEmptyClone(); + foreach ($this->labels as $labelIdx => $label) { + $row = null; + foreach ($this->getLabelVariations($label) as $labelVariation) { + $labelVariation = explode(self::SEPARATOR_RECURSIVE_LABEL, $labelVariation); + $labelVariation = array_map('urldecode', $labelVariation); + + $row = $this->doFilterRecursiveDescend($labelVariation, $dataTable); + if ($row) { + $result->addRow($row); + break; + } + } + + if (empty($row) + && $this->addEmptyRows + ) // if no row has been found, add an empty one + { + $row = new Piwik_DataTable_Row(); + $row->setColumn('label', $label); + $result->addRow($row); + } + } + return $result; + } } diff --git a/core/API/DocumentationGenerator.php b/core/API/DocumentationGenerator.php index 505f35ec04..1c2d9a62a4 100644 --- a/core/API/DocumentationGenerator.php +++ b/core/API/DocumentationGenerator.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,243 +15,220 @@ */ class Piwik_API_DocumentationGenerator { - protected $modulesToHide = array('CoreAdminHome', 'DBStats'); - protected $countPluginsLoaded = 0; + protected $modulesToHide = array('CoreAdminHome', 'DBStats'); + protected $countPluginsLoaded = 0; - /** - * trigger loading all plugins with an API.php file in the Proxy - */ - public function __construct() - { - $plugins = Piwik_PluginsManager::getInstance()->getLoadedPluginsName(); - foreach( $plugins as $plugin ) - { - $plugin = Piwik::unprefixClass($plugin); - try { - Piwik_API_Proxy::getInstance()->registerClass('Piwik_'.$plugin.'_API'); - } - catch(Exception $e){ - } - } - } + /** + * trigger loading all plugins with an API.php file in the Proxy + */ + public function __construct() + { + $plugins = Piwik_PluginsManager::getInstance()->getLoadedPluginsName(); + foreach ($plugins as $plugin) { + $plugin = Piwik::unprefixClass($plugin); + try { + Piwik_API_Proxy::getInstance()->registerClass('Piwik_' . $plugin . '_API'); + } catch (Exception $e) { + } + } + } - /** - * Returns a HTML page containing help for all the successfully loaded APIs. - * For each module it will return a mini help with the method names, parameters to give, - * links to get the result in Xml/Csv/etc - * - * @param bool $outputExampleUrls - * @param string $prefixUrls - * @return string - */ - public function getAllInterfaceString( $outputExampleUrls = true, $prefixUrls = '' ) - { - if(!empty($prefixUrls)) { - $prefixUrls = 'http://demo.piwik.org/'; - } - $str = $toc = ''; - $token_auth = "&token_auth=" . Piwik::getCurrentUserTokenAuth(); - $parametersToSet = array( - 'idSite' => Piwik_Common::getRequestVar('idSite', 1, 'int'), - 'period' => Piwik_Common::getRequestVar('period', 'day', 'string'), - 'date' => Piwik_Common::getRequestVar('date', 'today', 'string') - ); - - foreach(Piwik_API_Proxy::getInstance()->getMetadata() as $class => $info) - { - $moduleName = Piwik_API_Proxy::getInstance()->getModuleNameFromClassName($class); - if(in_array($moduleName, $this->modulesToHide)) - { - continue; - } - $toc .= "<a href='#$moduleName'>$moduleName</a><br/>"; - $str .= "\n<a name='$moduleName' id='$moduleName'></a><h2>Module ".$moduleName."</h2>"; - $str .= "<div class='apiDescription'> ".$info['__documentation'] . " </div>"; - foreach($info as $methodName => $infoMethod) - { - if($methodName=='__documentation') - { - continue; - } - $params = $this->getParametersString($class, $methodName); - $str .= "\n <div class='apiMethod'>- <b>$moduleName.$methodName </b>" . $params . ""; - $str .= '<small>'; - - if($outputExampleUrls) - { - // we prefix all URLs with $prefixUrls - // used when we include this output in the Piwik official documentation for example - $str .= "<span class=\"example\">"; - $exampleUrl = $this->getExampleUrl($class, $methodName, $parametersToSet); - if($exampleUrl !== false) - { - $lastNUrls = ''; - if( preg_match('/(&period)|(&date)/',$exampleUrl)) - { - $exampleUrlRss1 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last10', 'period' => 'day') + $parametersToSet) ; - $exampleUrlRss2 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last5','period' => 'week',) + $parametersToSet ); - $lastNUrls = ", RSS of the last <a target=_blank href='$exampleUrlRss1&format=rss$token_auth&translateColumnNames=1'>10 days</a>"; - } - $exampleUrl = $prefixUrls . $exampleUrl ; - $str .= " [ Example in + /** + * Returns a HTML page containing help for all the successfully loaded APIs. + * For each module it will return a mini help with the method names, parameters to give, + * links to get the result in Xml/Csv/etc + * + * @param bool $outputExampleUrls + * @param string $prefixUrls + * @return string + */ + public function getAllInterfaceString($outputExampleUrls = true, $prefixUrls = '') + { + if (!empty($prefixUrls)) { + $prefixUrls = 'http://demo.piwik.org/'; + } + $str = $toc = ''; + $token_auth = "&token_auth=" . Piwik::getCurrentUserTokenAuth(); + $parametersToSet = array( + 'idSite' => Piwik_Common::getRequestVar('idSite', 1, 'int'), + 'period' => Piwik_Common::getRequestVar('period', 'day', 'string'), + 'date' => Piwik_Common::getRequestVar('date', 'today', 'string') + ); + + foreach (Piwik_API_Proxy::getInstance()->getMetadata() as $class => $info) { + $moduleName = Piwik_API_Proxy::getInstance()->getModuleNameFromClassName($class); + if (in_array($moduleName, $this->modulesToHide)) { + continue; + } + $toc .= "<a href='#$moduleName'>$moduleName</a><br/>"; + $str .= "\n<a name='$moduleName' id='$moduleName'></a><h2>Module " . $moduleName . "</h2>"; + $str .= "<div class='apiDescription'> " . $info['__documentation'] . " </div>"; + foreach ($info as $methodName => $infoMethod) { + if ($methodName == '__documentation') { + continue; + } + $params = $this->getParametersString($class, $methodName); + $str .= "\n <div class='apiMethod'>- <b>$moduleName.$methodName </b>" . $params . ""; + $str .= '<small>'; + + if ($outputExampleUrls) { + // we prefix all URLs with $prefixUrls + // used when we include this output in the Piwik official documentation for example + $str .= "<span class=\"example\">"; + $exampleUrl = $this->getExampleUrl($class, $methodName, $parametersToSet); + if ($exampleUrl !== false) { + $lastNUrls = ''; + if (preg_match('/(&period)|(&date)/', $exampleUrl)) { + $exampleUrlRss1 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last10', 'period' => 'day') + $parametersToSet); + $exampleUrlRss2 = $prefixUrls . $this->getExampleUrl($class, $methodName, array('date' => 'last5', 'period' => 'week',) + $parametersToSet); + $lastNUrls = ", RSS of the last <a target=_blank href='$exampleUrlRss1&format=rss$token_auth&translateColumnNames=1'>10 days</a>"; + } + $exampleUrl = $prefixUrls . $exampleUrl; + $str .= " [ Example in <a target=_blank href='$exampleUrl&format=xml$token_auth'>XML</a>, <a target=_blank href='$exampleUrl&format=JSON$token_auth'>Json</a>, <a target=_blank href='$exampleUrl&format=Tsv$token_auth&translateColumnNames=1'>Tsv (Excel)</a> $lastNUrls ]"; - } - else - { - $str .= " [ No example available ]"; - } - $str .= "</span>"; - } - $str .= '</small>'; - $str .= "</div>\n"; - } - $str .= '<div style="margin:15px;"><a href="#topApiRef" style="color:#95AECB">↑ Back to top</a></div>'; - } - - $str = "<h2 id='topApiRef' name='topApiRef'>Quick access to APIs</h2> + } else { + $str .= " [ No example available ]"; + } + $str .= "</span>"; + } + $str .= '</small>'; + $str .= "</div>\n"; + } + $str .= '<div style="margin:15px;"><a href="#topApiRef" style="color:#95AECB">↑ Back to top</a></div>'; + } + + $str = "<h2 id='topApiRef' name='topApiRef'>Quick access to APIs</h2> $toc $str"; - return $str; - } + return $str; + } + + /** + * Returns a string containing links to examples on how to call a given method on a given API + * It will export links to XML, CSV, HTML, JSON, PHP, etc. + * It will not export links for methods such as deleteSite or deleteUser + * + * @param string $class the class + * @param string $methodName the method + * @param array $parametersToSet parameters to set + * @return string|false when not possible + */ + public function getExampleUrl($class, $methodName, $parametersToSet = array()) + { + $knowExampleDefaultParametersValues = array( + 'access' => 'view', + 'userLogin' => 'test', + 'passwordMd5ied' => 'passwordExample', + 'email' => 'test@example.org', + + 'languageCode' => 'fr', + 'url' => 'http://forum.piwik.org/', + 'apiModule' => 'UserCountry', + 'apiAction' => 'getCountry', + 'lastMinutes' => '30', + 'abandonedCarts' => '0', + 'ip' => '194.57.91.215', + ); + + foreach ($parametersToSet as $name => $value) { + $knowExampleDefaultParametersValues[$name] = $value; + } - /** - * Returns a string containing links to examples on how to call a given method on a given API - * It will export links to XML, CSV, HTML, JSON, PHP, etc. - * It will not export links for methods such as deleteSite or deleteUser - * - * @param string $class the class - * @param string $methodName the method - * @param array $parametersToSet parameters to set - * @return string|false when not possible - */ - public function getExampleUrl($class, $methodName, $parametersToSet = array()) - { - $knowExampleDefaultParametersValues = array( - 'access' => 'view', - 'userLogin' => 'test', - 'passwordMd5ied' => 'passwordExample', - 'email' => 'test@example.org', - - 'languageCode' => 'fr', - 'url' => 'http://forum.piwik.org/', - 'apiModule' => 'UserCountry', - 'apiAction' => 'getCountry', - 'lastMinutes' => '30', - 'abandonedCarts' => '0', - 'ip' => '194.57.91.215', - ); - - foreach($parametersToSet as $name => $value) - { - $knowExampleDefaultParametersValues[$name] = $value; - } - - // no links for these method names - $doNotPrintExampleForTheseMethods = array( - //Sites - 'deleteSite', - 'addSite', - 'updateSite', - 'addSiteAliasUrls', - //Users - 'deleteUser', - 'addUser', - 'updateUser', - 'setUserAccess', - //Goals - 'addGoal', - 'updateGoal', - 'deleteGoal', - ); - - if(in_array($methodName,$doNotPrintExampleForTheseMethods)) - { - return false; - } - - // we try to give an URL example to call the API - $aParameters = Piwik_API_Proxy::getInstance()->getParametersList($class, $methodName); - // Kindly force some known generic parameters to appear in the final list - // the parameter 'format' can be set to all API methods (used in tests) - // the parameter 'hideIdSubDatable' is used for integration tests only - // the parameter 'serialize' sets php outputs human readable, used in integration tests and debug - // the parameter 'language' sets the language for the response (eg. country names) - // the parameter 'flat' reduces a hierarchical table to a single level by concatenating labels - // the parameter 'include_aggregate_rows' can be set to include inner nodes in flat reports - // the parameter 'translateColumnNames' can be set to translate metric names in csv/tsv exports - $aParameters['format'] = false; - $aParameters['hideIdSubDatable'] = false; - $aParameters['serialize'] = false; - $aParameters['language'] = false; - $aParameters['translateColumnNames'] = false; + // no links for these method names + $doNotPrintExampleForTheseMethods = array( + //Sites + 'deleteSite', + 'addSite', + 'updateSite', + 'addSiteAliasUrls', + //Users + 'deleteUser', + 'addUser', + 'updateUser', + 'setUserAccess', + //Goals + 'addGoal', + 'updateGoal', + 'deleteGoal', + ); + + if (in_array($methodName, $doNotPrintExampleForTheseMethods)) { + return false; + } + + // we try to give an URL example to call the API + $aParameters = Piwik_API_Proxy::getInstance()->getParametersList($class, $methodName); + // Kindly force some known generic parameters to appear in the final list + // the parameter 'format' can be set to all API methods (used in tests) + // the parameter 'hideIdSubDatable' is used for integration tests only + // the parameter 'serialize' sets php outputs human readable, used in integration tests and debug + // the parameter 'language' sets the language for the response (eg. country names) + // the parameter 'flat' reduces a hierarchical table to a single level by concatenating labels + // the parameter 'include_aggregate_rows' can be set to include inner nodes in flat reports + // the parameter 'translateColumnNames' can be set to translate metric names in csv/tsv exports + $aParameters['format'] = false; + $aParameters['hideIdSubDatable'] = false; + $aParameters['serialize'] = false; + $aParameters['language'] = false; + $aParameters['translateColumnNames'] = false; $aParameters['label'] = false; - $aParameters['flat'] = false; - $aParameters['include_aggregate_rows'] = false; + $aParameters['flat'] = false; + $aParameters['include_aggregate_rows'] = false; $aParameters['filter_limit'] = false; //@review without adding this, I can not set filter_limit in $otherRequestParameters integration tests $aParameters['filter_sort_column'] = false; //@review without adding this, I can not set filter_sort_column in $otherRequestParameters integration tests $aParameters['filter_truncate'] = false; $aParameters['hideColumns'] = false; $aParameters['showColumns'] = false; $aParameters['filter_pattern_recursive'] = false; - - $moduleName = Piwik_API_Proxy::getInstance()->getModuleNameFromClassName($class); - $aParameters = array_merge(array('module' => 'API', 'method' => $moduleName.'.'.$methodName), $aParameters); - - foreach($aParameters as $nameVariable => &$defaultValue) - { - if(isset($knowExampleDefaultParametersValues[$nameVariable])) - { - $defaultValue = $knowExampleDefaultParametersValues[$nameVariable]; - } - // if there isn't a default value for a given parameter, - // we need a 'know default value' or we can't generate the link - elseif($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue) - { - return false; - } - } - return '?'.Piwik_Url::getQueryStringFromParameters($aParameters); - } - - - /** - * Returns the methods $class.$name parameters (and default value if provided) as a string. - * - * @param string $class The class name - * @param string $name The method name - * @return string For example "(idSite, period, date = 'today')" - */ - public function getParametersString($class, $name) - { - $aParameters = Piwik_API_Proxy::getInstance()->getParametersList($class, $name); - $asParameters = array(); - foreach($aParameters as $nameVariable=> $defaultValue) - { - // Do not show API parameters starting with _ - // They are supposed to be used only in internal API calls - if(strpos($nameVariable, '_') === 0) - { - continue; - } - $str = $nameVariable; - if(!($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue)) - { - if (is_array($defaultValue)) - { - $str .= " = 'Array'"; - } - else - { - $str .= " = '$defaultValue'"; - } - } - $asParameters[] = $str; - } - $sParameters = implode(", ", $asParameters); - return "($sParameters)"; - } + + $moduleName = Piwik_API_Proxy::getInstance()->getModuleNameFromClassName($class); + $aParameters = array_merge(array('module' => 'API', 'method' => $moduleName . '.' . $methodName), $aParameters); + + foreach ($aParameters as $nameVariable => &$defaultValue) { + if (isset($knowExampleDefaultParametersValues[$nameVariable])) { + $defaultValue = $knowExampleDefaultParametersValues[$nameVariable]; + } // if there isn't a default value for a given parameter, + // we need a 'know default value' or we can't generate the link + elseif ($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue) { + return false; + } + } + return '?' . Piwik_Url::getQueryStringFromParameters($aParameters); + } + + + /** + * Returns the methods $class.$name parameters (and default value if provided) as a string. + * + * @param string $class The class name + * @param string $name The method name + * @return string For example "(idSite, period, date = 'today')" + */ + public function getParametersString($class, $name) + { + $aParameters = Piwik_API_Proxy::getInstance()->getParametersList($class, $name); + $asParameters = array(); + foreach ($aParameters as $nameVariable => $defaultValue) { + // Do not show API parameters starting with _ + // They are supposed to be used only in internal API calls + if (strpos($nameVariable, '_') === 0) { + continue; + } + $str = $nameVariable; + if (!($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue)) { + if (is_array($defaultValue)) { + $str .= " = 'Array'"; + } else { + $str .= " = '$defaultValue'"; + } + } + $asParameters[] = $str; + } + $sParameters = implode(", ", $asParameters); + return "($sParameters)"; + } } diff --git a/core/API/Proxy.php b/core/API/Proxy.php index 3619e0fc87..727d291b29 100644 --- a/core/API/Proxy.php +++ b/core/API/Proxy.php @@ -1,419 +1,402 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * To differentiate between "no value" and default value of null - * + * * @package Piwik * @subpackage Piwik_API */ -class Piwik_API_Proxy_NoDefaultValue {} +class Piwik_API_Proxy_NoDefaultValue +{ +} /** - * Proxy is a singleton that has the knowledge of every method available, their parameters + * Proxy is a singleton that has the knowledge of every method available, their parameters * and default values. - * Proxy receives all the API calls requests via call() and forwards them to the right - * object, with the parameters in the right order. - * + * Proxy receives all the API calls requests via call() and forwards them to the right + * object, with the parameters in the right order. + * * It will also log the performance of API calls (time spent, parameter values, etc.) if logger available - * + * * @package Piwik * @subpackage Piwik_API */ class Piwik_API_Proxy { - // array of already registered plugins names - protected $alreadyRegistered = array(); - - private $metadataArray = array(); - private $hideIgnoredFunctions = true; - - // when a parameter doesn't have a default value we use this - private $noDefaultValue; - - /** - * Singleton instance - * @var self|null - */ - static private $instance = null; - - /** - * protected constructor - */ - protected function __construct() - { - $this->noDefaultValue = new Piwik_API_Proxy_NoDefaultValue(); - } - - /** - * Singleton, returns instance - * - * @return Piwik_API_Proxy - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Returns array containing reflection meta data for all the loaded classes - * eg. number of parameters, method names, etc. - * - * @return array - */ - public function getMetadata() - { - ksort($this->metadataArray); - return $this->metadataArray; - } - - /** - * Registers the API information of a given module. - * - * The module to be registered must be - * - a singleton (providing a getInstance() method) - * - the API file must be located in plugins/ModuleName/API.php - * for example plugins/Referers/API.php - * - * The method will introspect the methods, their parameters, etc. - * - * @param string $className ModuleName eg. "Piwik_UserSettings_API" - */ - public function registerClass( $className ) - { - if(isset($this->alreadyRegistered[$className])) - { - return; - } - $this->includeApiFile( $className ); - $this->checkClassIsSingleton($className); - - $rClass = new ReflectionClass($className); - foreach($rClass->getMethods() as $method) - { - $this->loadMethodMetadata($className, $method); - } - - $this->setDocumentation($rClass, $className); - $this->alreadyRegistered[$className] = true; - } - - /** - * Will be displayed in the API page - * - * @param ReflectionClass $rClass Instance of ReflectionClass - * @param string $className Name of the class - */ - private function setDocumentation($rClass, $className) - { - // Doc comment - $doc = $rClass->getDocComment(); - $doc = str_replace(" * ".PHP_EOL, "<br>", $doc); - - // boldify the first line only if there is more than one line, otherwise too much bold - if(substr_count($doc, '<br>') > 1) - { - $firstLineBreak = strpos($doc, "<br>"); - $doc = "<div class='apiFirstLine'>".substr($doc, 0, $firstLineBreak)."</div>".substr($doc,$firstLineBreak+strlen("<br>")); - } - $doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc); - $doc = str_replace(array("\t","\n", "/**", "*/", " * "," *"," ", "\t*", " * @package"), " ", $doc); - $this->metadataArray[$className]['__documentation'] = $doc; - } - - /** - * Returns number of classes already loaded - * @return int - */ - public function getCountRegisteredClasses() - { - return count($this->alreadyRegistered); - } - - /** - * Will execute $className->$methodName($parametersValues) - * If any error is detected (wrong number of parameters, method not found, class not found, etc.) - * it will throw an exception - * - * It also logs the API calls, with the parameters values, the returned value, the performance, etc. - * You can enable logging in config/global.ini.php (log_api_call) - * - * @param string $className The class name (eg. Piwik_Referers_API) - * @param string $methodName The method name - * @param array $parametersRequest The parameters pairs (name=>value) - * - * @return mixed|null - * @throws Exception|Piwik_Access_NoAccessException - */ - public function call($className, $methodName, $parametersRequest ) - { - $returnedValue = null; - - // Temporarily sets the Request array to this API call context - $saveGET = $_GET; - foreach($parametersRequest as $param => $value) { - $_GET[$param] = $value; - } - - try { - $this->registerClass($className); - - // instanciate the object - $object = call_user_func(array($className, "getInstance")); - - // check method exists - $this->checkMethodExists($className, $methodName); - - // get the list of parameters required by the method - $parameterNamesDefaultValues = $this->getParametersList($className, $methodName); - - // load parameters in the right order, etc. - $finalParameters = $this->getRequestParametersArray( $parameterNamesDefaultValues, $parametersRequest ); - - // start the timer - $timer = new Piwik_Timer(); - - // call the method - $returnedValue = call_user_func_array(array($object, $methodName), $finalParameters); - - // allow plugins to manipulate the value - if (substr($className, 0, 6) == 'Piwik_' && substr($className, -4) == '_API') - { - $pluginName = substr($className, 6, -4); - Piwik_PostEvent('API.Proxy.processReturnValue', $returnedValue, array( - 'className' => $className, - 'module' => $pluginName, - 'action' => $methodName, - 'parameters' => &$parametersRequest - )); - } - - // Restore the request - $_GET = $saveGET; - - // log the API Call - try { - Zend_Registry::get('logger_api_call')->logEvent( - $className, - $methodName, - $parameterNamesDefaultValues, - $finalParameters, - $timer->getTimeMs(), - $returnedValue - ); - } catch (Exception $e) { - // logger can fail (eg. Tracker request) - } - } catch (Exception $e) { - $_GET = $saveGET; - throw $e; - } - - return $returnedValue; - } - - /** - * Returns the parameters names and default values for the method $name - * of the class $class - * - * @param string $class The class name - * @param string $name The method name - * @return array Format array( - * 'testParameter' => null, // no default value - * 'life' => 42, // default value = 42 - * 'date' => 'yesterday', - * ); - */ - public function getParametersList($class, $name) - { - return $this->metadataArray[$class][$name]['parameters']; - } - - /** - * Returns the 'moduleName' part of 'Piwik_moduleName_API' classname - * - * @param string $className "Piwik_Referers_API" - * @return string "Referers" - */ - public function getModuleNameFromClassName( $className ) - { - return str_replace(array('Piwik_', '_API'), '', $className); - } - - /** - * Sets whether to hide '@ignore'd functions from method metadata or not. - * - * @param bool $hideIgnoredFunctions - */ - public function setHideIgnoredFunctions( $hideIgnoredFunctions ) - { - $this->hideIgnoredFunctions = $hideIgnoredFunctions; - - // make sure metadata gets reloaded - $this->alreadyRegistered = array(); - $this->metadataArray = array(); - } - - /** - * Returns an array containing the values of the parameters to pass to the method to call - * - * @param array $requiredParameters array of (parameter name, default value) - * @param array $parametersRequest - * @throws Exception - * @return array values to pass to the function call - */ - private function getRequestParametersArray( $requiredParameters, $parametersRequest ) - { - $finalParameters = array(); - foreach($requiredParameters as $name => $defaultValue) - { - try{ - if($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue) - { - $requestValue = Piwik_Common::getRequestVar($name, null, null, $parametersRequest); - } - else - { - try{ - $requestValue = Piwik_Common::getRequestVar($name, $defaultValue, null, $parametersRequest); - } catch(Exception $e) { - // Special case: empty parameter in the URL, should return the empty string - if(isset($parametersRequest[$name]) - && $parametersRequest[$name] === '') - { - $requestValue = ''; - } - else - { - $requestValue = $defaultValue; - } - } - } - } catch(Exception $e) { - throw new Exception(Piwik_TranslateException('General_PleaseSpecifyValue', array($name))); - } - $finalParameters[] = $requestValue; - } - return $finalParameters; - } - - /** - * Includes the class Piwik_UserSettings_API by looking up plugins/UserSettings/API.php - * - * @param string $fileName api class name eg. "Piwik_UserSettings_API" - * @throws Exception - */ - private function includeApiFile($fileName) - { - $module = self::getModuleNameFromClassName($fileName); - $path = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/API.php'; - - if(is_readable($path)) - { - require_once $path; // prefixed by PIWIK_INCLUDE_PATH - } - else - { - throw new Exception("API module $module not found."); - } - } - - /** - * @param string $class name of a class - * @param ReflectionMethod $method instance of ReflectionMethod - */ - private function loadMethodMetadata($class, $method) - { - if($method->isPublic() - && !$method->isConstructor() - && $method->getName() != 'getInstance' - && false === strstr($method->getDocComment(), '@deprecated') - && (!$this->hideIgnoredFunctions || false === strstr($method->getDocComment(), '@ignore')) - ) - { - $name = $method->getName(); - $parameters = $method->getParameters(); - - $aParameters = array(); - foreach($parameters as $parameter) - { - $nameVariable = $parameter->getName(); - - $defaultValue = $this->noDefaultValue; - if($parameter->isDefaultValueAvailable()) - { - $defaultValue = $parameter->getDefaultValue(); - } - - $aParameters[$nameVariable] = $defaultValue; - } - $this->metadataArray[$class][$name]['parameters'] = $aParameters; - $this->metadataArray[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters(); - } - } - - /** - * Checks that the method exists in the class - * - * @param string $className The class name - * @param string $methodName The method name - * @throws Exception If the method is not found - */ - private function checkMethodExists($className, $methodName) - { - if(!$this->isMethodAvailable($className, $methodName)) - { - throw new Exception(Piwik_TranslateException('General_ExceptionMethodNotFound', array($methodName,$className))); - } - } - - /** - * Returns the number of required parameters (parameters without default values). - * - * @param string $class The class name - * @param string $name The method name - * @return int The number of required parameters - */ - private function getNumberOfRequiredParameters($class, $name) - { - return $this->metadataArray[$class][$name]['numberOfRequiredParameters']; - } - - /** - * Returns true if the method is found in the API of the given class name. - * - * @param string $className The class name - * @param string $methodName The method name - * @return bool - */ - private function isMethodAvailable( $className, $methodName) - { - return isset($this->metadataArray[$className][$methodName]); - } - - /** - * Checks that the class is a Singleton (presence of the getInstance() method) - * - * @param string $className The class name - * @throws Exception If the class is not a Singleton - */ - private function checkClassIsSingleton($className) - { - if(!method_exists($className, "getInstance")) - { - throw new Exception("Objects that provide an API must be Singleton and have a 'static public function getInstance()' method."); - } - } + // array of already registered plugins names + protected $alreadyRegistered = array(); + + private $metadataArray = array(); + private $hideIgnoredFunctions = true; + + // when a parameter doesn't have a default value we use this + private $noDefaultValue; + + /** + * Singleton instance + * @var self|null + */ + static private $instance = null; + + /** + * protected constructor + */ + protected function __construct() + { + $this->noDefaultValue = new Piwik_API_Proxy_NoDefaultValue(); + } + + /** + * Singleton, returns instance + * + * @return Piwik_API_Proxy + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Returns array containing reflection meta data for all the loaded classes + * eg. number of parameters, method names, etc. + * + * @return array + */ + public function getMetadata() + { + ksort($this->metadataArray); + return $this->metadataArray; + } + + /** + * Registers the API information of a given module. + * + * The module to be registered must be + * - a singleton (providing a getInstance() method) + * - the API file must be located in plugins/ModuleName/API.php + * for example plugins/Referers/API.php + * + * The method will introspect the methods, their parameters, etc. + * + * @param string $className ModuleName eg. "Piwik_UserSettings_API" + */ + public function registerClass($className) + { + if (isset($this->alreadyRegistered[$className])) { + return; + } + $this->includeApiFile($className); + $this->checkClassIsSingleton($className); + + $rClass = new ReflectionClass($className); + foreach ($rClass->getMethods() as $method) { + $this->loadMethodMetadata($className, $method); + } + + $this->setDocumentation($rClass, $className); + $this->alreadyRegistered[$className] = true; + } + + /** + * Will be displayed in the API page + * + * @param ReflectionClass $rClass Instance of ReflectionClass + * @param string $className Name of the class + */ + private function setDocumentation($rClass, $className) + { + // Doc comment + $doc = $rClass->getDocComment(); + $doc = str_replace(" * " . PHP_EOL, "<br>", $doc); + + // boldify the first line only if there is more than one line, otherwise too much bold + if (substr_count($doc, '<br>') > 1) { + $firstLineBreak = strpos($doc, "<br>"); + $doc = "<div class='apiFirstLine'>" . substr($doc, 0, $firstLineBreak) . "</div>" . substr($doc, $firstLineBreak + strlen("<br>")); + } + $doc = preg_replace("/(@package)[a-z _A-Z]*/", "", $doc); + $doc = str_replace(array("\t", "\n", "/**", "*/", " * ", " *", " ", "\t*", " * @package"), " ", $doc); + $this->metadataArray[$className]['__documentation'] = $doc; + } + + /** + * Returns number of classes already loaded + * @return int + */ + public function getCountRegisteredClasses() + { + return count($this->alreadyRegistered); + } + + /** + * Will execute $className->$methodName($parametersValues) + * If any error is detected (wrong number of parameters, method not found, class not found, etc.) + * it will throw an exception + * + * It also logs the API calls, with the parameters values, the returned value, the performance, etc. + * You can enable logging in config/global.ini.php (log_api_call) + * + * @param string $className The class name (eg. Piwik_Referers_API) + * @param string $methodName The method name + * @param array $parametersRequest The parameters pairs (name=>value) + * + * @return mixed|null + * @throws Exception|Piwik_Access_NoAccessException + */ + public function call($className, $methodName, $parametersRequest) + { + $returnedValue = null; + + // Temporarily sets the Request array to this API call context + $saveGET = $_GET; + foreach ($parametersRequest as $param => $value) { + $_GET[$param] = $value; + } + + try { + $this->registerClass($className); + + // instanciate the object + $object = call_user_func(array($className, "getInstance")); + + // check method exists + $this->checkMethodExists($className, $methodName); + + // get the list of parameters required by the method + $parameterNamesDefaultValues = $this->getParametersList($className, $methodName); + + // load parameters in the right order, etc. + $finalParameters = $this->getRequestParametersArray($parameterNamesDefaultValues, $parametersRequest); + + // start the timer + $timer = new Piwik_Timer(); + + // call the method + $returnedValue = call_user_func_array(array($object, $methodName), $finalParameters); + + // allow plugins to manipulate the value + if (substr($className, 0, 6) == 'Piwik_' && substr($className, -4) == '_API') { + $pluginName = substr($className, 6, -4); + Piwik_PostEvent('API.Proxy.processReturnValue', $returnedValue, array( + 'className' => $className, + 'module' => $pluginName, + 'action' => $methodName, + 'parameters' => &$parametersRequest + )); + } + + // Restore the request + $_GET = $saveGET; + + // log the API Call + try { + Zend_Registry::get('logger_api_call')->logEvent( + $className, + $methodName, + $parameterNamesDefaultValues, + $finalParameters, + $timer->getTimeMs(), + $returnedValue + ); + } catch (Exception $e) { + // logger can fail (eg. Tracker request) + } + } catch (Exception $e) { + $_GET = $saveGET; + throw $e; + } + + return $returnedValue; + } + + /** + * Returns the parameters names and default values for the method $name + * of the class $class + * + * @param string $class The class name + * @param string $name The method name + * @return array Format array( + * 'testParameter' => null, // no default value + * 'life' => 42, // default value = 42 + * 'date' => 'yesterday', + * ); + */ + public function getParametersList($class, $name) + { + return $this->metadataArray[$class][$name]['parameters']; + } + + /** + * Returns the 'moduleName' part of 'Piwik_moduleName_API' classname + * + * @param string $className "Piwik_Referers_API" + * @return string "Referers" + */ + public function getModuleNameFromClassName($className) + { + return str_replace(array('Piwik_', '_API'), '', $className); + } + + /** + * Sets whether to hide '@ignore'd functions from method metadata or not. + * + * @param bool $hideIgnoredFunctions + */ + public function setHideIgnoredFunctions($hideIgnoredFunctions) + { + $this->hideIgnoredFunctions = $hideIgnoredFunctions; + + // make sure metadata gets reloaded + $this->alreadyRegistered = array(); + $this->metadataArray = array(); + } + + /** + * Returns an array containing the values of the parameters to pass to the method to call + * + * @param array $requiredParameters array of (parameter name, default value) + * @param array $parametersRequest + * @throws Exception + * @return array values to pass to the function call + */ + private function getRequestParametersArray($requiredParameters, $parametersRequest) + { + $finalParameters = array(); + foreach ($requiredParameters as $name => $defaultValue) { + try { + if ($defaultValue instanceof Piwik_API_Proxy_NoDefaultValue) { + $requestValue = Piwik_Common::getRequestVar($name, null, null, $parametersRequest); + } else { + try { + $requestValue = Piwik_Common::getRequestVar($name, $defaultValue, null, $parametersRequest); + } catch (Exception $e) { + // Special case: empty parameter in the URL, should return the empty string + if (isset($parametersRequest[$name]) + && $parametersRequest[$name] === '' + ) { + $requestValue = ''; + } else { + $requestValue = $defaultValue; + } + } + } + } catch (Exception $e) { + throw new Exception(Piwik_TranslateException('General_PleaseSpecifyValue', array($name))); + } + $finalParameters[] = $requestValue; + } + return $finalParameters; + } + + /** + * Includes the class Piwik_UserSettings_API by looking up plugins/UserSettings/API.php + * + * @param string $fileName api class name eg. "Piwik_UserSettings_API" + * @throws Exception + */ + private function includeApiFile($fileName) + { + $module = self::getModuleNameFromClassName($fileName); + $path = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/API.php'; + + if (is_readable($path)) { + require_once $path; // prefixed by PIWIK_INCLUDE_PATH + } else { + throw new Exception("API module $module not found."); + } + } + + /** + * @param string $class name of a class + * @param ReflectionMethod $method instance of ReflectionMethod + */ + private function loadMethodMetadata($class, $method) + { + if ($method->isPublic() + && !$method->isConstructor() + && $method->getName() != 'getInstance' + && false === strstr($method->getDocComment(), '@deprecated') + && (!$this->hideIgnoredFunctions || false === strstr($method->getDocComment(), '@ignore')) + ) { + $name = $method->getName(); + $parameters = $method->getParameters(); + + $aParameters = array(); + foreach ($parameters as $parameter) { + $nameVariable = $parameter->getName(); + + $defaultValue = $this->noDefaultValue; + if ($parameter->isDefaultValueAvailable()) { + $defaultValue = $parameter->getDefaultValue(); + } + + $aParameters[$nameVariable] = $defaultValue; + } + $this->metadataArray[$class][$name]['parameters'] = $aParameters; + $this->metadataArray[$class][$name]['numberOfRequiredParameters'] = $method->getNumberOfRequiredParameters(); + } + } + + /** + * Checks that the method exists in the class + * + * @param string $className The class name + * @param string $methodName The method name + * @throws Exception If the method is not found + */ + private function checkMethodExists($className, $methodName) + { + if (!$this->isMethodAvailable($className, $methodName)) { + throw new Exception(Piwik_TranslateException('General_ExceptionMethodNotFound', array($methodName, $className))); + } + } + + /** + * Returns the number of required parameters (parameters without default values). + * + * @param string $class The class name + * @param string $name The method name + * @return int The number of required parameters + */ + private function getNumberOfRequiredParameters($class, $name) + { + return $this->metadataArray[$class][$name]['numberOfRequiredParameters']; + } + + /** + * Returns true if the method is found in the API of the given class name. + * + * @param string $className The class name + * @param string $methodName The method name + * @return bool + */ + private function isMethodAvailable($className, $methodName) + { + return isset($this->metadataArray[$className][$methodName]); + } + + /** + * Checks that the class is a Singleton (presence of the getInstance() method) + * + * @param string $className The class name + * @throws Exception If the class is not a Singleton + */ + private function checkClassIsSingleton($className) + { + if (!method_exists($className, "getInstance")) { + throw new Exception("Objects that provide an API must be Singleton and have a 'static public function getInstance()' method."); + } + } } diff --git a/core/API/Request.php b/core/API/Request.php index 1c0b970cf3..2abf779c52 100644 --- a/core/API/Request.php +++ b/core/API/Request.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,183 +12,175 @@ /** * An API request is the object used to make a call to the API and get the result. * The request has the format of a normal GET request, ie. parameter_1=X¶meter_2=Y - * + * * You can use this object from anywhere in piwik (inside plugins for example). * You can even call it outside of piwik using the REST API over http * or in a php script on the same server as piwik, by including piwik/index.php * (see examples in the documentation http://piwik.org/docs/analytics-api) - * - * Example: + * + * Example: * $request = new Piwik_API_Request(' - * method=UserSettings.getWideScreen - * &idSite=1 - * &date=yesterday - * &period=week - * &format=xml - * &filter_limit=5 - * &filter_offset=0 - * '); - * $result = $request->process(); + * method=UserSettings.getWideScreen + * &idSite=1 + * &date=yesterday + * &period=week + * &format=xml + * &filter_limit=5 + * &filter_offset=0 + * '); + * $result = $request->process(); * echo $result; - * + * * @see http://piwik.org/docs/analytics-api * @package Piwik * @subpackage Piwik_API */ class Piwik_API_Request -{ - protected $request = null; - - /** - * Returns the request array as string - * - * @param string|array $request - * @return array|null - */ - static public function getRequestArrayFromString($request) - { - $defaultRequest = $_GET + $_POST; - $requestArray = $defaultRequest; - - if(!is_null($request)) - { - if(is_array($request)) - { +{ + protected $request = null; + + /** + * Returns the request array as string + * + * @param string|array $request + * @return array|null + */ + static public function getRequestArrayFromString($request) + { + $defaultRequest = $_GET + $_POST; + $requestArray = $defaultRequest; + + if (!is_null($request)) { + if (is_array($request)) { $url = array(); - foreach ($request as $key => $value) - { + foreach ($request as $key => $value) { $url[] = $key . "=" . $value; } $request = implode("&", $url); } - $request = trim($request); - $request = str_replace(array("\n","\t"),'', $request); - parse_str($request, $requestArray); - - $requestArray = $requestArray + $defaultRequest; - } - - foreach($requestArray as &$element) - { - if(!is_array($element)) - { - $element = trim($element); - } - } - return $requestArray; - } - - /** - * Constructs the request to the API, given the request url - * - * @param string $request GET request that defines the API call (must at least contain a "method" parameter) - * Example: method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week&format=xml - * If a request is not provided, then we use the $_GET and $_POST superglobal and fetch - * the values directly from the HTTP GET query. - */ - function __construct($request = null) - { - $this->request = self::getRequestArrayFromString($request); - } - - /** - * Handles the request to the API. - * It first checks that the method called (parameter 'method') is available in the module (it means that the method exists and is public) - * It then reads the parameters from the request string and throws an exception if there are missing parameters. - * It then calls the API Proxy which will call the requested method. - * - * @throws Piwik_FrontController_PluginDeactivatedException - * @return mixed The data resulting from the API call - */ - public function process() - { - // read the format requested for the output data - $outputFormat = strtolower(Piwik_Common::getRequestVar('format', 'xml', 'string', $this->request)); - - // create the response - $response = new Piwik_API_ResponseBuilder($outputFormat, $this->request); - - try { - // read parameters - $moduleMethod = Piwik_Common::getRequestVar('method', null, 'string', $this->request); - - list($module, $method) = $this->extractModuleAndMethod($moduleMethod); - - if(!Piwik_PluginsManager::getInstance()->isPluginActivated($module)) - { - throw new Piwik_FrontController_PluginDeactivatedException($module); - } - $moduleClass = "Piwik_" . $module . "_API"; - - self::reloadAuthUsingTokenAuth($this->request); - - // call the method - $returnedValue = Piwik_API_Proxy::getInstance()->call($moduleClass, $method, $this->request); - - $toReturn = $response->getResponse($returnedValue, $module, $method); - } catch(Exception $e ) { - $toReturn = $response->getResponseException( $e ); - } - return $toReturn; - } - - /** - * 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 array $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(); - Piwik::raiseMemoryLimitIfNecessary(); - } - } - - /** - * Returns array( $class, $method) from the given string $class.$method - * - * @param string $parameter - * @throws Exception - * @return array - */ - private function extractModuleAndMethod($parameter) - { - $a = explode('.',$parameter); - if(count($a) != 2) - { - throw new Exception("The method name is invalid. Expected 'module.methodName'"); - } - return $a; - } - - /** - * Helper method to process an API request using the variables in $_GET and $_POST. - * - * @param string $method The API method to call, ie, Actions.getPageTitles - * @param array $paramOverride The parameter name-value pairs to use instead of what's - * in $_GET & $_POST. - * @param mixed The result of the API request. - */ - public static function processRequest( $method, $paramOverride = array() ) - { - // set up request params - $params = $_GET + $_POST; - $params['format'] = 'original'; - $params['module'] = 'API'; - $params['method'] = $method; - $params = $paramOverride + $params; - - // process request - $request = new Piwik_API_Request($params); - return $request->process(); - } + $request = trim($request); + $request = str_replace(array("\n", "\t"), '', $request); + parse_str($request, $requestArray); + + $requestArray = $requestArray + $defaultRequest; + } + + foreach ($requestArray as &$element) { + if (!is_array($element)) { + $element = trim($element); + } + } + return $requestArray; + } + + /** + * Constructs the request to the API, given the request url + * + * @param string $request GET request that defines the API call (must at least contain a "method" parameter) + * Example: method=UserSettings.getWideScreen&idSite=1&date=yesterday&period=week&format=xml + * If a request is not provided, then we use the $_GET and $_POST superglobal and fetch + * the values directly from the HTTP GET query. + */ + function __construct($request = null) + { + $this->request = self::getRequestArrayFromString($request); + } + + /** + * Handles the request to the API. + * It first checks that the method called (parameter 'method') is available in the module (it means that the method exists and is public) + * It then reads the parameters from the request string and throws an exception if there are missing parameters. + * It then calls the API Proxy which will call the requested method. + * + * @throws Piwik_FrontController_PluginDeactivatedException + * @return mixed The data resulting from the API call + */ + public function process() + { + // read the format requested for the output data + $outputFormat = strtolower(Piwik_Common::getRequestVar('format', 'xml', 'string', $this->request)); + + // create the response + $response = new Piwik_API_ResponseBuilder($outputFormat, $this->request); + + try { + // read parameters + $moduleMethod = Piwik_Common::getRequestVar('method', null, 'string', $this->request); + + list($module, $method) = $this->extractModuleAndMethod($moduleMethod); + + if (!Piwik_PluginsManager::getInstance()->isPluginActivated($module)) { + throw new Piwik_FrontController_PluginDeactivatedException($module); + } + $moduleClass = "Piwik_" . $module . "_API"; + + self::reloadAuthUsingTokenAuth($this->request); + + // call the method + $returnedValue = Piwik_API_Proxy::getInstance()->call($moduleClass, $method, $this->request); + + $toReturn = $response->getResponse($returnedValue, $module, $method); + } catch (Exception $e) { + $toReturn = $response->getResponseException($e); + } + return $toReturn; + } + + /** + * 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 array $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(); + Piwik::raiseMemoryLimitIfNecessary(); + } + } + + /** + * Returns array( $class, $method) from the given string $class.$method + * + * @param string $parameter + * @throws Exception + * @return array + */ + private function extractModuleAndMethod($parameter) + { + $a = explode('.', $parameter); + if (count($a) != 2) { + throw new Exception("The method name is invalid. Expected 'module.methodName'"); + } + return $a; + } + + /** + * Helper method to process an API request using the variables in $_GET and $_POST. + * + * @param string $method The API method to call, ie, Actions.getPageTitles + * @param array $paramOverride The parameter name-value pairs to use instead of what's + * in $_GET & $_POST. + * @param mixed The result of the API request. + */ + public static function processRequest($method, $paramOverride = array()) + { + // set up request params + $params = $_GET + $_POST; + $params['format'] = 'original'; + $params['module'] = 'API'; + $params['method'] = $method; + $params = $paramOverride + $params; + + // process request + $request = new Piwik_API_Request($params); + return $request->process(); + } } diff --git a/core/API/ResponseBuilder.php b/core/API/ResponseBuilder.php index 07df53ce7e..6830e34c0e 100644 --- a/core/API/ResponseBuilder.php +++ b/core/API/ResponseBuilder.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,495 +15,452 @@ */ class Piwik_API_ResponseBuilder { - private $request = null; - private $outputFormat = null; - + private $request = null; + private $outputFormat = null; + private $apiModule = false; private $apiMethod = false; - /** - * @param string $outputFormat - * @param array $request - */ - public function __construct($outputFormat, $request = array()) - { - $this->request = $request; - $this->outputFormat = $outputFormat; - } - - /** - * This method processes the data resulting from the API call. - * - * - If the data resulted from the API call is a Piwik_DataTable then - * - we apply the standard filters if the parameters have been found - * in the URL. For example to offset,limit the Table you can add the following parameters to any API - * call that returns a DataTable: filter_limit=10&filter_offset=20 - * - we apply the filters that have been previously queued on the DataTable - * @see Piwik_DataTable::queueFilter() - * - we apply the renderer that generate the DataTable in a given format (XML, PHP, HTML, JSON, etc.) - * the format can be changed using the 'format' parameter in the request. - * Example: format=xml - * - * - If there is nothing returned (void) we display a standard success message - * - * - If there is a PHP array returned, we try to convert it to a dataTable - * It is then possible to convert this datatable to any requested format (xml/etc) - * - * - If a bool is returned we convert to a string (true is displayed as 'true' false as 'false') - * - * - If an integer / float is returned, we simply return it - * - * @param mixed $value The initial returned value, before post process. If set to null, success response is returned. - * @param bool|string $apiModule The API module that was called - * @param bool|string $apiMethod The API method that was called - * @return mixed Usually a string, but can still be a PHP data structure if the format requested is 'original' - */ - public function getResponse($value = null, $apiModule = false, $apiMethod = false) - { + /** + * @param string $outputFormat + * @param array $request + */ + public function __construct($outputFormat, $request = array()) + { + $this->request = $request; + $this->outputFormat = $outputFormat; + } + + /** + * This method processes the data resulting from the API call. + * + * - If the data resulted from the API call is a Piwik_DataTable then + * - we apply the standard filters if the parameters have been found + * in the URL. For example to offset,limit the Table you can add the following parameters to any API + * call that returns a DataTable: filter_limit=10&filter_offset=20 + * - we apply the filters that have been previously queued on the DataTable + * @see Piwik_DataTable::queueFilter() + * - we apply the renderer that generate the DataTable in a given format (XML, PHP, HTML, JSON, etc.) + * the format can be changed using the 'format' parameter in the request. + * Example: format=xml + * + * - If there is nothing returned (void) we display a standard success message + * + * - If there is a PHP array returned, we try to convert it to a dataTable + * It is then possible to convert this datatable to any requested format (xml/etc) + * + * - If a bool is returned we convert to a string (true is displayed as 'true' false as 'false') + * + * - If an integer / float is returned, we simply return it + * + * @param mixed $value The initial returned value, before post process. If set to null, success response is returned. + * @param bool|string $apiModule The API module that was called + * @param bool|string $apiMethod The API method that was called + * @return mixed Usually a string, but can still be a PHP data structure if the format requested is 'original' + */ + public function getResponse($value = null, $apiModule = false, $apiMethod = false) + { $this->apiModule = $apiModule; $this->apiMethod = $apiMethod; - - // 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 - if($value instanceof Piwik_DataTable - || $value instanceof Piwik_DataTable_Array) - { - return $this->handleDataTable($value); - } - - // Case an array is returned from the API call, we convert it to the requested format - // - if calling from inside the application (format = original) - // => the data stays unchanged (ie. a standard php array or whatever data structure) - // - if any other format is requested, we have to convert this data structure (which we assume - // to be an array) to a DataTable in order to apply the requested DataTable_Renderer (for example XML) - if(is_array($value)) - { - return $this->handleArray($value); - } - - // original data structure requested, we return without process - if( $this->outputFormat == 'original' ) - { - return $value; - } - - if( is_object($value) - || is_resource($value)) - { - return $this->getResponseException(new Exception('The API cannot handle this data structure.')); - } - - // bool // integer // float // serialized object - return $this->handleScalar($value); - } - - /** - * Returns an error $message in the requested $format - * - * @param Exception $e - * @throws Exception - * @return string - */ - public function getResponseException(Exception $e) - { - $format = strtolower($this->outputFormat); - - if( $format == 'original' ) - { - throw $e; - } - - try { - $renderer = Piwik_DataTable_Renderer::factory($format); - } catch (Exception $exceptionRenderer) { - return "Error: " . $e->getMessage() . " and: " . $exceptionRenderer->getMessage(); - } - - $renderer->setException($e); - - if($format == 'php') - { - $renderer->setSerialize($this->caseRendererPHPSerialize()); - } - - return $renderer->renderException(); - } - - /** - * Returns true if the user requested to serialize the output data (&serialize=1 in the request) - * - * @param mixed $defaultSerializeValue Default value in case the user hasn't specified a value - * @return bool - */ - protected function caseRendererPHPSerialize($defaultSerializeValue = 1) - { - $serialize = Piwik_Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request); - if($serialize) - { - return true; - } - return false; - } - - /** - * Apply the specified renderer to the DataTable - * - * @param Piwik_DataTable|array $dataTable - * @return string - */ - protected function getRenderedDataTable($dataTable) - { - $format = strtolower($this->outputFormat); - - // if asked for original dataStructure - if($format == 'original') - { - // by default "original" data is not serialized - if($this->caseRendererPHPSerialize( $defaultSerialize = 0)) - { - $dataTable = serialize($dataTable); - } - return $dataTable; - } - - $method = Piwik_Common::getRequestVar('method', '', 'string', $this->request); - - $renderer = Piwik_DataTable_Renderer::factory($format); - $renderer->setTable($dataTable); - $renderer->setRenderSubTables(Piwik_Common::getRequestVar('expanded', false, 'int', $this->request)); - $renderer->setHideIdSubDatableFromResponse(Piwik_Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request)); - - if($format == 'php') - { - $renderer->setSerialize($this->caseRendererPHPSerialize()); - $renderer->setPrettyDisplay(Piwik_Common::getRequestVar('prettyDisplay', false, 'int', $this->request)); - } - else if($format == 'html') - { - $renderer->setTableId($this->request['method']); - } - else if($format == 'csv' || $format == 'tsv') - { - $renderer->setConvertToUnicode(Piwik_Common::getRequestVar('convertToUnicode', true, 'int', $this->request)); - } - - // prepare translation of column names - if ($format == 'html' || $format == 'csv' || $format == 'tsv' || $format = 'rss') - { - $renderer->setApiMethod($method); - $renderer->setIdSite(Piwik_Common::getRequestVar('idSite', false, 'int', $this->request)); - $renderer->setTranslateColumnNames(Piwik_Common::getRequestVar('translateColumnNames', false, 'int', $this->request)); - } - - return $renderer->render(); - } - - /** - * Returns a success $message in the requested $format - * - * @param string $message - * @return string - */ - protected function handleSuccess( $message = 'ok' ) - { - // return a success message only if no content has already been buffered, useful when APIs return raw text or html content to the browser - if(!ob_get_contents()) - { - switch($this->outputFormat) - { - case 'xml': - @header("Content-Type: text/xml;charset=utf-8"); - $return = - "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" . - "<result>\n". - "\t<success message=\"".$message."\" />\n". - "</result>"; - break; - case 'json': - @header( "Content-Type: application/json" ); - $return = '{"result":"success", "message":"'.$message.'"}'; - break; - case 'php': - $return = array('result' => 'success', 'message' => $message); - if($this->caseRendererPHPSerialize()) - { - $return = serialize($return); - } - break; - - case 'csv': - @header("Content-Type: application/vnd.ms-excel"); - @header("Content-Disposition: attachment; filename=piwik-report-export.csv"); - $return = "message\n".$message; - break; - - default: - $return = 'Success:'.$message; - break; - } - return $return; - } - } - - /** - * Converts the given scalar to an data table - * - * @param mixed $scalar - * @return string - */ - protected function handleScalar($scalar) - { - $dataTable = new Piwik_DataTable_Simple(); - $dataTable->addRowsFromArray( array($scalar) ); - return $this->getRenderedDataTable($dataTable); - } - - /** - * Handles the given data table - * - * @param Piwik_DataTable $datatable - * @return string - */ - protected function handleDataTable($datatable) - { - // if requested, flatten nested tables - if (Piwik_Common::getRequestVar('flat', '0', 'string', $this->request) == '1') - { - $flattener = new Piwik_API_DataTableManipulator_Flattener($this->apiModule, $this->apiMethod, $this->request); - if (Piwik_Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') - { - $flattener->includeAggregateRows(); - } - $datatable = $flattener->flatten($datatable); - } - - // if the flag disable_generic_filters is defined we skip the generic filters - if(0 == Piwik_Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) - { - $genericFilter = new Piwik_API_DataTableGenericFilter($this->request); - $genericFilter->filter($datatable); - } - - // we automatically safe decode all datatable labels (against xss) - $datatable->queueFilter('SafeDecodeLabel'); - - // if the flag disable_queued_filters is defined we skip the filters that were queued - if(Piwik_Common::getRequestVar('disable_queued_filters', 'false', 'string', $this->request) == 'false') - { - $datatable->applyQueuedFilters(); - } - - // use the ColumnDelete filter if hideColumns/showColumns is provided (must be done - // after queued filters are run so processed metrics can be removed, too) - $hideColumns = Piwik_Common::getRequestVar('hideColumns', '', 'string', $this->request); - $showColumns = Piwik_Common::getRequestVar('showColumns', '', 'string', $this->request); - if ($hideColumns !== '' || $showColumns !== '') - { - $datatable->filter('ColumnDelete', array($hideColumns, $showColumns)); - } - - // apply label filter: only return rows matching the label parameter (more than one if more than one label) - $label = $this->getLabelQueryParam(); - if (!empty($label)) - { - $label = Piwik_Common::unsanitizeInputValues($label); - $addEmptyRows = Piwik_Common::getRequestVar('labelFilterAddEmptyRows', 0, 'int', $this->request) == 1; - - $filter = new Piwik_API_DataTableManipulator_LabelFilter($this->apiModule, $this->apiMethod, $this->request); - $datatable = $filter->filter($label, $datatable, $addEmptyRows); - } - return $this->getRenderedDataTable($datatable); - } - - /** - * Converts the given simple array to a data table - * - * @param array $array - * @return string - */ - protected function handleArray($array) - { - if($this->outputFormat == 'original') - { - // we handle the serialization. Because some php array have a very special structure that - // couldn't be converted with the automatic DataTable->addRowsFromSimpleArray - // the user may want to request the original PHP data structure serialized by the API - // in case he has to setup serialize=1 in the URL - if($this->caseRendererPHPSerialize( $defaultSerialize = 0)) - { - return serialize($array); - } - return $array; - } - $multiDimensional = $this->handleMultiDimensionalArray($array); - if($multiDimensional !== false) - { - return $multiDimensional; - } - - return $this->getRenderedDataTable($array); - } - - /** - * Is this a multi dimensional array? - * Multi dim arrays are not supported by the Datatable renderer. - * We manually render these. - * - * array( - * array( - * 1, - * 2 => array( 1, - * 2 - * ) - * ), - * array( 2, - * 3 - * ) - * ); - * - * @param array $array - * @return string|bool false if it isn't a multidim array - */ - protected function handleMultiDimensionalArray($array) - { - $first = reset($array); - foreach($array as $first) - { - if(is_array($first)) - { - foreach($first as $key => $value) - { - // Yes, this is a multi dim array - if(is_array($value)) - { - switch($this->outputFormat) - { - case 'json': - @header( "Content-Type: application/json" ); - return self::convertMultiDimensionalArrayToJson($array); - break; - - case 'php': - if($this->caseRendererPHPSerialize( $defaultSerialize = 0)) - { - return serialize($array); - } - return $array; - - case 'xml': - @header("Content-Type: text/xml;charset=utf-8"); - return $this->getRenderedDataTable($array); - default: - break; - } - } - } - } - } - return false; - } - - /** - * Render a multidimensional array to Json - * Handle Piwik_DataTable|Piwik_DataTable_Array elements in the first dimension only, following case does not work: - * array( - * array( - * Piwik_DataTable, - * 2 => array( - * 1, - * 2 - * ), - * ), - * ); - * - * @param array $array can contain scalar, arrays, Piwik_DataTable and Piwik_DataTable_Array - * @return string - */ - public static function convertMultiDimensionalArrayToJson($array) - { - $isAssociative = Piwik::isAssociativeArray($array); - - if($isAssociative) - { - $json = "{"; - } - else - { - $json = "["; - } - - foreach ($array as $key=>$value) - { - if($isAssociative) - { - $json .= "\"".$key."\":"; - } - - switch(true) - { - // Case dimension is a PHP array - case (is_array($value)): - - $json .= Piwik_Common::json_encode($value); - break; - - // Case dimension is a Piwik_DataTable_Array or a Piwik_DataTable - case ($value instanceof Piwik_DataTable_Array || $value instanceof Piwik_DataTable): - - $XMLRenderer = new Piwik_DataTable_Renderer_Json(); - $XMLRenderer->setTable($value); - $renderedReport = $XMLRenderer->render(); - $json .= $renderedReport; - break; - - // Case scalar - default: - - $json .= Piwik_Common::json_encode($value); - break; - } - - $json .= ","; - } - - // Remove trailing "," - $json = substr ($json, 0, strlen($json) - 1); - - if($isAssociative) - { - $json .= "}"; - } - else - { - $json .= "]"; - } - return $json; - } - - /** - * Returns the value for the label query parameter which can be either a string - * (ie, label=...) or array (ie, label[]=...). - * - * @return array - */ - private function getLabelQueryParam() - { + + // 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 + if ($value instanceof Piwik_DataTable + || $value instanceof Piwik_DataTable_Array + ) { + return $this->handleDataTable($value); + } + + // Case an array is returned from the API call, we convert it to the requested format + // - if calling from inside the application (format = original) + // => the data stays unchanged (ie. a standard php array or whatever data structure) + // - if any other format is requested, we have to convert this data structure (which we assume + // to be an array) to a DataTable in order to apply the requested DataTable_Renderer (for example XML) + if (is_array($value)) { + return $this->handleArray($value); + } + + // original data structure requested, we return without process + if ($this->outputFormat == 'original') { + return $value; + } + + if (is_object($value) + || is_resource($value) + ) { + return $this->getResponseException(new Exception('The API cannot handle this data structure.')); + } + + // bool // integer // float // serialized object + return $this->handleScalar($value); + } + + /** + * Returns an error $message in the requested $format + * + * @param Exception $e + * @throws Exception + * @return string + */ + public function getResponseException(Exception $e) + { + $format = strtolower($this->outputFormat); + + if ($format == 'original') { + throw $e; + } + + try { + $renderer = Piwik_DataTable_Renderer::factory($format); + } catch (Exception $exceptionRenderer) { + return "Error: " . $e->getMessage() . " and: " . $exceptionRenderer->getMessage(); + } + + $renderer->setException($e); + + if ($format == 'php') { + $renderer->setSerialize($this->caseRendererPHPSerialize()); + } + + return $renderer->renderException(); + } + + /** + * Returns true if the user requested to serialize the output data (&serialize=1 in the request) + * + * @param mixed $defaultSerializeValue Default value in case the user hasn't specified a value + * @return bool + */ + protected function caseRendererPHPSerialize($defaultSerializeValue = 1) + { + $serialize = Piwik_Common::getRequestVar('serialize', $defaultSerializeValue, 'int', $this->request); + if ($serialize) { + return true; + } + return false; + } + + /** + * Apply the specified renderer to the DataTable + * + * @param Piwik_DataTable|array $dataTable + * @return string + */ + protected function getRenderedDataTable($dataTable) + { + $format = strtolower($this->outputFormat); + + // if asked for original dataStructure + if ($format == 'original') { + // by default "original" data is not serialized + if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) { + $dataTable = serialize($dataTable); + } + return $dataTable; + } + + $method = Piwik_Common::getRequestVar('method', '', 'string', $this->request); + + $renderer = Piwik_DataTable_Renderer::factory($format); + $renderer->setTable($dataTable); + $renderer->setRenderSubTables(Piwik_Common::getRequestVar('expanded', false, 'int', $this->request)); + $renderer->setHideIdSubDatableFromResponse(Piwik_Common::getRequestVar('hideIdSubDatable', false, 'int', $this->request)); + + if ($format == 'php') { + $renderer->setSerialize($this->caseRendererPHPSerialize()); + $renderer->setPrettyDisplay(Piwik_Common::getRequestVar('prettyDisplay', false, 'int', $this->request)); + } else if ($format == 'html') { + $renderer->setTableId($this->request['method']); + } else if ($format == 'csv' || $format == 'tsv') { + $renderer->setConvertToUnicode(Piwik_Common::getRequestVar('convertToUnicode', true, 'int', $this->request)); + } + + // prepare translation of column names + if ($format == 'html' || $format == 'csv' || $format == 'tsv' || $format = 'rss') { + $renderer->setApiMethod($method); + $renderer->setIdSite(Piwik_Common::getRequestVar('idSite', false, 'int', $this->request)); + $renderer->setTranslateColumnNames(Piwik_Common::getRequestVar('translateColumnNames', false, 'int', $this->request)); + } + + return $renderer->render(); + } + + /** + * Returns a success $message in the requested $format + * + * @param string $message + * @return string + */ + protected function handleSuccess($message = 'ok') + { + // return a success message only if no content has already been buffered, useful when APIs return raw text or html content to the browser + if (!ob_get_contents()) { + switch ($this->outputFormat) { + case 'xml': + @header("Content-Type: text/xml;charset=utf-8"); + $return = + "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" . + "<result>\n" . + "\t<success message=\"" . $message . "\" />\n" . + "</result>"; + break; + case 'json': + @header("Content-Type: application/json"); + $return = '{"result":"success", "message":"' . $message . '"}'; + break; + case 'php': + $return = array('result' => 'success', 'message' => $message); + if ($this->caseRendererPHPSerialize()) { + $return = serialize($return); + } + break; + + case 'csv': + @header("Content-Type: application/vnd.ms-excel"); + @header("Content-Disposition: attachment; filename=piwik-report-export.csv"); + $return = "message\n" . $message; + break; + + default: + $return = 'Success:' . $message; + break; + } + return $return; + } + } + + /** + * Converts the given scalar to an data table + * + * @param mixed $scalar + * @return string + */ + protected function handleScalar($scalar) + { + $dataTable = new Piwik_DataTable_Simple(); + $dataTable->addRowsFromArray(array($scalar)); + return $this->getRenderedDataTable($dataTable); + } + + /** + * Handles the given data table + * + * @param Piwik_DataTable $datatable + * @return string + */ + protected function handleDataTable($datatable) + { + // if requested, flatten nested tables + if (Piwik_Common::getRequestVar('flat', '0', 'string', $this->request) == '1') { + $flattener = new Piwik_API_DataTableManipulator_Flattener($this->apiModule, $this->apiMethod, $this->request); + if (Piwik_Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') { + $flattener->includeAggregateRows(); + } + $datatable = $flattener->flatten($datatable); + } + + // if the flag disable_generic_filters is defined we skip the generic filters + if (0 == Piwik_Common::getRequestVar('disable_generic_filters', '0', 'string', $this->request)) { + $genericFilter = new Piwik_API_DataTableGenericFilter($this->request); + $genericFilter->filter($datatable); + } + + // we automatically safe decode all datatable labels (against xss) + $datatable->queueFilter('SafeDecodeLabel'); + + // if the flag disable_queued_filters is defined we skip the filters that were queued + if (Piwik_Common::getRequestVar('disable_queued_filters', 'false', 'string', $this->request) == 'false') { + $datatable->applyQueuedFilters(); + } + + // use the ColumnDelete filter if hideColumns/showColumns is provided (must be done + // after queued filters are run so processed metrics can be removed, too) + $hideColumns = Piwik_Common::getRequestVar('hideColumns', '', 'string', $this->request); + $showColumns = Piwik_Common::getRequestVar('showColumns', '', 'string', $this->request); + if ($hideColumns !== '' || $showColumns !== '') { + $datatable->filter('ColumnDelete', array($hideColumns, $showColumns)); + } + + // apply label filter: only return rows matching the label parameter (more than one if more than one label) + $label = $this->getLabelQueryParam(); + if (!empty($label)) { + $label = Piwik_Common::unsanitizeInputValues($label); + $addEmptyRows = Piwik_Common::getRequestVar('labelFilterAddEmptyRows', 0, 'int', $this->request) == 1; + + $filter = new Piwik_API_DataTableManipulator_LabelFilter($this->apiModule, $this->apiMethod, $this->request); + $datatable = $filter->filter($label, $datatable, $addEmptyRows); + } + return $this->getRenderedDataTable($datatable); + } + + /** + * Converts the given simple array to a data table + * + * @param array $array + * @return string + */ + protected function handleArray($array) + { + if ($this->outputFormat == 'original') { + // we handle the serialization. Because some php array have a very special structure that + // couldn't be converted with the automatic DataTable->addRowsFromSimpleArray + // the user may want to request the original PHP data structure serialized by the API + // in case he has to setup serialize=1 in the URL + if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) { + return serialize($array); + } + return $array; + } + $multiDimensional = $this->handleMultiDimensionalArray($array); + if ($multiDimensional !== false) { + return $multiDimensional; + } + + return $this->getRenderedDataTable($array); + } + + /** + * Is this a multi dimensional array? + * Multi dim arrays are not supported by the Datatable renderer. + * We manually render these. + * + * array( + * array( + * 1, + * 2 => array( 1, + * 2 + * ) + * ), + * array( 2, + * 3 + * ) + * ); + * + * @param array $array + * @return string|bool false if it isn't a multidim array + */ + protected function handleMultiDimensionalArray($array) + { + $first = reset($array); + foreach ($array as $first) { + if (is_array($first)) { + foreach ($first as $key => $value) { + // Yes, this is a multi dim array + if (is_array($value)) { + switch ($this->outputFormat) { + case 'json': + @header("Content-Type: application/json"); + return self::convertMultiDimensionalArrayToJson($array); + break; + + case 'php': + if ($this->caseRendererPHPSerialize($defaultSerialize = 0)) { + return serialize($array); + } + return $array; + + case 'xml': + @header("Content-Type: text/xml;charset=utf-8"); + return $this->getRenderedDataTable($array); + default: + break; + } + } + } + } + } + return false; + } + + /** + * Render a multidimensional array to Json + * Handle Piwik_DataTable|Piwik_DataTable_Array elements in the first dimension only, following case does not work: + * array( + * array( + * Piwik_DataTable, + * 2 => array( + * 1, + * 2 + * ), + * ), + * ); + * + * @param array $array can contain scalar, arrays, Piwik_DataTable and Piwik_DataTable_Array + * @return string + */ + public static function convertMultiDimensionalArrayToJson($array) + { + $isAssociative = Piwik::isAssociativeArray($array); + + if ($isAssociative) { + $json = "{"; + } else { + $json = "["; + } + + foreach ($array as $key => $value) { + if ($isAssociative) { + $json .= "\"" . $key . "\":"; + } + + switch (true) { + // Case dimension is a PHP array + case (is_array($value)): + + $json .= Piwik_Common::json_encode($value); + break; + + // Case dimension is a Piwik_DataTable_Array or a Piwik_DataTable + case ($value instanceof Piwik_DataTable_Array || $value instanceof Piwik_DataTable): + + $XMLRenderer = new Piwik_DataTable_Renderer_Json(); + $XMLRenderer->setTable($value); + $renderedReport = $XMLRenderer->render(); + $json .= $renderedReport; + break; + + // Case scalar + default: + + $json .= Piwik_Common::json_encode($value); + break; + } + + $json .= ","; + } + + // Remove trailing "," + $json = substr($json, 0, strlen($json) - 1); + + if ($isAssociative) { + $json .= "}"; + } else { + $json .= "]"; + } + return $json; + } + + /** + * Returns the value for the label query parameter which can be either a string + * (ie, label=...) or array (ie, label[]=...). + * + * @return array + */ + private function getLabelQueryParam() + { $label = Piwik_Common::getRequestVar('label', array(), 'array', $this->request); - if (empty($label)) - { - $label = Piwik_Common::getRequestVar('label', '', 'string', $this->request); - if (!empty($label)) - { - $label = array($label); - } + if (empty($label)) { + $label = Piwik_Common::getRequestVar('label', '', 'string', $this->request); + if (!empty($label)) { + $label = array($label); + } } return $label; - } + } } diff --git a/core/Access.php b/core/Access.php index ed7b2e386e..0e3bc42054 100644 --- a/core/Access.php +++ b/core/Access.php @@ -11,9 +11,9 @@ /** * Class to handle User Access: - * - loads user access from the Piwik_Auth_Result object + * - loads user access from the Piwik_Auth_Result object * - provides easy to use API to check the permissions for the current (check* methods) - * + * * In Piwik there are mainly 4 access levels * - no access * - VIEW access @@ -24,389 +24,368 @@ * A given user has a given access level for a given website. * For example: * User Noemie has - * - VIEW access on the website 1, + * - VIEW access on the website 1, * - ADMIN on the website 2 and 4, and * - NO access on the website 3 and 5 * - * There is only one Super User. He has ADMIN access to all the websites + * There is only one Super User. He has ADMIN access to all the websites * and he only can change the main configuration settings. * * @package Piwik * @subpackage Piwik_Access */ class Piwik_Access -{ - /** - * Array of idsites available to the current user, indexed by permission level - * @see getSitesIdWith*() - * - * @var array - */ - protected $idsitesByAccess = null; - - /** - * Login of the current user - * - * @var string - */ - protected $login = null; - - /** - * token_auth of the current user - * - * @var string - */ - protected $token_auth = null; - - /** - * Defines if the current user is the super user - * @see isSuperUser() - * - * @var bool - */ - protected $isSuperUser = false; - - /** - * List of available permissions in Piwik - * - * @var array - */ - static private $availableAccess = array('noaccess', 'view', 'admin', 'superuser'); - - /** - * Authentification object (see Piwik_Auth) - * - * @var Piwik_Auth - */ - private $auth = null; - - /** - * Returns the list of the existing Access level. - * Useful when a given API method requests a given acccess Level. - * We first check that the required access level exists. - * - * @return array - */ - static public function getListAccess() - { - return self::$availableAccess; - } - - /** - * Constructor - */ - function __construct() - { - $this->idsitesByAccess = array( - 'view' => array(), - 'admin' => array(), - 'superuser' => array() - ); - } - - /** - * Loads the access levels for the current user. - * - * Calls the authentication method to try to log the user in the system. - * If the user credentials are not correct we don't load anything. - * If the login/password is correct the user is either the SuperUser or a normal user. - * We load the access levels for this user for all the websites. - * - * @param null|Piwik_Auth $auth Auth adapter - * @return bool true on success, false if reloading access failed (when auth object wasn't specified and user is not enforced to be Super User) - */ - public function reloadAccess(Piwik_Auth $auth = null) - { - if(!is_null($auth)) - { - $this->auth = $auth; - } - - // if the Piwik_Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail - if(is_null($this->auth)) - { - if($this->isSuperUser()) - { - return $this->reloadAccessSuperUser(); - } - return false; - } - - // access = array ( idsite => accessIdSite, idsite2 => accessIdSite2) - $result = $this->auth->authenticate(); - - if(!$result->isValid()) - { - return false; - } - $this->login = $result->getIdentity(); - $this->token_auth = $result->getTokenAuth(); - - // case the superUser is logged in - if($result->getCode() == Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE) - { - return $this->reloadAccessSuperUser(); - } - // in case multiple calls to API using different tokens, we ensure we reset it as not SU - $this->setSuperUser(false); - - // 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 = self::getRawSitesWithSomeViewAccess($this->login); - foreach($accessRaw as $access) - { - $this->idsitesByAccess[$access['access']][] = $access['idsite']; - } - return true; - } - - static public function getRawSitesWithSomeViewAccess($login) - { - return Piwik_FetchAll(self::getSqlAccessSite("access, t2.idsite"), $login); - } - - /** - * Returns the SQL query joining sites and access table for a given login - * - * @param string $select Columns or expression to SELECT FROM table, 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 - */ - protected function reloadAccessSuperUser() - { - $this->isSuperUser = true; - $this->idsitesByAccess['superuser'] = Piwik_SitesManager_API::getInstance()->getAllSitesId(); - $this->login = Piwik_Config::getInstance()->superuser['login']; - return true; - } - - /** - * We bypass the normal auth method and give the current user Super User rights. - * This should be very carefully used. - * - * @param bool $bool - */ - public function setSuperUser($bool = true) - { - if($bool) - { - $this->reloadAccessSuperUser(); - } - else - { - $this->isSuperUser = false; - $this->idsitesByAccess['superuser'] = array(); - } - } - - /** - * Returns true if the current user is logged in as the super user - * - * @return bool - */ - public function isSuperUser() - { - return $this->isSuperUser; - } - - /** - * Returns the current user login - * - * @return string|null - */ - public function getLogin() - { - return $this->login; - } - - /** - * Returns the token_auth used to authenticate this user in the API - * - * @return string|null - */ - public function getTokenAuth() - { - return $this->token_auth; - } - - /** - * Returns an array of ID sites for which the user has at least a VIEW access. - * Which means VIEW or ADMIN or SUPERUSER. - * - * @return array Example if the user is ADMIN for 4 - * and has VIEW access for 1 and 7, it returns array(1, 4, 7); - */ - public function getSitesIdWithAtLeastViewAccess() - { - return array_unique(array_merge( - $this->idsitesByAccess['view'], - $this->idsitesByAccess['admin'], - $this->idsitesByAccess['superuser']) - ); - } - - /** - * Returns an array of ID sites for which the user has an ADMIN access. - * - * @return array Example if the user is ADMIN for 4 and 8 - * and has VIEW access for 1 and 7, it returns array(4, 8); - */ - public function getSitesIdWithAdminAccess() - { - return array_unique(array_merge( - $this->idsitesByAccess['admin'], - $this->idsitesByAccess['superuser']) - ); - } - - - /** - * Returns an array of ID sites for which the user has a VIEW access only. - * - * @return array Example if the user is ADMIN for 4 - * and has VIEW access for 1 and 7, it returns array(1, 7); - * @see getSitesIdWithAtLeastViewAccess() - */ - public function getSitesIdWithViewAccess() - { - return $this->idsitesByAccess['view']; - } - - /** - * Throws an exception if the user is not the SuperUser - * - * @throws Piwik_Access_NoAccessException - */ - public function checkUserIsSuperUser() - { - if(!$this->isSuperUser()) - { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilege', array("'superuser'"))); - } - } - - /** - * If the user doesn't have an ADMIN access for at least one website, throws an exception - * - * @throws Piwik_Access_NoAccessException - */ - public function checkUserHasSomeAdminAccess() - { - if($this->isSuperUser()) - { - return; - } - $idSitesAccessible = $this->getSitesIdWithAdminAccess(); - if(count($idSitesAccessible) == 0) - { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin'))); - } - } - - /** - * If the user doesn't have any view permission, throw exception - * - * @throws Piwik_Access_NoAccessException - */ - public function checkUserHasSomeViewAccess() - { - if($this->isSuperUser()) - { - return; - } - $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); - if(count($idSitesAccessible) == 0) - { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAtLeastOneWebsite', array('view'))); - } - } - - /** - * This method checks that the user has ADMIN access for the given list of websites. - * If the user doesn't have ADMIN access for at least one website of the list, we throw an exception. - * - * @param int|array $idSites List of ID sites to check - * @throws Piwik_Access_NoAccessException If for any of the websites the user doesn't have an ADMIN access - */ - public function checkUserHasAdminAccess( $idSites ) - { - if($this->isSuperUser()) - { - return; - } - $idSites = $this->getIdSites($idSites); - $idSitesAccessible = $this->getSitesIdWithAdminAccess(); - foreach($idSites as $idsite) - { - if(!in_array($idsite, $idSitesAccessible)) - { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'admin'", $idsite))); - } - } - } - - /** - * This method checks that the user has VIEW or ADMIN access for the given list of websites. - * If the user doesn't have VIEW or ADMIN access for at least one website of the list, we throw an exception. - * - * @param int|array|string $idSites List of ID sites to check (integer, array of integers, string comma separated list of integers) - * @throws Piwik_Access_NoAccessException If for any of the websites the user doesn't have an VIEW or ADMIN access - */ - public function checkUserHasViewAccess( $idSites ) - { - if($this->isSuperUser()) - { - return; - } - $idSites = $this->getIdSites($idSites); - $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); - foreach($idSites as $idsite) - { - if(!in_array($idsite, $idSitesAccessible)) - { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idsite))); - } - } - } - - /** - * @param int|array|string $idSites - * @return array - * @throws Piwik_Access_NoAccessException - */ - protected function getIdSites($idSites) - { - if($idSites === 'all') - { - $idSites = $this->getSitesIdWithAtLeastViewAccess(); - } - - $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); - if(empty($idSites)) - { - throw new Piwik_Access_NoAccessException("The parameter 'idSite=' is missing from the request."); - } - return $idSites; - } +{ + /** + * Array of idsites available to the current user, indexed by permission level + * @see getSitesIdWith*() + * + * @var array + */ + protected $idsitesByAccess = null; + + /** + * Login of the current user + * + * @var string + */ + protected $login = null; + + /** + * token_auth of the current user + * + * @var string + */ + protected $token_auth = null; + + /** + * Defines if the current user is the super user + * @see isSuperUser() + * + * @var bool + */ + protected $isSuperUser = false; + + /** + * List of available permissions in Piwik + * + * @var array + */ + static private $availableAccess = array('noaccess', 'view', 'admin', 'superuser'); + + /** + * Authentification object (see Piwik_Auth) + * + * @var Piwik_Auth + */ + private $auth = null; + + /** + * Returns the list of the existing Access level. + * Useful when a given API method requests a given acccess Level. + * We first check that the required access level exists. + * + * @return array + */ + static public function getListAccess() + { + return self::$availableAccess; + } + + /** + * Constructor + */ + function __construct() + { + $this->idsitesByAccess = array( + 'view' => array(), + 'admin' => array(), + 'superuser' => array() + ); + } + + /** + * Loads the access levels for the current user. + * + * Calls the authentication method to try to log the user in the system. + * If the user credentials are not correct we don't load anything. + * If the login/password is correct the user is either the SuperUser or a normal user. + * We load the access levels for this user for all the websites. + * + * @param null|Piwik_Auth $auth Auth adapter + * @return bool true on success, false if reloading access failed (when auth object wasn't specified and user is not enforced to be Super User) + */ + public function reloadAccess(Piwik_Auth $auth = null) + { + if (!is_null($auth)) { + $this->auth = $auth; + } + + // if the Piwik_Auth wasn't set, we may be in the special case of setSuperUser(), otherwise we fail + if (is_null($this->auth)) { + if ($this->isSuperUser()) { + return $this->reloadAccessSuperUser(); + } + return false; + } + + // access = array ( idsite => accessIdSite, idsite2 => accessIdSite2) + $result = $this->auth->authenticate(); + + if (!$result->isValid()) { + return false; + } + $this->login = $result->getIdentity(); + $this->token_auth = $result->getTokenAuth(); + + // case the superUser is logged in + if ($result->getCode() == Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE) { + return $this->reloadAccessSuperUser(); + } + // in case multiple calls to API using different tokens, we ensure we reset it as not SU + $this->setSuperUser(false); + + // 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 = self::getRawSitesWithSomeViewAccess($this->login); + foreach ($accessRaw as $access) { + $this->idsitesByAccess[$access['access']][] = $access['idsite']; + } + return true; + } + + static public function getRawSitesWithSomeViewAccess($login) + { + return Piwik_FetchAll(self::getSqlAccessSite("access, t2.idsite"), $login); + } + + /** + * Returns the SQL query joining sites and access table for a given login + * + * @param string $select Columns or expression to SELECT FROM table, 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 + */ + protected function reloadAccessSuperUser() + { + $this->isSuperUser = true; + $this->idsitesByAccess['superuser'] = Piwik_SitesManager_API::getInstance()->getAllSitesId(); + $this->login = Piwik_Config::getInstance()->superuser['login']; + return true; + } + + /** + * We bypass the normal auth method and give the current user Super User rights. + * This should be very carefully used. + * + * @param bool $bool + */ + public function setSuperUser($bool = true) + { + if ($bool) { + $this->reloadAccessSuperUser(); + } else { + $this->isSuperUser = false; + $this->idsitesByAccess['superuser'] = array(); + } + } + + /** + * Returns true if the current user is logged in as the super user + * + * @return bool + */ + public function isSuperUser() + { + return $this->isSuperUser; + } + + /** + * Returns the current user login + * + * @return string|null + */ + public function getLogin() + { + return $this->login; + } + + /** + * Returns the token_auth used to authenticate this user in the API + * + * @return string|null + */ + public function getTokenAuth() + { + return $this->token_auth; + } + + /** + * Returns an array of ID sites for which the user has at least a VIEW access. + * Which means VIEW or ADMIN or SUPERUSER. + * + * @return array Example if the user is ADMIN for 4 + * and has VIEW access for 1 and 7, it returns array(1, 4, 7); + */ + public function getSitesIdWithAtLeastViewAccess() + { + return array_unique(array_merge( + $this->idsitesByAccess['view'], + $this->idsitesByAccess['admin'], + $this->idsitesByAccess['superuser']) + ); + } + + /** + * Returns an array of ID sites for which the user has an ADMIN access. + * + * @return array Example if the user is ADMIN for 4 and 8 + * and has VIEW access for 1 and 7, it returns array(4, 8); + */ + public function getSitesIdWithAdminAccess() + { + return array_unique(array_merge( + $this->idsitesByAccess['admin'], + $this->idsitesByAccess['superuser']) + ); + } + + + /** + * Returns an array of ID sites for which the user has a VIEW access only. + * + * @return array Example if the user is ADMIN for 4 + * and has VIEW access for 1 and 7, it returns array(1, 7); + * @see getSitesIdWithAtLeastViewAccess() + */ + public function getSitesIdWithViewAccess() + { + return $this->idsitesByAccess['view']; + } + + /** + * Throws an exception if the user is not the SuperUser + * + * @throws Piwik_Access_NoAccessException + */ + public function checkUserIsSuperUser() + { + if (!$this->isSuperUser()) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilege', array("'superuser'"))); + } + } + + /** + * If the user doesn't have an ADMIN access for at least one website, throws an exception + * + * @throws Piwik_Access_NoAccessException + */ + public function checkUserHasSomeAdminAccess() + { + if ($this->isSuperUser()) { + return; + } + $idSitesAccessible = $this->getSitesIdWithAdminAccess(); + if (count($idSitesAccessible) == 0) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAtLeastOneWebsite', array('admin'))); + } + } + + /** + * If the user doesn't have any view permission, throw exception + * + * @throws Piwik_Access_NoAccessException + */ + public function checkUserHasSomeViewAccess() + { + if ($this->isSuperUser()) { + return; + } + $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); + if (count($idSitesAccessible) == 0) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAtLeastOneWebsite', array('view'))); + } + } + + /** + * This method checks that the user has ADMIN access for the given list of websites. + * If the user doesn't have ADMIN access for at least one website of the list, we throw an exception. + * + * @param int|array $idSites List of ID sites to check + * @throws Piwik_Access_NoAccessException If for any of the websites the user doesn't have an ADMIN access + */ + public function checkUserHasAdminAccess($idSites) + { + if ($this->isSuperUser()) { + return; + } + $idSites = $this->getIdSites($idSites); + $idSitesAccessible = $this->getSitesIdWithAdminAccess(); + foreach ($idSites as $idsite) { + if (!in_array($idsite, $idSitesAccessible)) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'admin'", $idsite))); + } + } + } + + /** + * This method checks that the user has VIEW or ADMIN access for the given list of websites. + * If the user doesn't have VIEW or ADMIN access for at least one website of the list, we throw an exception. + * + * @param int|array|string $idSites List of ID sites to check (integer, array of integers, string comma separated list of integers) + * @throws Piwik_Access_NoAccessException If for any of the websites the user doesn't have an VIEW or ADMIN access + */ + public function checkUserHasViewAccess($idSites) + { + if ($this->isSuperUser()) { + return; + } + $idSites = $this->getIdSites($idSites); + $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); + foreach ($idSites as $idsite) { + if (!in_array($idsite, $idSitesAccessible)) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idsite))); + } + } + } + + /** + * @param int|array|string $idSites + * @return array + * @throws Piwik_Access_NoAccessException + */ + protected function getIdSites($idSites) + { + if ($idSites === 'all') { + $idSites = $this->getSitesIdWithAtLeastViewAccess(); + } + + $idSites = Piwik_Site::getIdSitesFromIdSitesString($idSites); + if (empty($idSites)) { + throw new Piwik_Access_NoAccessException("The parameter 'idSite=' is missing from the request."); + } + return $idSites; + } } /** * Exception thrown when a user doesn't have sufficient access. - * + * * @package Piwik * @subpackage Piwik_Access */ class Piwik_Access_NoAccessException extends Exception -{} +{ +} diff --git a/core/Archive.php b/core/Archive.php index 1a18efd53a..530694f7a6 100644 --- a/core/Archive.php +++ b/core/Archive.php @@ -1,478 +1,456 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * The archive object is used to query specific data for a day or a period of statistics for a given website. - * + * * Example: * <pre> - * $archive = Piwik_Archive::build($idSite = 1, $period = 'week', '2008-03-08' ); - * $dataTable = $archive->getDataTable('Provider_hostnameExt'); - * $dataTable->queueFilter('ReplaceColumnNames'); - * return $dataTable; + * $archive = Piwik_Archive::build($idSite = 1, $period = 'week', '2008-03-08' ); + * $dataTable = $archive->getDataTable('Provider_hostnameExt'); + * $dataTable->queueFilter('ReplaceColumnNames'); + * return $dataTable; * </pre> - * + * * Example bis: * <pre> - * $archive = Piwik_Archive::build($idSite = 3, $period = 'day', $date = 'today' ); - * $nbVisits = $archive->getNumeric('nb_visits'); - * return $nbVisits; + * $archive = Piwik_Archive::build($idSite = 3, $period = 'day', $date = 'today' ); + * $nbVisits = $archive->getNumeric('nb_visits'); + * return $nbVisits; * </pre> - * + * * If the requested statistics are not yet processed, Archive uses ArchiveProcessing to archive the statistics. - * + * * @package Piwik * @subpackage Piwik_Archive */ abstract class Piwik_Archive { - /** - * When saving DataTables in the DB, we sometimes replace the columns name by these IDs so we save up lots of bytes - * Eg. INDEX_NB_UNIQ_VISITORS is an integer: 4 bytes, but 'nb_uniq_visitors' is 16 bytes at least - * (in php it's actually even much more) - * - */ - const INDEX_NB_UNIQ_VISITORS = 1; - const INDEX_NB_VISITS = 2; - const INDEX_NB_ACTIONS = 3; - const INDEX_MAX_ACTIONS = 4; - const INDEX_SUM_VISIT_LENGTH = 5; - const INDEX_BOUNCE_COUNT = 6; - const INDEX_NB_VISITS_CONVERTED = 7; - const INDEX_NB_CONVERSIONS = 8; - const INDEX_REVENUE = 9; - const INDEX_GOALS = 10; - const INDEX_SUM_DAILY_NB_UNIQ_VISITORS = 11; - - // Specific to the Actions reports - const INDEX_PAGE_NB_HITS = 12; - const INDEX_PAGE_SUM_TIME_SPENT = 13; - - const INDEX_PAGE_EXIT_NB_UNIQ_VISITORS = 14; - const INDEX_PAGE_EXIT_NB_VISITS = 15; - const INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS = 16; - - const INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS = 17; - const INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS = 18; - const INDEX_PAGE_ENTRY_NB_VISITS = 19; - const INDEX_PAGE_ENTRY_NB_ACTIONS = 20; - const INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH = 21; - const INDEX_PAGE_ENTRY_BOUNCE_COUNT = 22; - - // Ecommerce Items reports - const INDEX_ECOMMERCE_ITEM_REVENUE = 23; - const INDEX_ECOMMERCE_ITEM_QUANTITY = 24; - const INDEX_ECOMMERCE_ITEM_PRICE = 25; - const INDEX_ECOMMERCE_ORDERS = 26; - const INDEX_ECOMMERCE_ITEM_PRICE_VIEWED = 27; - - // Site Search - const INDEX_SITE_SEARCH_HAS_NO_RESULT = 28; - const INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS = 29; - - // Performance Analytics - const INDEX_PAGE_SUM_TIME_GENERATION = 30; - const INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION = 31; - - // Goal reports - const INDEX_GOAL_NB_CONVERSIONS = 1; - const INDEX_GOAL_REVENUE = 2; - const INDEX_GOAL_NB_VISITS_CONVERTED = 3; - - const INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL = 4; - const INDEX_GOAL_ECOMMERCE_REVENUE_TAX = 5; - const INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING = 6; - const INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT = 7; - const INDEX_GOAL_ECOMMERCE_ITEMS = 8; - - public static $mappingFromIdToName = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 'nb_uniq_visitors', - Piwik_Archive::INDEX_NB_VISITS => 'nb_visits', - Piwik_Archive::INDEX_NB_ACTIONS => 'nb_actions', - Piwik_Archive::INDEX_MAX_ACTIONS => 'max_actions', - Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 'sum_visit_length', - Piwik_Archive::INDEX_BOUNCE_COUNT => 'bounce_count', - Piwik_Archive::INDEX_NB_VISITS_CONVERTED => 'nb_visits_converted', - Piwik_Archive::INDEX_NB_CONVERSIONS => 'nb_conversions', - Piwik_Archive::INDEX_REVENUE => 'revenue', - Piwik_Archive::INDEX_GOALS => 'goals', - Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_nb_uniq_visitors', - - // Actions metrics - Piwik_Archive::INDEX_PAGE_NB_HITS => 'nb_hits', - Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT => 'sum_time_spent', - Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION => 'sum_time_generation', - Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION => 'nb_hits_with_time_generation', - - Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => 'exit_nb_uniq_visitors', - Piwik_Archive::INDEX_PAGE_EXIT_NB_VISITS => 'exit_nb_visits', - Piwik_Archive::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_exit_nb_uniq_visitors', - - Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => 'entry_nb_uniq_visitors', - Piwik_Archive::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_entry_nb_uniq_visitors', - Piwik_Archive::INDEX_PAGE_ENTRY_NB_VISITS => 'entry_nb_visits', - Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS => 'entry_nb_actions', - Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH => 'entry_sum_visit_length', - Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT => 'entry_bounce_count', - Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS => 'nb_hits_following_search', - - // Items reports metrics - Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE => 'revenue', - Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY => 'quantity', - Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE => 'price', - Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED => 'price_viewed', - Piwik_Archive::INDEX_ECOMMERCE_ORDERS => 'orders', - ); - - public static $mappingFromIdToNameGoal = array( - Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 'nb_conversions', - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 'nb_visits_converted', - Piwik_Archive::INDEX_GOAL_REVENUE => 'revenue', - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 'revenue_subtotal', - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 'revenue_tax', - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 'revenue_shipping', - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 'revenue_discount', - Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 'items', - ); - - /** - * string indexed column name => Integer indexed column name + /** + * When saving DataTables in the DB, we sometimes replace the columns name by these IDs so we save up lots of bytes + * Eg. INDEX_NB_UNIQ_VISITORS is an integer: 4 bytes, but 'nb_uniq_visitors' is 16 bytes at least + * (in php it's actually even much more) + * + */ + const INDEX_NB_UNIQ_VISITORS = 1; + const INDEX_NB_VISITS = 2; + const INDEX_NB_ACTIONS = 3; + const INDEX_MAX_ACTIONS = 4; + const INDEX_SUM_VISIT_LENGTH = 5; + const INDEX_BOUNCE_COUNT = 6; + const INDEX_NB_VISITS_CONVERTED = 7; + const INDEX_NB_CONVERSIONS = 8; + const INDEX_REVENUE = 9; + const INDEX_GOALS = 10; + const INDEX_SUM_DAILY_NB_UNIQ_VISITORS = 11; + + // Specific to the Actions reports + const INDEX_PAGE_NB_HITS = 12; + const INDEX_PAGE_SUM_TIME_SPENT = 13; + + const INDEX_PAGE_EXIT_NB_UNIQ_VISITORS = 14; + const INDEX_PAGE_EXIT_NB_VISITS = 15; + const INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS = 16; + + const INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS = 17; + const INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS = 18; + const INDEX_PAGE_ENTRY_NB_VISITS = 19; + const INDEX_PAGE_ENTRY_NB_ACTIONS = 20; + const INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH = 21; + const INDEX_PAGE_ENTRY_BOUNCE_COUNT = 22; + + // Ecommerce Items reports + const INDEX_ECOMMERCE_ITEM_REVENUE = 23; + const INDEX_ECOMMERCE_ITEM_QUANTITY = 24; + const INDEX_ECOMMERCE_ITEM_PRICE = 25; + const INDEX_ECOMMERCE_ORDERS = 26; + const INDEX_ECOMMERCE_ITEM_PRICE_VIEWED = 27; + + // Site Search + const INDEX_SITE_SEARCH_HAS_NO_RESULT = 28; + const INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS = 29; + + // Performance Analytics + const INDEX_PAGE_SUM_TIME_GENERATION = 30; + const INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION = 31; + + // Goal reports + const INDEX_GOAL_NB_CONVERSIONS = 1; + const INDEX_GOAL_REVENUE = 2; + const INDEX_GOAL_NB_VISITS_CONVERTED = 3; + + const INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL = 4; + const INDEX_GOAL_ECOMMERCE_REVENUE_TAX = 5; + const INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING = 6; + const INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT = 7; + const INDEX_GOAL_ECOMMERCE_ITEMS = 8; + + public static $mappingFromIdToName = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 'nb_uniq_visitors', + Piwik_Archive::INDEX_NB_VISITS => 'nb_visits', + Piwik_Archive::INDEX_NB_ACTIONS => 'nb_actions', + Piwik_Archive::INDEX_MAX_ACTIONS => 'max_actions', + Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 'sum_visit_length', + Piwik_Archive::INDEX_BOUNCE_COUNT => 'bounce_count', + Piwik_Archive::INDEX_NB_VISITS_CONVERTED => 'nb_visits_converted', + Piwik_Archive::INDEX_NB_CONVERSIONS => 'nb_conversions', + Piwik_Archive::INDEX_REVENUE => 'revenue', + Piwik_Archive::INDEX_GOALS => 'goals', + Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_nb_uniq_visitors', + + // Actions metrics + Piwik_Archive::INDEX_PAGE_NB_HITS => 'nb_hits', + Piwik_Archive::INDEX_PAGE_SUM_TIME_SPENT => 'sum_time_spent', + Piwik_Archive::INDEX_PAGE_SUM_TIME_GENERATION => 'sum_time_generation', + Piwik_Archive::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION => 'nb_hits_with_time_generation', + + Piwik_Archive::INDEX_PAGE_EXIT_NB_UNIQ_VISITORS => 'exit_nb_uniq_visitors', + Piwik_Archive::INDEX_PAGE_EXIT_NB_VISITS => 'exit_nb_visits', + Piwik_Archive::INDEX_PAGE_EXIT_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_exit_nb_uniq_visitors', + + Piwik_Archive::INDEX_PAGE_ENTRY_NB_UNIQ_VISITORS => 'entry_nb_uniq_visitors', + Piwik_Archive::INDEX_PAGE_ENTRY_SUM_DAILY_NB_UNIQ_VISITORS => 'sum_daily_entry_nb_uniq_visitors', + Piwik_Archive::INDEX_PAGE_ENTRY_NB_VISITS => 'entry_nb_visits', + Piwik_Archive::INDEX_PAGE_ENTRY_NB_ACTIONS => 'entry_nb_actions', + Piwik_Archive::INDEX_PAGE_ENTRY_SUM_VISIT_LENGTH => 'entry_sum_visit_length', + Piwik_Archive::INDEX_PAGE_ENTRY_BOUNCE_COUNT => 'entry_bounce_count', + Piwik_Archive::INDEX_PAGE_IS_FOLLOWING_SITE_SEARCH_NB_HITS => 'nb_hits_following_search', + + // Items reports metrics + Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE => 'revenue', + Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY => 'quantity', + Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE => 'price', + Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED => 'price_viewed', + Piwik_Archive::INDEX_ECOMMERCE_ORDERS => 'orders', + ); + + public static $mappingFromIdToNameGoal = array( + Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 'nb_conversions', + Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 'nb_visits_converted', + Piwik_Archive::INDEX_GOAL_REVENUE => 'revenue', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 'revenue_subtotal', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 'revenue_tax', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 'revenue_shipping', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 'revenue_discount', + Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 'items', + ); + + /** + * string indexed column name => Integer indexed column name * @var array - */ - public static $mappingFromNameToId = array( - 'nb_uniq_visitors' => Piwik_Archive::INDEX_NB_UNIQ_VISITORS, - 'nb_visits' => Piwik_Archive::INDEX_NB_VISITS, - 'nb_actions' => Piwik_Archive::INDEX_NB_ACTIONS, - 'max_actions' => Piwik_Archive::INDEX_MAX_ACTIONS, - 'sum_visit_length' => Piwik_Archive::INDEX_SUM_VISIT_LENGTH, - 'bounce_count' => Piwik_Archive::INDEX_BOUNCE_COUNT, - 'nb_visits_converted' => Piwik_Archive::INDEX_NB_VISITS_CONVERTED, - 'nb_conversions' => Piwik_Archive::INDEX_NB_CONVERSIONS, - 'revenue' => Piwik_Archive::INDEX_REVENUE, - 'goals' => Piwik_Archive::INDEX_GOALS, - 'sum_daily_nb_uniq_visitors' => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, - ); - - /** - * Metrics calculated and archived by the Actions plugin. - * - * @var array - */ - public static $actionsMetrics = array( - 'nb_pageviews', - 'nb_uniq_pageviews', - 'nb_downloads', - 'nb_uniq_downloads', - 'nb_outlinks', - 'nb_uniq_outlinks', - 'nb_searches', - 'nb_keywords', - 'nb_hits', - 'nb_hits_following_search', - ); - - const LABEL_ECOMMERCE_CART = 'ecommerceAbandonedCart'; - const LABEL_ECOMMERCE_ORDER = 'ecommerceOrder'; - - /** - * Website Piwik_Site - * - * @var Piwik_Site - */ - protected $site = null; - - /** - * Segment applied to the visits set - * @var Piwik_Segment - */ - protected $segment = false; - - /** - * Builds an Archive object or returns the same archive if previously built. - * - * @param int|string $idSite integer, or comma separated list of integer - * @param string $period 'week' 'day' etc. - * @param Piwik_Date|string $strDate 'YYYY-MM-DD' or magic keywords 'today' @see Piwik_Date::factory() - * @param bool|string $segment Segment definition - defaults to false for Backward Compatibility - * @param bool|string $_restrictSitesToLogin Used only when running as a scheduled task - * @return Piwik_Archive - */ - static public function build($idSite, $period, $strDate, $segment = false, $_restrictSitesToLogin = false ) - { - if($idSite === 'all') - { - $sites = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin); - } - else - { - $sites = Piwik_Site::getIdSitesFromIdSitesString($idSite); - } - - if (!($segment instanceof Piwik_Segment)) - { - $segment = new Piwik_Segment($segment, $idSite); - } - - // idSite=1,3 or idSite=all - if( $idSite === 'all' - || is_array($idSite) - || count($sites) > 1 ) - { - $archive = new Piwik_Archive_Array_IndexedBySite($sites, $period, $strDate, $segment, $_restrictSitesToLogin); - } - // if a period date string is detected: either 'last30', 'previous10' or 'YYYY-MM-DD,YYYY-MM-DD' - elseif(is_string($strDate) && self::isMultiplePeriod($strDate, $period)) - { - $oSite = new Piwik_Site($idSite); - $archive = new Piwik_Archive_Array_IndexedByDate($oSite, $period, $strDate, $segment); - } - // case we request a single archive - else - { - $oSite = new Piwik_Site($idSite); - $oPeriod = Piwik_Archive::makePeriodFromQueryParams($oSite, $period, $strDate); - - $archive = new Piwik_Archive_Single(); - $archive->setPeriod($oPeriod); - $archive->setSite($oSite); - $archive->setSegment($segment); - } - return $archive; - } - - /** - * Creates a period instance using a Piwik_Site instance and two strings describing - * the period & date. - * - * @param Piwik_Site $site - * @param string $strPeriod The period string: day, week, month, year, range - * @param string $strDate The date or date range string. - * @return Piwik_Period - */ - static public function makePeriodFromQueryParams( $site, $strPeriod, $strDate ) - { - $tz = $site->getTimezone(); - - if($strPeriod == 'range') - { - $oPeriod = new Piwik_Period_Range('range', $strDate, $tz, Piwik_Date::factory('today', $tz)); - } - else - { - $oDate = $strDate; - if(!($strDate instanceof Piwik_Date)) - { - if($strDate == 'now' || $strDate == 'today') - { - $strDate = date('Y-m-d', Piwik_Date::factory('now', $tz)->getTimestamp()); - } - elseif($strDate == 'yesterday' || $strDate == 'yesterdaySameTime') - { - $strDate = date('Y-m-d', Piwik_Date::factory('now', $tz)->subDay(1)->getTimestamp()); - } - $oDate = Piwik_Date::factory($strDate); - } - $date = $oDate->toString(); - $oPeriod = Piwik_Period::factory($strPeriod, $oDate); - } - - return $oPeriod; - } - - abstract public function prepareArchive(); - - /** - * Returns the value of the element $name from the current archive - * The value to be returned is a numeric value and is stored in the archive_numeric_* tables - * - * @param string $name For example Referers_distinctKeywords - * @return float|int|false False if no value with the given name - */ - abstract public function getNumeric( $name ); - - /** - * Returns the value of the element $name from the current archive - * - * The value to be returned is a blob value and is stored in the archive_numeric_* tables - * - * It can return anything from strings, to serialized PHP arrays or PHP objects, etc. - * - * @param string $name For example Referers_distinctKeywords - * @return mixed False if no value with the given name - */ - abstract public function getBlob( $name ); - - /** - * - * @param $fields - * @return Piwik_DataTable - */ - abstract public function getDataTableFromNumeric( $fields ); - - /** - * This method will build a dataTable from the blob value $name in the current archive. - * - * For example $name = 'Referers_searchEngineByKeyword' will return a Piwik_DataTable containing all the keywords - * If a idSubTable is given, the method will return the subTable of $name - * - * @param string $name - * @param int $idSubTable or null if requesting the parent table - * @return Piwik_DataTable - * @throws exception If the value cannot be found - */ - abstract public function getDataTable( $name, $idSubTable = null ); - - /** - * Same as getDataTable() except that it will also load in memory - * all the subtables for the DataTable $name. - * You can then access the subtables by using the Piwik_DataTable_Manager getTable() - * - * @param string $name - * @param int|null $idSubTable null if requesting the parent table - * @return Piwik_DataTable - */ - abstract public function getDataTableExpanded($name, $idSubTable = null); - - - /** - * Helper - Loads a DataTable from the Archive. - * Optionally loads the table recursively, - * or optionally fetches a given subtable with $idSubtable - * - * @param string $name - * @param int $idSite - * @param string $period - * @param Piwik_Date $date - * @param string $segment - * @param bool $expanded - * @param null $idSubtable - * @return Piwik_DataTable|Piwik_DataTable_Array - */ - static public function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable = null ) - { - Piwik::checkUserHasViewAccess( $idSite ); - $archive = Piwik_Archive::build($idSite, $period, $date, $segment ); - if($idSubtable === false) - { - $idSubtable = null; - } - - if($expanded) - { - $dataTable = $archive->getDataTableExpanded($name, $idSubtable); - } - else - { - $dataTable = $archive->getDataTable($name, $idSubtable); - } - - $dataTable->queueFilter('ReplaceSummaryRowLabel'); - - return $dataTable; - } - - protected function formatNumericValue($value) - { - // If there is no dot, we return as is - // Note: this could be an integer bigger than 32 bits - if(strpos($value, '.') === false) - { - if($value === false) - { - return 0; - } - return (float)$value; - } - - // Round up the value with 2 decimals - // we cast the result as float because returns false when no visitors - $value = round((float)$value, 2); - return $value; - } - - public function getSegment() - { - return $this->segment; - } - - public function setSegment(Piwik_Segment $segment) - { - $this->segment = $segment; - } - - /** - * Sets the site - * - * @param Piwik_Site $site - */ - public function setSite( Piwik_Site $site ) - { - $this->site = $site; - } - - /** - * Gets the site - * - * @return Piwik_Site - */ - public function getSite() - { - return $this->site; - } - - /** - * Returns the Id site associated with this archive - * - * @return int - */ - public function getIdSite() - { - return $this->site->getId(); - } - - /** - * Returns true if Segmentation is allowed for this user - * - * @return bool - */ - static public function isSegmentationEnabled() - { - return !Piwik::isUserIsAnonymous() - || Piwik_Config::getInstance()->General['anonymous_user_enable_use_segments_API'] - ; - } - - /** - * Indicate if $dateString and $period correspond to multiple periods - * - * @static - * @param $dateString - * @param $period - * @return boolean - */ - static public function isMultiplePeriod($dateString, $period) - { - return (preg_match('/^(last|previous){1}([0-9]*)$/D', $dateString, $regs) - || Piwik_Period_Range::parseDateRange($dateString)) - && $period != 'range'; - } - - /** - * Indicate if $idSiteString corresponds to multiple sites. - * - * @param string $idSiteString - * @return bool - */ - static public function isMultipleSites( $idSiteString ) - { - return $idSiteString == 'all' || strpos($idSiteString, ',') !== false; - } + */ + public static $mappingFromNameToId = array( + 'nb_uniq_visitors' => Piwik_Archive::INDEX_NB_UNIQ_VISITORS, + 'nb_visits' => Piwik_Archive::INDEX_NB_VISITS, + 'nb_actions' => Piwik_Archive::INDEX_NB_ACTIONS, + 'max_actions' => Piwik_Archive::INDEX_MAX_ACTIONS, + 'sum_visit_length' => Piwik_Archive::INDEX_SUM_VISIT_LENGTH, + 'bounce_count' => Piwik_Archive::INDEX_BOUNCE_COUNT, + 'nb_visits_converted' => Piwik_Archive::INDEX_NB_VISITS_CONVERTED, + 'nb_conversions' => Piwik_Archive::INDEX_NB_CONVERSIONS, + 'revenue' => Piwik_Archive::INDEX_REVENUE, + 'goals' => Piwik_Archive::INDEX_GOALS, + 'sum_daily_nb_uniq_visitors' => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS, + ); + + /** + * Metrics calculated and archived by the Actions plugin. + * + * @var array + */ + public static $actionsMetrics = array( + 'nb_pageviews', + 'nb_uniq_pageviews', + 'nb_downloads', + 'nb_uniq_downloads', + 'nb_outlinks', + 'nb_uniq_outlinks', + 'nb_searches', + 'nb_keywords', + 'nb_hits', + 'nb_hits_following_search', + ); + + const LABEL_ECOMMERCE_CART = 'ecommerceAbandonedCart'; + const LABEL_ECOMMERCE_ORDER = 'ecommerceOrder'; + + /** + * Website Piwik_Site + * + * @var Piwik_Site + */ + protected $site = null; + + /** + * Segment applied to the visits set + * @var Piwik_Segment + */ + protected $segment = false; + + /** + * Builds an Archive object or returns the same archive if previously built. + * + * @param int|string $idSite integer, or comma separated list of integer + * @param string $period 'week' 'day' etc. + * @param Piwik_Date|string $strDate 'YYYY-MM-DD' or magic keywords 'today' @see Piwik_Date::factory() + * @param bool|string $segment Segment definition - defaults to false for Backward Compatibility + * @param bool|string $_restrictSitesToLogin Used only when running as a scheduled task + * @return Piwik_Archive + */ + static public function build($idSite, $period, $strDate, $segment = false, $_restrictSitesToLogin = false) + { + if ($idSite === 'all') { + $sites = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess($_restrictSitesToLogin); + } else { + $sites = Piwik_Site::getIdSitesFromIdSitesString($idSite); + } + + if (!($segment instanceof Piwik_Segment)) { + $segment = new Piwik_Segment($segment, $idSite); + } + + // idSite=1,3 or idSite=all + if ($idSite === 'all' + || is_array($idSite) + || count($sites) > 1 + ) { + $archive = new Piwik_Archive_Array_IndexedBySite($sites, $period, $strDate, $segment, $_restrictSitesToLogin); + } // if a period date string is detected: either 'last30', 'previous10' or 'YYYY-MM-DD,YYYY-MM-DD' + elseif (is_string($strDate) && self::isMultiplePeriod($strDate, $period)) { + $oSite = new Piwik_Site($idSite); + $archive = new Piwik_Archive_Array_IndexedByDate($oSite, $period, $strDate, $segment); + } // case we request a single archive + else { + $oSite = new Piwik_Site($idSite); + $oPeriod = Piwik_Archive::makePeriodFromQueryParams($oSite, $period, $strDate); + + $archive = new Piwik_Archive_Single(); + $archive->setPeriod($oPeriod); + $archive->setSite($oSite); + $archive->setSegment($segment); + } + return $archive; + } + + /** + * Creates a period instance using a Piwik_Site instance and two strings describing + * the period & date. + * + * @param Piwik_Site $site + * @param string $strPeriod The period string: day, week, month, year, range + * @param string $strDate The date or date range string. + * @return Piwik_Period + */ + static public function makePeriodFromQueryParams($site, $strPeriod, $strDate) + { + $tz = $site->getTimezone(); + + if ($strPeriod == 'range') { + $oPeriod = new Piwik_Period_Range('range', $strDate, $tz, Piwik_Date::factory('today', $tz)); + } else { + $oDate = $strDate; + if (!($strDate instanceof Piwik_Date)) { + if ($strDate == 'now' || $strDate == 'today') { + $strDate = date('Y-m-d', Piwik_Date::factory('now', $tz)->getTimestamp()); + } elseif ($strDate == 'yesterday' || $strDate == 'yesterdaySameTime') { + $strDate = date('Y-m-d', Piwik_Date::factory('now', $tz)->subDay(1)->getTimestamp()); + } + $oDate = Piwik_Date::factory($strDate); + } + $date = $oDate->toString(); + $oPeriod = Piwik_Period::factory($strPeriod, $oDate); + } + + return $oPeriod; + } + + abstract public function prepareArchive(); + + /** + * Returns the value of the element $name from the current archive + * The value to be returned is a numeric value and is stored in the archive_numeric_* tables + * + * @param string $name For example Referers_distinctKeywords + * @return float|int|false False if no value with the given name + */ + abstract public function getNumeric($name); + + /** + * Returns the value of the element $name from the current archive + * + * The value to be returned is a blob value and is stored in the archive_numeric_* tables + * + * It can return anything from strings, to serialized PHP arrays or PHP objects, etc. + * + * @param string $name For example Referers_distinctKeywords + * @return mixed False if no value with the given name + */ + abstract public function getBlob($name); + + /** + * + * @param $fields + * @return Piwik_DataTable + */ + abstract public function getDataTableFromNumeric($fields); + + /** + * This method will build a dataTable from the blob value $name in the current archive. + * + * For example $name = 'Referers_searchEngineByKeyword' will return a Piwik_DataTable containing all the keywords + * If a idSubTable is given, the method will return the subTable of $name + * + * @param string $name + * @param int $idSubTable or null if requesting the parent table + * @return Piwik_DataTable + * @throws exception If the value cannot be found + */ + abstract public function getDataTable($name, $idSubTable = null); + + /** + * Same as getDataTable() except that it will also load in memory + * all the subtables for the DataTable $name. + * You can then access the subtables by using the Piwik_DataTable_Manager getTable() + * + * @param string $name + * @param int|null $idSubTable null if requesting the parent table + * @return Piwik_DataTable + */ + abstract public function getDataTableExpanded($name, $idSubTable = null); + + + /** + * Helper - Loads a DataTable from the Archive. + * Optionally loads the table recursively, + * or optionally fetches a given subtable with $idSubtable + * + * @param string $name + * @param int $idSite + * @param string $period + * @param Piwik_Date $date + * @param string $segment + * @param bool $expanded + * @param null $idSubtable + * @return Piwik_DataTable|Piwik_DataTable_Array + */ + static public function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable = null) + { + Piwik::checkUserHasViewAccess($idSite); + $archive = Piwik_Archive::build($idSite, $period, $date, $segment); + if ($idSubtable === false) { + $idSubtable = null; + } + + if ($expanded) { + $dataTable = $archive->getDataTableExpanded($name, $idSubtable); + } else { + $dataTable = $archive->getDataTable($name, $idSubtable); + } + + $dataTable->queueFilter('ReplaceSummaryRowLabel'); + + return $dataTable; + } + + protected function formatNumericValue($value) + { + // If there is no dot, we return as is + // Note: this could be an integer bigger than 32 bits + if (strpos($value, '.') === false) { + if ($value === false) { + return 0; + } + return (float)$value; + } + + // Round up the value with 2 decimals + // we cast the result as float because returns false when no visitors + $value = round((float)$value, 2); + return $value; + } + + public function getSegment() + { + return $this->segment; + } + + public function setSegment(Piwik_Segment $segment) + { + $this->segment = $segment; + } + + /** + * Sets the site + * + * @param Piwik_Site $site + */ + public function setSite(Piwik_Site $site) + { + $this->site = $site; + } + + /** + * Gets the site + * + * @return Piwik_Site + */ + public function getSite() + { + return $this->site; + } + + /** + * Returns the Id site associated with this archive + * + * @return int + */ + public function getIdSite() + { + return $this->site->getId(); + } + + /** + * Returns true if Segmentation is allowed for this user + * + * @return bool + */ + static public function isSegmentationEnabled() + { + return !Piwik::isUserIsAnonymous() + || Piwik_Config::getInstance()->General['anonymous_user_enable_use_segments_API']; + } + + /** + * Indicate if $dateString and $period correspond to multiple periods + * + * @static + * @param $dateString + * @param $period + * @return boolean + */ + static public function isMultiplePeriod($dateString, $period) + { + return (preg_match('/^(last|previous){1}([0-9]*)$/D', $dateString, $regs) + || Piwik_Period_Range::parseDateRange($dateString)) + && $period != 'range'; + } + + /** + * Indicate if $idSiteString corresponds to multiple sites. + * + * @param string $idSiteString + * @return bool + */ + static public function isMultipleSites($idSiteString) + { + return $idSiteString == 'all' || strpos($idSiteString, ',') !== false; + } } diff --git a/core/Archive/Array.php b/core/Archive/Array.php index cd9472a245..896cc1e17c 100644 --- a/core/Archive/Array.php +++ b/core/Archive/Array.php @@ -1,152 +1,147 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * Piwik_Archive_Array is used to store multiple archives, + * Piwik_Archive_Array is used to store multiple archives, * for example one archive for a given day for each Piwik website * * @package Piwik * @subpackage Piwik_Archive */ abstract class Piwik_Archive_Array extends Piwik_Archive -{ - /** - * This array contains one Piwik_Archive per entry in the period - * - * @var Piwik_Archive[] - */ - protected $archives = array(); - - abstract protected function getIndexName(); - abstract protected function getDataTableLabelValue( $archive ); +{ + /** + * This array contains one Piwik_Archive per entry in the period + * + * @var Piwik_Archive[] + */ + protected $archives = array(); + + abstract protected function getIndexName(); + + abstract protected function getDataTableLabelValue($archive); + + /** + * Destructor + */ + public function __destruct() + { + foreach ($this->archives as $archive) { + destroy($archive); + } + $this->archives = array(); + } + + /** + * Prepares each archive in the array + */ + public function prepareArchive() + { + foreach ($this->archives as $archive) { + $archive->prepareArchive(); + } + } + + /** + * Returns a newly created Piwik_DataTable_Array. + * + * @return Piwik_DataTable_Array + */ + protected function getNewDataTableArray() + { + $table = new Piwik_DataTable_Array(); + $table->setKeyName($this->getIndexName()); + return $table; + } + + /** + * Returns a DataTable_Array containing numeric values + * of the element $name from the archives in this Archive_Array. + * + * @param string $name Name of the mysql table field to load eg. Referers_distinctKeywords + * @return Piwik_DataTable_Array containing the requested numeric value for each Archive + */ + public function getNumeric($name) + { + $table = $this->getNewDataTableArray(); + + foreach ($this->archives as $archive) { + $numeric = $archive->getNumeric($name); + $subTable = $archive->makeDataTable($isSimple = true); + $subTable->addRowsFromArray(array($numeric)); + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + } + + return $table; + } + + /** + * Returns a DataTable_Array containing values + * of the element $name from the archives in this Archive_Array. + * + * The value to be returned are blob values (stored in the archive_numeric_* tables in the DB). * + * It can return anything from strings, to serialized PHP arrays or PHP objects, etc. + * + * @param string $name Name of the mysql table field to load eg. Referers_keywordBySearchEngine + * @return Piwik_DataTable_Array containing the requested blob values for each Archive + */ + public function getBlob($name) + { + $table = $this->getNewDataTableArray(); + + foreach ($this->archives as $archive) { + $blob = $archive->getBlob($name); + $subTable = $archive->makeNewDataTable($isSimple = true); + $subTable->addRowsFromArray(array('blob' => $blob)); + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + } + return $table; + } + + /** + * Given a BLOB field name (eg. 'Referers_searchEngineByKeyword'), it will return a Piwik_DataTable_Array + * which is an array of Piwik_DataTable, ordered by chronological order + * + * @param string $name Name of the mysql table field to load + * @param int $idSubTable optional idSubDataTable + * @return Piwik_DataTable_Array + * @throws Exception If the value cannot be found + */ + public function getDataTable($name, $idSubTable = null) + { + $table = $this->getNewDataTableArray(); + foreach ($this->archives as $archive) { + $subTable = $archive->getDataTable($name, $idSubTable); + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + } + return $table; + } - /** - * Destructor - */ - public function __destruct() - { - foreach($this->archives as $archive) - { - destroy($archive); - } - $this->archives = array(); - } - /** - * Prepares each archive in the array - */ - public function prepareArchive() - { - foreach($this->archives as $archive) - { - $archive->prepareArchive(); - } - } - - /** - * Returns a newly created Piwik_DataTable_Array. - * - * @return Piwik_DataTable_Array - */ - protected function getNewDataTableArray() - { - $table = new Piwik_DataTable_Array(); - $table->setKeyName($this->getIndexName()); - return $table; - } - - /** - * Returns a DataTable_Array containing numeric values - * of the element $name from the archives in this Archive_Array. - * - * @param string $name Name of the mysql table field to load eg. Referers_distinctKeywords - * @return Piwik_DataTable_Array containing the requested numeric value for each Archive - */ - public function getNumeric( $name ) - { - $table = $this->getNewDataTableArray(); - - foreach($this->archives as $archive) - { - $numeric = $archive->getNumeric( $name ) ; - $subTable = $archive->makeDataTable($isSimple = true); - $subTable->addRowsFromArray( array( $numeric ) ); - $table->addTable($subTable, $this->getDataTableLabelValue($archive)); - } - - return $table; - } - - /** - * Returns a DataTable_Array containing values - * of the element $name from the archives in this Archive_Array. - * - * The value to be returned are blob values (stored in the archive_numeric_* tables in the DB). * - * It can return anything from strings, to serialized PHP arrays or PHP objects, etc. - * - * @param string $name Name of the mysql table field to load eg. Referers_keywordBySearchEngine - * @return Piwik_DataTable_Array containing the requested blob values for each Archive - */ - public function getBlob( $name ) - { - $table = $this->getNewDataTableArray(); - - foreach($this->archives as $archive) - { - $blob = $archive->getBlob( $name ) ; - $subTable = $archive->makeNewDataTable($isSimple = true); - $subTable->addRowsFromArray( array('blob' => $blob)); - $table->addTable($subTable, $this->getDataTableLabelValue($archive)); - } - return $table; - } - - /** - * Given a BLOB field name (eg. 'Referers_searchEngineByKeyword'), it will return a Piwik_DataTable_Array - * which is an array of Piwik_DataTable, ordered by chronological order - * - * @param string $name Name of the mysql table field to load - * @param int $idSubTable optional idSubDataTable - * @return Piwik_DataTable_Array - * @throws Exception If the value cannot be found - */ - public function getDataTable( $name, $idSubTable = null ) - { - $table = $this->getNewDataTableArray(); - foreach($this->archives as $archive) - { - $subTable = $archive->getDataTable( $name, $idSubTable ) ; - $table->addTable($subTable, $this->getDataTableLabelValue($archive)); - } - return $table; - } - - - /** - * Same as getDataTable() except that it will also load in memory - * all the subtables for the DataTable $name. - * You can then access the subtables by using the Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); - * - * @param string $name Name of the mysql table field to load - * @param int $idSubTable optional idSubDataTable - * @return Piwik_DataTable_Array - */ - public function getDataTableExpanded($name, $idSubTable = null) - { - $table = $this->getNewDataTableArray(); - foreach($this->archives as $archive) - { - $subTable = $archive->getDataTableExpanded( $name, $idSubTable ) ; - $table->addTable($subTable, $this->getDataTableLabelValue($archive)); - } - return $table; - } + /** + * Same as getDataTable() except that it will also load in memory + * all the subtables for the DataTable $name. + * You can then access the subtables by using the Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + * + * @param string $name Name of the mysql table field to load + * @param int $idSubTable optional idSubDataTable + * @return Piwik_DataTable_Array + */ + public function getDataTableExpanded($name, $idSubTable = null) + { + $table = $this->getNewDataTableArray(); + foreach ($this->archives as $archive) { + $subTable = $archive->getDataTableExpanded($name, $idSubTable); + $table->addTable($subTable, $this->getDataTableLabelValue($archive)); + } + return $table; + } } diff --git a/core/Archive/Array/IndexedByDate.php b/core/Archive/Array/IndexedByDate.php index 91bbab2a78..9a59712d4a 100644 --- a/core/Archive/Array/IndexedByDate.php +++ b/core/Archive/Array/IndexedByDate.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -13,129 +13,119 @@ * @package Piwik * @subpackage Piwik_Archive */ -class Piwik_Archive_Array_IndexedByDate extends Piwik_Archive_Array +class Piwik_Archive_Array_IndexedByDate extends Piwik_Archive_Array { - /** - * Builds an array of Piwik_Archive of a given date range - * - * @param Piwik_Site $oSite - * @param string $strPeriod eg. 'day' 'week' etc. - * @param string $strDate A date range, eg. 'last10', 'previous5' or 'YYYY-MM-DD,YYYY-MM-DD' - * @param Piwik_Segment $segment - */ - public function __construct(Piwik_Site $oSite, $strPeriod, $strDate, Piwik_Segment $segment) - { - $rangePeriod = new Piwik_Period_Range($strPeriod, $strDate, $oSite->getTimezone()); - foreach($rangePeriod->getSubperiods() as $subPeriod) - { - $startDate = $subPeriod->getDateStart(); - $archive = Piwik_Archive::build($oSite->getId(), $strPeriod, $startDate, $segment->getString() ); - $archive->setSegment($segment); - $this->archives[] = $archive; - } - $this->setSite($oSite); - } + /** + * Builds an array of Piwik_Archive of a given date range + * + * @param Piwik_Site $oSite + * @param string $strPeriod eg. 'day' 'week' etc. + * @param string $strDate A date range, eg. 'last10', 'previous5' or 'YYYY-MM-DD,YYYY-MM-DD' + * @param Piwik_Segment $segment + */ + public function __construct(Piwik_Site $oSite, $strPeriod, $strDate, Piwik_Segment $segment) + { + $rangePeriod = new Piwik_Period_Range($strPeriod, $strDate, $oSite->getTimezone()); + foreach ($rangePeriod->getSubperiods() as $subPeriod) { + $startDate = $subPeriod->getDateStart(); + $archive = Piwik_Archive::build($oSite->getId(), $strPeriod, $startDate, $segment->getString()); + $archive->setSegment($segment); + $this->archives[] = $archive; + } + $this->setSite($oSite); + } - /** - * @return string - */ - protected function getIndexName() - { - return 'date'; - } + /** + * @return string + */ + protected function getIndexName() + { + return 'date'; + } - /** - * @param Piwik_Archive $archive - * @return mixed - */ - protected function getDataTableLabelValue( $archive ) - { - return $archive->getPrettyDate(); - } - - /** - * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Array - * which is an array of Piwik_DataTable_Simple, ordered by chronological order - * - * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load - * @return Piwik_DataTable_Array - */ - public function getDataTableFromNumeric( $fields ) - { - $inNames = Piwik_Common::getSqlStringFieldsArray($fields); - - // we select in different shots - // one per distinct table (case we select last 300 days, maybe we will select from 10 different tables) - $queries = array(); - foreach($this->archives as $archive) - { - $archive->setRequestedReport( is_string($fields) ? $fields : current($fields) ); - $archive->prepareArchive(); - if(!$archive->isThereSomeVisits) - { - continue; - } - - $table = $archive->archiveProcessing->getTableArchiveNumericName(); + /** + * @param Piwik_Archive $archive + * @return mixed + */ + protected function getDataTableLabelValue($archive) + { + return $archive->getPrettyDate(); + } - // for every query store IDs - $queries[$table][] = $archive->getIdArchive(); - } - // we select the requested value - $db = Zend_Registry::get('db'); - - // date => array( 'field1' =>X, 'field2'=>Y) - // date2 => array( 'field1' =>X2, 'field2'=>Y2) - - $arrayValues = array(); - foreach($queries as $table => $aIds) - { - $inIds = implode(', ', array_filter($aIds)); - if(empty($inIds)) - { - // Probable timezone configuration error, i.e., mismatch between PHP and MySQL server. - continue; - } + /** + * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Array + * which is an array of Piwik_DataTable_Simple, ordered by chronological order + * + * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load + * @return Piwik_DataTable_Array + */ + public function getDataTableFromNumeric($fields) + { + $inNames = Piwik_Common::getSqlStringFieldsArray($fields); - $sql = "SELECT value, name, date1 as startDate + // we select in different shots + // one per distinct table (case we select last 300 days, maybe we will select from 10 different tables) + $queries = array(); + foreach ($this->archives as $archive) { + $archive->setRequestedReport(is_string($fields) ? $fields : current($fields)); + $archive->prepareArchive(); + if (!$archive->isThereSomeVisits) { + continue; + } + + $table = $archive->archiveProcessing->getTableArchiveNumericName(); + + // for every query store IDs + $queries[$table][] = $archive->getIdArchive(); + } + // we select the requested value + $db = Zend_Registry::get('db'); + + // date => array( 'field1' =>X, 'field2'=>Y) + // date2 => array( 'field1' =>X2, 'field2'=>Y2) + + $arrayValues = array(); + foreach ($queries as $table => $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, date1 as startDate FROM $table WHERE idarchive IN ( $inIds ) AND name IN ( $inNames ) ORDER BY date1, name"; - $values = $db->fetchAll($sql, $fields); - foreach($values as $value) - { - $timestamp = Piwik_Date::factory($value['startDate'])->getTimestamp(); - $arrayValues[$timestamp][$value['name']] = $this->formatNumericValue($value['value']); - } - } - - $contentArray = array(); - // we add empty tables so that every requested date has an entry, even if there is nothing - // example: <result date="2007-01-01" /> - $archiveByTimestamp = array(); - foreach($this->archives as $archive) - { - $timestamp = $archive->getTimestampStartDate(); - $archiveByTimestamp[$timestamp] = $archive; - $contentArray[$timestamp]['table'] = $archive->makeDataTable($simple = true); - $contentArray[$timestamp]['prettyDate'] = $archive->getPrettyDate(); - } + $values = $db->fetchAll($sql, $fields); + foreach ($values as $value) { + $timestamp = Piwik_Date::factory($value['startDate'])->getTimestamp(); + $arrayValues[$timestamp][$value['name']] = $this->formatNumericValue($value['value']); + } + } + + $contentArray = array(); + // we add empty tables so that every requested date has an entry, even if there is nothing + // example: <result date="2007-01-01" /> + $archiveByTimestamp = array(); + foreach ($this->archives as $archive) { + $timestamp = $archive->getTimestampStartDate(); + $archiveByTimestamp[$timestamp] = $archive; + $contentArray[$timestamp]['table'] = $archive->makeDataTable($simple = true); + $contentArray[$timestamp]['prettyDate'] = $archive->getPrettyDate(); + } - foreach($arrayValues as $timestamp => $aNameValues) - { - // undefined in some edge/unknown cases see http://dev.piwik.org/trac/ticket/2578 - if(isset($contentArray[$timestamp]['table'])) - { - $contentArray[$timestamp]['table']->addRowsFromArray($aNameValues); - } - } + foreach ($arrayValues as $timestamp => $aNameValues) { + // undefined in some edge/unknown cases see http://dev.piwik.org/trac/ticket/2578 + if (isset($contentArray[$timestamp]['table'])) { + $contentArray[$timestamp]['table']->addRowsFromArray($aNameValues); + } + } - $tableArray = $this->getNewDataTableArray(); - foreach($contentArray as $timestamp => $aData) - { - $tableArray->addTable($aData['table'], $aData['prettyDate']); - } - return $tableArray; - } + $tableArray = $this->getNewDataTableArray(); + foreach ($contentArray as $timestamp => $aData) { + $tableArray->addTable($aData['table'], $aData['prettyDate']); + } + return $tableArray; + } } diff --git a/core/Archive/Array/IndexedBySite.php b/core/Archive/Array/IndexedBySite.php index c6fee07bcc..1ac9ff682a 100644 --- a/core/Archive/Array/IndexedBySite.php +++ b/core/Archive/Array/IndexedBySite.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -13,274 +13,254 @@ * @package Piwik * @subpackage Piwik_Archive */ -class Piwik_Archive_Array_IndexedBySite extends Piwik_Archive_Array +class Piwik_Archive_Array_IndexedBySite extends Piwik_Archive_Array { - /** - * Used to cache the name of the table that holds the data this archive. - * - * This will only be used if the archives held by this instance are instances of - * Piwik_Archive_Single. - */ - private $tableName = null; - - /** - * @param array $sites array of siteIds - * @param string $strPeriod eg. 'day' 'week' etc. - * @param string $strDate A date range, eg. 'last10', 'previous5' or 'YYYY-MM-DD,YYYY-MM-DD' - * @param Piwik_Segment $segment - * @param string $_restrictSitesToLogin - */ - function __construct($sites, $strPeriod, $strDate, Piwik_Segment $segment, $_restrictSitesToLogin) - { - foreach($sites as $idSite) - { - $archive = Piwik_Archive::build($idSite, $strPeriod, $strDate, $segment, $_restrictSitesToLogin ); - $this->archives[$idSite] = $archive; - } - ksort( $this->archives ); - } - - /** - * @return string - */ - protected function getIndexName() - { - return 'idSite'; - } - - /** - * @param Piwik_Archive $archive - * @return mixed - */ - protected function getDataTableLabelValue( $archive ) - { - return $archive->getIdSite(); - } - - /** - * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Array - * ordered by idsite - * - * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load - * @return Piwik_DataTable_Array - */ - public function getDataTableFromNumeric( $fields ) - { - $tableArray = $this->getNewDataTableArray(); - if ($this->getFirstArchive() instanceof Piwik_Archive_Single) - { - $values = $this->getValues($fields); - foreach($this->archives as $idSite => $archive) - { - $table = $archive->makeDataTable($isSimple = true); - if (array_key_exists($idSite, $values)) - { - $table->addRowsFromArray($values[$idSite]); - } - $tableArray->addTable($table, $idSite); - } - } - elseif ($this->getFirstArchive() instanceof Piwik_Archive_Array) - { - foreach($this->archives as $idSite => $archive) - { - $tableArray->addTable($archive->getDataTableFromNumeric($fields), $idSite); - } - } - - return $tableArray; - } - - /** - * Returns the values of the requested fields - * - * @param array $fields - * @return array - */ - private function getValues($fields) - { - // Creating the default array, to ensure consistent order - $defaultValues = array(); - foreach($fields as $field) { $defaultValues[$field] = null; } - - $arrayValues = array(); - foreach($this->loadValuesFromDB($fields) as $value) - { - if(!isset($arrayValues[$value['idsite']])) - { - $arrayValues[$value['idsite']] = $defaultValues; - } - $arrayValues[$value['idsite']][$value['name']] = $this->formatNumericValue( $value['value'] ); - } - return $arrayValues; - } - - /** - * @param $fields - * @return array|array (one row in the array per row fetched in the DB) - */ - private function loadValuesFromDB($fields) - { - $requestedMetrics = is_string($fields) ? array($fields) : $fields; - $inNames = Piwik_Common::getSqlStringFieldsArray($fields); - - // get the archive ids - if (!$this->getFirstArchive()->isArchivingDisabled()) - { - $archiveIds = $this->getArchiveIdsAfterLaunching($requestedMetrics); - } - else - { - $archiveIds = $this->getArchiveIdsWithoutLaunching($requestedMetrics); - } - - $archiveIds = implode(', ', array_filter($archiveIds)); - - // if no archive ids are found, avoid executing any SQL queries - if(empty($archiveIds)) - { - return array(); - } - - // select archive data - $sql = "SELECT value, name, idarchive, idsite + /** + * Used to cache the name of the table that holds the data this archive. + * + * This will only be used if the archives held by this instance are instances of + * Piwik_Archive_Single. + */ + private $tableName = null; + + /** + * @param array $sites array of siteIds + * @param string $strPeriod eg. 'day' 'week' etc. + * @param string $strDate A date range, eg. 'last10', 'previous5' or 'YYYY-MM-DD,YYYY-MM-DD' + * @param Piwik_Segment $segment + * @param string $_restrictSitesToLogin + */ + function __construct($sites, $strPeriod, $strDate, Piwik_Segment $segment, $_restrictSitesToLogin) + { + foreach ($sites as $idSite) { + $archive = Piwik_Archive::build($idSite, $strPeriod, $strDate, $segment, $_restrictSitesToLogin); + $this->archives[$idSite] = $archive; + } + ksort($this->archives); + } + + /** + * @return string + */ + protected function getIndexName() + { + return 'idSite'; + } + + /** + * @param Piwik_Archive $archive + * @return mixed + */ + protected function getDataTableLabelValue($archive) + { + return $archive->getIdSite(); + } + + /** + * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Array + * ordered by idsite + * + * @param array|string $fields array( fieldName1, fieldName2, ...) Names of the mysql table fields to load + * @return Piwik_DataTable_Array + */ + public function getDataTableFromNumeric($fields) + { + $tableArray = $this->getNewDataTableArray(); + if ($this->getFirstArchive() instanceof Piwik_Archive_Single) { + $values = $this->getValues($fields); + foreach ($this->archives as $idSite => $archive) { + $table = $archive->makeDataTable($isSimple = true); + if (array_key_exists($idSite, $values)) { + $table->addRowsFromArray($values[$idSite]); + } + $tableArray->addTable($table, $idSite); + } + } elseif ($this->getFirstArchive() instanceof Piwik_Archive_Array) { + foreach ($this->archives as $idSite => $archive) { + $tableArray->addTable($archive->getDataTableFromNumeric($fields), $idSite); + } + } + + return $tableArray; + } + + /** + * Returns the values of the requested fields + * + * @param array $fields + * @return array + */ + private function getValues($fields) + { + // Creating the default array, to ensure consistent order + $defaultValues = array(); + foreach ($fields as $field) { + $defaultValues[$field] = null; + } + + $arrayValues = array(); + foreach ($this->loadValuesFromDB($fields) as $value) { + if (!isset($arrayValues[$value['idsite']])) { + $arrayValues[$value['idsite']] = $defaultValues; + } + $arrayValues[$value['idsite']][$value['name']] = $this->formatNumericValue($value['value']); + } + return $arrayValues; + } + + /** + * @param $fields + * @return array|array (one row in the array per row fetched in the DB) + */ + private function loadValuesFromDB($fields) + { + $requestedMetrics = is_string($fields) ? array($fields) : $fields; + $inNames = Piwik_Common::getSqlStringFieldsArray($fields); + + // get the archive ids + if (!$this->getFirstArchive()->isArchivingDisabled()) { + $archiveIds = $this->getArchiveIdsAfterLaunching($requestedMetrics); + } else { + $archiveIds = $this->getArchiveIdsWithoutLaunching($requestedMetrics); + } + + $archiveIds = implode(', ', array_filter($archiveIds)); + + // if no archive ids are found, avoid executing any SQL queries + if (empty($archiveIds)) { + return array(); + } + + // select archive data + $sql = "SELECT value, name, idarchive, idsite FROM {$this->getNumericTableName()} WHERE idarchive IN ( $archiveIds ) AND name IN ( $inNames )"; - - return Piwik_FetchAll($sql, $fields); - } - - /** - * Returns the first archive in the list - * - * @return Piwik_Archive - */ - private function getFirstArchive() - { - return reset($this->archives); - } - - /** - * Gets the archive id of every Single archive this archive holds. This method - * will launch the archiving process if appropriate. - * - * @param array $metrics The requested archive metrics. - * @throws Exception - * @return array - */ - private function getArchiveIdsAfterLaunching( $metrics ) - { - // collect the individual report names for the requested metrics - $reports = array(); - foreach($metrics as $metric) - { - $report = Piwik_Archive_Single::getRequestedReportFor($metric); - $reports[$report] = $metric; - } - - // process archives for each individual report - $archiveIds = array(); - foreach($reports as $report => $metric) - { - // prepare archives (this will launch archiving when appropriate) - foreach($this->archives as $archive) - { - // NOTE: Piwik_Archive_Single expects a metric here, not a report - $archive->setRequestedReport( $metric ); - $archive->prepareArchive(); - } - - // collect archive ids for archives that have visits - foreach($this->archives as $archive) - { - if( !$archive->isThereSomeVisits ) - { - continue; - } - - $archiveIds[] = $archive->getIdArchive(); - - if( $this->getNumericTableName() != $archive->archiveProcessing->getTableArchiveNumericName()) - { - throw new Exception("Piwik_Archive_Array_IndexedBySite::getDataTableFromNumeric() algorithm won't work if data is stored in different tables"); - } - } - } - - return $archiveIds; - } - - /** - * Gets the archive id of every Single archive this archive holds. This method - * will not launch the archiving process. - * - * @param array $metrics The requested archive metrics. - * @return array - */ - private function getArchiveIdsWithoutLaunching( $metrics ) - { - $firstArchive = $this->getFirstArchive(); - $segment = $firstArchive->getSegment(); - $period = $firstArchive->getPeriod(); - - // the flags used to tell how the archiving process for a specific archive was completed, - // if it was completed - $doneFlags = array(); - foreach ($metrics as $metric) - { - $done = Piwik_ArchiveProcessing::getDoneStringFlagFor($segment, $period, $metric); - $donePlugins = Piwik_ArchiveProcessing::getDoneStringFlagFor($segment, $period, $metric, true); - - $doneFlags[$done] = $done; - $doneFlags[$donePlugins] = $donePlugins; - } - - $allDoneFlags = "'".implode("','", $doneFlags)."'"; - - // create the SQL to query every archive ID - $nameCondition = "(name IN ($allDoneFlags)) AND - (value = '".Piwik_ArchiveProcessing::DONE_OK."' OR - value = '".Piwik_ArchiveProcessing::DONE_OK_TEMPORARY."')"; - - $sql = "SELECT idsite, + + return Piwik_FetchAll($sql, $fields); + } + + /** + * Returns the first archive in the list + * + * @return Piwik_Archive + */ + private function getFirstArchive() + { + return reset($this->archives); + } + + /** + * Gets the archive id of every Single archive this archive holds. This method + * will launch the archiving process if appropriate. + * + * @param array $metrics The requested archive metrics. + * @throws Exception + * @return array + */ + private function getArchiveIdsAfterLaunching($metrics) + { + // collect the individual report names for the requested metrics + $reports = array(); + foreach ($metrics as $metric) { + $report = Piwik_Archive_Single::getRequestedReportFor($metric); + $reports[$report] = $metric; + } + + // process archives for each individual report + $archiveIds = array(); + foreach ($reports as $report => $metric) { + // prepare archives (this will launch archiving when appropriate) + foreach ($this->archives as $archive) { + // NOTE: Piwik_Archive_Single expects a metric here, not a report + $archive->setRequestedReport($metric); + $archive->prepareArchive(); + } + + // collect archive ids for archives that have visits + foreach ($this->archives as $archive) { + if (!$archive->isThereSomeVisits) { + continue; + } + + $archiveIds[] = $archive->getIdArchive(); + + if ($this->getNumericTableName() != $archive->archiveProcessing->getTableArchiveNumericName()) { + throw new Exception("Piwik_Archive_Array_IndexedBySite::getDataTableFromNumeric() algorithm won't work if data is stored in different tables"); + } + } + } + + return $archiveIds; + } + + /** + * Gets the archive id of every Single archive this archive holds. This method + * will not launch the archiving process. + * + * @param array $metrics The requested archive metrics. + * @return array + */ + private function getArchiveIdsWithoutLaunching($metrics) + { + $firstArchive = $this->getFirstArchive(); + $segment = $firstArchive->getSegment(); + $period = $firstArchive->getPeriod(); + + // the flags used to tell how the archiving process for a specific archive was completed, + // if it was completed + $doneFlags = array(); + foreach ($metrics as $metric) { + $done = Piwik_ArchiveProcessing::getDoneStringFlagFor($segment, $period, $metric); + $donePlugins = Piwik_ArchiveProcessing::getDoneStringFlagFor($segment, $period, $metric, true); + + $doneFlags[$done] = $done; + $doneFlags[$donePlugins] = $donePlugins; + } + + $allDoneFlags = "'" . implode("','", $doneFlags) . "'"; + + // create the SQL to query every archive ID + $nameCondition = "(name IN ($allDoneFlags)) AND + (value = '" . Piwik_ArchiveProcessing::DONE_OK . "' OR + value = '" . Piwik_ArchiveProcessing::DONE_OK_TEMPORARY . "')"; + + $sql = "SELECT idsite, MAX(idarchive) AS idarchive - FROM ".$this->getNumericTableName()." + FROM " . $this->getNumericTableName() . " WHERE date1 = ? AND date2 = ? AND period = ? AND $nameCondition - AND idsite IN (".implode(',', array_keys($this->archives)).") + AND idsite IN (" . implode(',', array_keys($this->archives)) . ") GROUP BY idsite"; - - $bind = array($period->getDateStart()->toString('Y-m-d'), - $period->getDateEnd()->toString('Y-m-d'), - $period->getId()); - - // execute the query and process the results. - $archiveIds = array(); - foreach (Piwik_FetchAll($sql, $bind) as $row) - { - $archiveIds[] = $row['idarchive']; - } - - return $archiveIds; - } - - /** - * Gets the name of the database table that holds the numeric archive data for - * this archive. - * - * @return string - */ - private function getNumericTableName() - { - if (is_null($this->tableName)) - { - $table = Piwik_ArchiveProcessing::makeNumericArchiveTable($this->getFirstArchive()->getPeriod()); - $this->tableName = $table->getTableName(); - } - - return $this->tableName; - } + + $bind = array($period->getDateStart()->toString('Y-m-d'), + $period->getDateEnd()->toString('Y-m-d'), + $period->getId()); + + // execute the query and process the results. + $archiveIds = array(); + foreach (Piwik_FetchAll($sql, $bind) as $row) { + $archiveIds[] = $row['idarchive']; + } + + return $archiveIds; + } + + /** + * Gets the name of the database table that holds the numeric archive data for + * this archive. + * + * @return string + */ + private function getNumericTableName() + { + if (is_null($this->tableName)) { + $table = Piwik_ArchiveProcessing::makeNumericArchiveTable($this->getFirstArchive()->getPeriod()); + $this->tableName = $table->getTableName(); + } + + return $this->tableName; + } } diff --git a/core/Archive/Single.php b/core/Archive/Single.php index adb73095e5..2f4d5e283e 100644 --- a/core/Archive/Single.php +++ b/core/Archive/Single.php @@ -1,668 +1,631 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * - * + * + * * @category Piwik * @package Piwik */ /** - * Piwik_Archive_Single is used to store the data of a single archive, - * for example the statistics for the 'day' '2008-02-21' for the website idSite '2' + * Piwik_Archive_Single is used to store the data of a single archive, + * for example the statistics for the 'day' '2008-02-21' for the website idSite '2' * * @package Piwik * @subpackage Piwik_Archive */ class Piwik_Archive_Single extends Piwik_Archive { - /** - * The Piwik_ArchiveProcessing object used to check that the archive is available - * and launch the processing if the archive was not yet processed - * - * @var Piwik_ArchiveProcessing - */ - public $archiveProcessing = null; - - /** - * @var bool Set to true if the archive has at least 1 visit - */ - public $isThereSomeVisits = null; - - /** - * Period of this Archive - * - * @var Piwik_Period - */ - protected $period = null; - - /** - * Set to true will activate numeric value caching for this archive. - * - * @var bool - */ - protected $cacheEnabledForNumeric = true; - - /** - * Array of cached numeric values, used to make requests faster - * when requesting the same value again and again - * - * @var array of numeric - */ - protected $numericCached = array(); - - /** - * Array of cached blob, used to make requests faster when requesting the same blob again and again - * - * @var array of mixed - */ - protected $blobCached = array(); - - /** - * idarchive of this Archive in the database - * - * @var int - */ - protected $idArchive = null; - - /** - * name of requested report - * - * @var string - */ - protected $requestedReport = null; - - /** - * Flag set to true once the archive has been checked (when we make sure it is archived) - * - * @var bool - */ - protected $alreadyChecked = array(); - - protected function clearCache() - { - foreach($this->blobCached as $name => $blob) - { - $this->freeBlob($name); - } - $this->blobCached = array(); - } - - public function __destruct() - { - $this->clearCache(); - } - - /** - * Returns the blob cache. For testing. - * - * @return array - */ - public function getBlobCache() - { - return $this->blobCached; - } - - /** - * Returns the pretty date of this Archive, eg. 'Thursday 20th March 2008' - * - * @return string - */ - public function getPrettyDate() - { - return $this->period->getPrettyString(); - } - - /** - * Returns the idarchive of this Archive used to index this archive in the DB - * - * @throws Exception - * @return int - */ - public function getIdArchive() - { - if(is_null($this->idArchive)) - { - throw new Exception("idArchive is null"); - } - return $this->idArchive; - } - - /** - * Set the period - * - * @param Piwik_Period $period - */ - public function setPeriod( Piwik_Period $period ) - { - $this->period = $period; - } - - public function getPeriod() - { - return $this->period; - } - - /** - * Returns the timestamp of the first date in the period for this Archive. - * This is used to sort archives by date when working on a Archive_Array - * - * @return int Unix timestamp - */ - public function getTimestampStartDate() - { - if(!is_null($this->archiveProcessing)) - { - $timestamp = $this->archiveProcessing->getTimestampStartDate(); - if(!empty($timestamp)) - { - return $timestamp; - } - } - return $this->period->getDateStart()->getTimestamp(); - } - - /** - * Prepares the archive. Gets the idarchive from the ArchiveProcessing. - * - * This will possibly launch the archiving process if the archive was not available. - * @return bool - */ - public function prepareArchive() - { - $archiveJustProcessed = false; - - - $periodString = $this->period->getLabel(); - $plugin = Piwik_ArchiveProcessing::getPluginBeingProcessed($this->getRequestedReport()); - - $cacheKey = 'all'; - if($periodString == 'range') - { - $cacheKey = $plugin; - } - if(!isset($this->alreadyChecked[$cacheKey])) - { - $this->isThereSomeVisits = false; - $this->alreadyChecked[$cacheKey] = true; - $dayString = $this->period->getPrettyString(); - $logMessage = sprintf("%s (%s), plugin %s", $periodString, $dayString, $plugin); - // 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() ) ) - { - Piwik::log(sprintf("Archive %s skipped, archive is before the website was created.", $logMessage)); - return; - } - - // if the starting date is in the future we know there is no visit - if( $this->period->getDateStart()->subDay(2)->isLater( Piwik_Date::today() ) ) - { - Piwik::log(sprintf("Archive %s skipped, archive is after today.", $logMessage)); - return; - } - - // we make sure the archive is available for the given date - $periodLabel = $this->period->getLabel(); - $this->archiveProcessing = Piwik_ArchiveProcessing::factory($periodLabel); - $this->archiveProcessing->setSite($this->site); - $this->archiveProcessing->setPeriod($this->period); - $this->archiveProcessing->setSegment($this->segment); - - $this->archiveProcessing->init(); - - $this->archiveProcessing->setRequestedReport( $this->getRequestedReport() ); - - $archivingDisabledArchiveNotProcessed = false; - $idArchive = $this->archiveProcessing->loadArchive(); - if(empty($idArchive)) - { - if($this->archiveProcessing->isArchivingDisabled()) - { - $archivingDisabledArchiveNotProcessed = true; - $logMessage = sprintf("Archiving disabled, for %s", $logMessage); - } - else - { - Piwik::log(sprintf("Processing %s, not archived yet...", $logMessage)); - $archiveJustProcessed = true; - - // Process the reports - $this->archiveProcessing->launchArchiving(); - - $idArchive = $this->archiveProcessing->getIdArchive(); - $logMessage = sprintf("Processed %d, for %s", $idArchive, $logMessage); - } - } - else - { - $logMessage = sprintf("Already processed, fetching idArchive = %d (idSite=%d), for %s", $idArchive, $this->site->getId(), $logMessage); - } - Piwik::log(sprintf("%s, Visits = %d", $logMessage, $this->archiveProcessing->getNumberOfVisits())); - $this->isThereSomeVisits = !$archivingDisabledArchiveNotProcessed - && $this->archiveProcessing->isThereSomeVisits(); - $this->idArchive = $idArchive; - } - return $archiveJustProcessed; - } - - /** - * Returns a value from the current archive with the name = $name - * Method used by getNumeric or getBlob - * - * @param string $name - * @param string $typeValue numeric|blob - * @param string|bool $archivedDate Value to store date of archive info in. If false, not stored. - * @return mixed|bool false if no result - */ - protected function get( $name, $typeValue = 'numeric', &$archivedDate = false ) - { - $this->setRequestedReport($name); - $this->prepareArchive(); - - // values previously "get" and now cached - if($typeValue == 'numeric' - && $this->cacheEnabledForNumeric - && isset($this->numericCached[$name]) - ) - { - return $this->numericCached[$name]; - } - - // During archiving we prefetch the blobs recursively - // and we get them faster from memory after - if($typeValue == 'blob' - && isset($this->blobCached[$name])) - { - return $this->blobCached[$name]; - } - - if($name == 'idarchive') - { - return $this->idArchive; - } - - if(!$this->isThereSomeVisits) - { - return false; - } - - // select the table to use depending on the type of the data requested - switch($typeValue) - { - case 'blob': - $table = $this->archiveProcessing->getTableArchiveBlobName(); - break; - - case 'numeric': - default: - $table = $this->archiveProcessing->getTableArchiveNumericName(); - break; - } - - $db = Zend_Registry::get('db'); - $row = $db->fetchRow("SELECT value, ts_archived + /** + * The Piwik_ArchiveProcessing object used to check that the archive is available + * and launch the processing if the archive was not yet processed + * + * @var Piwik_ArchiveProcessing + */ + public $archiveProcessing = null; + + /** + * @var bool Set to true if the archive has at least 1 visit + */ + public $isThereSomeVisits = null; + + /** + * Period of this Archive + * + * @var Piwik_Period + */ + protected $period = null; + + /** + * Set to true will activate numeric value caching for this archive. + * + * @var bool + */ + protected $cacheEnabledForNumeric = true; + + /** + * Array of cached numeric values, used to make requests faster + * when requesting the same value again and again + * + * @var array of numeric + */ + protected $numericCached = array(); + + /** + * Array of cached blob, used to make requests faster when requesting the same blob again and again + * + * @var array of mixed + */ + protected $blobCached = array(); + + /** + * idarchive of this Archive in the database + * + * @var int + */ + protected $idArchive = null; + + /** + * name of requested report + * + * @var string + */ + protected $requestedReport = null; + + /** + * Flag set to true once the archive has been checked (when we make sure it is archived) + * + * @var bool + */ + protected $alreadyChecked = array(); + + protected function clearCache() + { + foreach ($this->blobCached as $name => $blob) { + $this->freeBlob($name); + } + $this->blobCached = array(); + } + + public function __destruct() + { + $this->clearCache(); + } + + /** + * Returns the blob cache. For testing. + * + * @return array + */ + public function getBlobCache() + { + return $this->blobCached; + } + + /** + * Returns the pretty date of this Archive, eg. 'Thursday 20th March 2008' + * + * @return string + */ + public function getPrettyDate() + { + return $this->period->getPrettyString(); + } + + /** + * Returns the idarchive of this Archive used to index this archive in the DB + * + * @throws Exception + * @return int + */ + public function getIdArchive() + { + if (is_null($this->idArchive)) { + throw new Exception("idArchive is null"); + } + return $this->idArchive; + } + + /** + * Set the period + * + * @param Piwik_Period $period + */ + public function setPeriod(Piwik_Period $period) + { + $this->period = $period; + } + + public function getPeriod() + { + return $this->period; + } + + /** + * Returns the timestamp of the first date in the period for this Archive. + * This is used to sort archives by date when working on a Archive_Array + * + * @return int Unix timestamp + */ + public function getTimestampStartDate() + { + if (!is_null($this->archiveProcessing)) { + $timestamp = $this->archiveProcessing->getTimestampStartDate(); + if (!empty($timestamp)) { + return $timestamp; + } + } + return $this->period->getDateStart()->getTimestamp(); + } + + /** + * Prepares the archive. Gets the idarchive from the ArchiveProcessing. + * + * This will possibly launch the archiving process if the archive was not available. + * @return bool + */ + public function prepareArchive() + { + $archiveJustProcessed = false; + + + $periodString = $this->period->getLabel(); + $plugin = Piwik_ArchiveProcessing::getPluginBeingProcessed($this->getRequestedReport()); + + $cacheKey = 'all'; + if ($periodString == 'range') { + $cacheKey = $plugin; + } + if (!isset($this->alreadyChecked[$cacheKey])) { + $this->isThereSomeVisits = false; + $this->alreadyChecked[$cacheKey] = true; + $dayString = $this->period->getPrettyString(); + $logMessage = sprintf("%s (%s), plugin %s", $periodString, $dayString, $plugin); + // 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())) { + Piwik::log(sprintf("Archive %s skipped, archive is before the website was created.", $logMessage)); + return; + } + + // if the starting date is in the future we know there is no visit + if ($this->period->getDateStart()->subDay(2)->isLater(Piwik_Date::today())) { + Piwik::log(sprintf("Archive %s skipped, archive is after today.", $logMessage)); + return; + } + + // we make sure the archive is available for the given date + $periodLabel = $this->period->getLabel(); + $this->archiveProcessing = Piwik_ArchiveProcessing::factory($periodLabel); + $this->archiveProcessing->setSite($this->site); + $this->archiveProcessing->setPeriod($this->period); + $this->archiveProcessing->setSegment($this->segment); + + $this->archiveProcessing->init(); + + $this->archiveProcessing->setRequestedReport($this->getRequestedReport()); + + $archivingDisabledArchiveNotProcessed = false; + $idArchive = $this->archiveProcessing->loadArchive(); + if (empty($idArchive)) { + if ($this->archiveProcessing->isArchivingDisabled()) { + $archivingDisabledArchiveNotProcessed = true; + $logMessage = sprintf("Archiving disabled, for %s", $logMessage); + } else { + Piwik::log(sprintf("Processing %s, not archived yet...", $logMessage)); + $archiveJustProcessed = true; + + // Process the reports + $this->archiveProcessing->launchArchiving(); + + $idArchive = $this->archiveProcessing->getIdArchive(); + $logMessage = sprintf("Processed %d, for %s", $idArchive, $logMessage); + } + } else { + $logMessage = sprintf("Already processed, fetching idArchive = %d (idSite=%d), for %s", $idArchive, $this->site->getId(), $logMessage); + } + Piwik::log(sprintf("%s, Visits = %d", $logMessage, $this->archiveProcessing->getNumberOfVisits())); + $this->isThereSomeVisits = !$archivingDisabledArchiveNotProcessed + && $this->archiveProcessing->isThereSomeVisits(); + $this->idArchive = $idArchive; + } + return $archiveJustProcessed; + } + + /** + * Returns a value from the current archive with the name = $name + * Method used by getNumeric or getBlob + * + * @param string $name + * @param string $typeValue numeric|blob + * @param string|bool $archivedDate Value to store date of archive info in. If false, not stored. + * @return mixed|bool false if no result + */ + protected function get($name, $typeValue = 'numeric', &$archivedDate = false) + { + $this->setRequestedReport($name); + $this->prepareArchive(); + + // values previously "get" and now cached + if ($typeValue == 'numeric' + && $this->cacheEnabledForNumeric + && isset($this->numericCached[$name]) + ) { + return $this->numericCached[$name]; + } + + // During archiving we prefetch the blobs recursively + // and we get them faster from memory after + if ($typeValue == 'blob' + && isset($this->blobCached[$name]) + ) { + return $this->blobCached[$name]; + } + + if ($name == 'idarchive') { + return $this->idArchive; + } + + if (!$this->isThereSomeVisits) { + return false; + } + + // select the table to use depending on the type of the data requested + switch ($typeValue) { + case 'blob': + $table = $this->archiveProcessing->getTableArchiveBlobName(); + break; + + case 'numeric': + default: + $table = $this->archiveProcessing->getTableArchiveNumericName(); + break; + } + + $db = Zend_Registry::get('db'); + $row = $db->fetchRow("SELECT value, ts_archived FROM $table WHERE idarchive = ? AND name = ?", - array( $this->idArchive , $name) - ); - - $value = $tsArchived = false; - if (is_array($row)) - { - $value = $row['value']; - $tsArchived = $row['ts_archived']; - } - - if ($archivedDate !== false) - { - $archivedDate = $tsArchived; - } - - if($value === false) - { - if($typeValue == 'numeric' - && $this->cacheEnabledForNumeric) - { - $this->numericCached[$name] = false; - } - return $value; - } - - // uncompress when selecting from the BLOB table - if($typeValue == 'blob' && $db->hasBlobDataType()) - { - $value = $this->uncompress($value); - } - - if($typeValue == 'numeric' - && $this->cacheEnabledForNumeric) - { - $this->numericCached[$name] = $value; - } - return $value; - } - - - /** - * This method loads in memory all the subtables for the main table called $name. - * You have to give it the parent table $dataTableToLoad so we can lookup the sub tables ids to load. - * - * If $addMetadataSubtableId set to true, it will add for each row a 'metadata' called 'databaseSubtableId' - * containing the child ID of the subtable associated to this row. - * - * @param string $name - * @param Piwik_DataTable $dataTableToLoad - * @param bool $addMetadataSubtableId - */ - public function loadSubDataTables($name, Piwik_DataTable $dataTableToLoad, $addMetadataSubtableId = false) - { - // we have to recursively load all the subtables associated to this table's rows - // and update the subtableID so that it matches the newly instanciated table - foreach($dataTableToLoad->getRows() as $row) - { - $subTableID = $row->getIdSubDataTable(); - - if($subTableID !== null) - { - $subDataTableLoaded = $this->getDataTable($name, $subTableID); - - $row->setSubtable( $subDataTableLoaded ); - - $this->loadSubDataTables($name, $subDataTableLoaded, $addMetadataSubtableId); - - // we edit the subtable ID so that it matches the newly table created in memory - // NB: we dont overwrite the datatableid in the case we are displaying the table expanded. - if($addMetadataSubtableId) - { - // this will be written back to the column 'idsubdatatable' just before rendering, see Renderer/Php.php - $row->addMetadata('idsubdatatable_in_db', $row->getIdSubDataTable()); - } - } - } - } - - - /** - * Free the blob cache memory array - * @param $name - */ - public function freeBlob( $name ) - { - unset($this->blobCached[$name]); - $this->blobCached[$name] = null; - } - - protected function uncompress($data) - { - return @gzuncompress($data); - } - - /** - * Fetches all blob fields name_* at once for the current archive for performance reasons. - * - * @param $name - * @return - */ - public function preFetchBlob( $name ) - { - $this->setRequestedReport($name); - $this->prepareArchive(); - if(!$this->isThereSomeVisits) { return; } - - $tableBlob = $this->archiveProcessing->getTableArchiveBlobName(); - - $db = Zend_Registry::get('db'); - $hasBlobs = $db->hasBlobDataType(); - - // select blobs w/ name like "$name_[0-9]+" w/o using RLIKE - $nameEnd = strlen($name) + 2; - $query = $db->query("SELECT value, name + array($this->idArchive, $name) + ); + + $value = $tsArchived = false; + if (is_array($row)) { + $value = $row['value']; + $tsArchived = $row['ts_archived']; + } + + if ($archivedDate !== false) { + $archivedDate = $tsArchived; + } + + if ($value === false) { + if ($typeValue == 'numeric' + && $this->cacheEnabledForNumeric + ) { + $this->numericCached[$name] = false; + } + return $value; + } + + // uncompress when selecting from the BLOB table + if ($typeValue == 'blob' && $db->hasBlobDataType()) { + $value = $this->uncompress($value); + } + + if ($typeValue == 'numeric' + && $this->cacheEnabledForNumeric + ) { + $this->numericCached[$name] = $value; + } + return $value; + } + + + /** + * This method loads in memory all the subtables for the main table called $name. + * You have to give it the parent table $dataTableToLoad so we can lookup the sub tables ids to load. + * + * If $addMetadataSubtableId set to true, it will add for each row a 'metadata' called 'databaseSubtableId' + * containing the child ID of the subtable associated to this row. + * + * @param string $name + * @param Piwik_DataTable $dataTableToLoad + * @param bool $addMetadataSubtableId + */ + public function loadSubDataTables($name, Piwik_DataTable $dataTableToLoad, $addMetadataSubtableId = false) + { + // we have to recursively load all the subtables associated to this table's rows + // and update the subtableID so that it matches the newly instanciated table + foreach ($dataTableToLoad->getRows() as $row) { + $subTableID = $row->getIdSubDataTable(); + + if ($subTableID !== null) { + $subDataTableLoaded = $this->getDataTable($name, $subTableID); + + $row->setSubtable($subDataTableLoaded); + + $this->loadSubDataTables($name, $subDataTableLoaded, $addMetadataSubtableId); + + // we edit the subtable ID so that it matches the newly table created in memory + // NB: we dont overwrite the datatableid in the case we are displaying the table expanded. + if ($addMetadataSubtableId) { + // this will be written back to the column 'idsubdatatable' just before rendering, see Renderer/Php.php + $row->addMetadata('idsubdatatable_in_db', $row->getIdSubDataTable()); + } + } + } + } + + + /** + * Free the blob cache memory array + * @param $name + */ + public function freeBlob($name) + { + unset($this->blobCached[$name]); + $this->blobCached[$name] = null; + } + + protected function uncompress($data) + { + return @gzuncompress($data); + } + + /** + * Fetches all blob fields name_* at once for the current archive for performance reasons. + * + * @param $name + * @return + */ + public function preFetchBlob($name) + { + $this->setRequestedReport($name); + $this->prepareArchive(); + if (!$this->isThereSomeVisits) { + return; + } + + $tableBlob = $this->archiveProcessing->getTableArchiveBlobName(); + + $db = Zend_Registry::get('db'); + $hasBlobs = $db->hasBlobDataType(); + + // select blobs w/ name like "$name_[0-9]+" w/o using RLIKE + $nameEnd = strlen($name) + 2; + $query = $db->query("SELECT value, name FROM $tableBlob WHERE idarchive = ? AND (name = ? OR (name LIKE ? AND SUBSTRING(name, $nameEnd, 1) >= '0' AND SUBSTRING(name, $nameEnd, 1) <= '9') )", - array( $this->idArchive, $name, $name.'%' ) - ); - - while($row = $query->fetch()) - { - $value = $row['value']; - $name = $row['name']; - - if($hasBlobs) - { - $this->blobCached[$name] = $this->uncompress($value); - if($this->blobCached[$name] === false) - { - //throw new Exception("Error gzuncompress $name "); - } - } - else - { - $this->blobCached[$name] = $value; - } - } - } - - /** - * Returns a numeric value from this Archive, with the name '$name' - * - * @param string $name - * @return int|float - */ - public function getNumeric( $name ) - { - return $this->formatNumericValue( $this->get($name, 'numeric') ); - } - - - /** - * Returns a blob value from this Archive, with the name '$name' - * Blob values are all values except int and float. - * - * @param string $name - * @return mixed - */ - public function getBlob( $name ) - { - return $this->get($name, 'blob'); - } - - /** - * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Simple - * containing one row per field name. - * - * For example $fields = array( 'max_actions', - * 'nb_uniq_visitors', - * 'nb_visits', - * 'nb_actions', - * 'sum_visit_length', - * 'bounce_count', - * 'nb_visits_converted' - * ); - * - * @param string|array $fields Name or array of names of Archive fields - * - * @return Piwik_DataTable_Simple - */ - public function getDataTableFromNumeric( $fields ) - { - if(!is_array($fields)) - { - $fields = array($fields); - } - - $values = array(); - foreach($fields as $field) - { - $values[$field] = $this->getNumeric($field); - } - - $table = new Piwik_DataTable_Simple(); - $table->addRowsFromArray($values); - return $table; - } - - /** - * Returns a DataTable that has the name '$name' from the current Archive. - * If $idSubTable is specified, returns the subDataTable called '$name_$idSubTable' - * - * @param string $name - * @param int $idSubTable optional id SubDataTable - * @return Piwik_DataTable - */ - public function getDataTable( $name, $idSubTable = null ) - { - if(!is_null($idSubTable)) - { - $name .= sprintf("_%s", $idSubTable); - } - - $this->setRequestedReport($name); - - $data = $this->get($name, 'blob', $tsArchived); - - $table = $this->makeDataTable(); - - if($data !== false) - { - $table->addRowsFromSerializedArray($data); - $table->setMetadata(Piwik_DataTable::ARCHIVED_DATE_METADATA_NAME, $tsArchived); - } - if($data === false - && $idSubTable !== null) - { - // This is not expected, but somehow happens in some unknown cases and very rarely. - // Do not throw error in this case - //throw new Exception("not expected"); - return new Piwik_DataTable(); - } - - return $table; - } - - /** - * Creates a new DataTable with some metadata set. Sets the following metadata: - * - 'site' => Piwik_Site instance for this archive - * - 'period' => Piwik_Period instance for this archive - * - 'timestamp' => the timestamp of the first date in this archive - * - * @param bool $isSimple Whether the DataTable should be a DataTable_Simple - * instance or not. - * @return Piwik_DataTable - */ - public function makeDataTable( $isSimple = false ) - { - if ($isSimple) - { - $result = new Piwik_DataTable_Simple(); - } - else - { - $result = new Piwik_DataTable(); - } - - $result->setMetadata('site', $this->getSite()); - $result->setMetadata('period', $this->getPeriod()); - $result->setMetadata('timestamp', $this->getTimestampStartDate()); - - return $result; - } - - public function setRequestedReport($requestedReport ) - { - $this->requestedReport = $requestedReport; - } - - /** - * Returns the report (the named collection of metrics) this Archive instance is - * currently going to query/process. - * - * @return string - */ - protected function getRequestedReport() - { - return self::getRequestedReportFor($this->requestedReport); - } - - /** - * Returns the name of the report (the named collection of metrics) that contains the - * specified metric. - * - * @param string $metric The metric whose report is being requested. If this does - * not belong to a known report, its assumed to be the report - * itself. - * @return string - */ - public static function getRequestedReportFor($metric) - { - // Core metrics are always processed in Core, for the requested date/period/segment - if(in_array($metric, Piwik_ArchiveProcessing::getCoreMetrics()) - || $metric == 'max_actions') - { - return 'VisitsSummary_CoreMetrics'; - } - // VisitFrequency metrics don't follow the same naming convention (HACK) - if(strpos($metric, '_returning') > 0 - // ignore Goal_visitor_returning_1_1_nb_conversions - && strpos($metric, 'Goal_') === false) - { - return 'VisitFrequency_Metrics'; - } - // Goal_* metrics are processed by the Goals plugin (HACK) - if(strpos($metric, 'Goal_') === 0) - { - return 'Goals_Metrics'; - } - // Actions metrics are processed by the Actions plugin (HACK) (3RD HACK IN FACT) (YES, THIS IS TOO MUCH HACKING) - // (FIXME PLEASE). - if (in_array($metric, Piwik_Archive::$actionsMetrics)) - { - return 'Actions_Metrics'; - } - return $metric; - } - - /** - * Returns a DataTable that has the name '$name' from the current Archive. - * Also loads in memory all subDataTable for this DataTable. - * - * For example, if $name = 'Referers_keywordBySearchEngine' it will load all DataTable - * named 'Referers_keywordBySearchEngine_*' and they will be set as subDataTable to the - * rows. You can then go through the rows - * $rows = DataTable->getRows(); - * and for each row request the subDataTable (in this case the DataTable of the keywords for each search engines) - * $idSubTable = $row->getIdSubDataTable(); - * $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); - * - * @param string $name - * @param int $idSubTable Optional subDataTable to load instead of loading the parent DataTable - * @return Piwik_DataTable - */ - public function getDataTableExpanded($name, $idSubTable = null) - { - $this->preFetchBlob($name); - $dataTableToLoad = $this->getDataTable($name, $idSubTable); - $this->loadSubDataTables($name, $dataTableToLoad, $addMetadataSubtableId = true); - $dataTableToLoad->enableRecursiveFilters(); - $this->freeBlob($name); - return $dataTableToLoad; - } - - /** - * Returns true if Piwik can launch the archiving process for this archive, - * false if otherwise. - * - * @return bool - */ - public function isArchivingDisabled() - { - return Piwik_ArchiveProcessing::isArchivingDisabledFor($this->segment, $this->period); - } + array($this->idArchive, $name, $name . '%') + ); + + while ($row = $query->fetch()) { + $value = $row['value']; + $name = $row['name']; + + if ($hasBlobs) { + $this->blobCached[$name] = $this->uncompress($value); + if ($this->blobCached[$name] === false) { + //throw new Exception("Error gzuncompress $name "); + } + } else { + $this->blobCached[$name] = $value; + } + } + } + + /** + * Returns a numeric value from this Archive, with the name '$name' + * + * @param string $name + * @return int|float + */ + public function getNumeric($name) + { + return $this->formatNumericValue($this->get($name, 'numeric')); + } + + + /** + * Returns a blob value from this Archive, with the name '$name' + * Blob values are all values except int and float. + * + * @param string $name + * @return mixed + */ + public function getBlob($name) + { + return $this->get($name, 'blob'); + } + + /** + * Given a list of fields defining numeric values, it will return a Piwik_DataTable_Simple + * containing one row per field name. + * + * For example $fields = array( 'max_actions', + * 'nb_uniq_visitors', + * 'nb_visits', + * 'nb_actions', + * 'sum_visit_length', + * 'bounce_count', + * 'nb_visits_converted' + * ); + * + * @param string|array $fields Name or array of names of Archive fields + * + * @return Piwik_DataTable_Simple + */ + public function getDataTableFromNumeric($fields) + { + if (!is_array($fields)) { + $fields = array($fields); + } + + $values = array(); + foreach ($fields as $field) { + $values[$field] = $this->getNumeric($field); + } + + $table = new Piwik_DataTable_Simple(); + $table->addRowsFromArray($values); + return $table; + } + + /** + * Returns a DataTable that has the name '$name' from the current Archive. + * If $idSubTable is specified, returns the subDataTable called '$name_$idSubTable' + * + * @param string $name + * @param int $idSubTable optional id SubDataTable + * @return Piwik_DataTable + */ + public function getDataTable($name, $idSubTable = null) + { + if (!is_null($idSubTable)) { + $name .= sprintf("_%s", $idSubTable); + } + + $this->setRequestedReport($name); + + $data = $this->get($name, 'blob', $tsArchived); + + $table = $this->makeDataTable(); + + if ($data !== false) { + $table->addRowsFromSerializedArray($data); + $table->setMetadata(Piwik_DataTable::ARCHIVED_DATE_METADATA_NAME, $tsArchived); + } + if ($data === false + && $idSubTable !== null + ) { + // This is not expected, but somehow happens in some unknown cases and very rarely. + // Do not throw error in this case + //throw new Exception("not expected"); + return new Piwik_DataTable(); + } + + return $table; + } + + /** + * Creates a new DataTable with some metadata set. Sets the following metadata: + * - 'site' => Piwik_Site instance for this archive + * - 'period' => Piwik_Period instance for this archive + * - 'timestamp' => the timestamp of the first date in this archive + * + * @param bool $isSimple Whether the DataTable should be a DataTable_Simple + * instance or not. + * @return Piwik_DataTable + */ + public function makeDataTable($isSimple = false) + { + if ($isSimple) { + $result = new Piwik_DataTable_Simple(); + } else { + $result = new Piwik_DataTable(); + } + + $result->setMetadata('site', $this->getSite()); + $result->setMetadata('period', $this->getPeriod()); + $result->setMetadata('timestamp', $this->getTimestampStartDate()); + + return $result; + } + + public function setRequestedReport($requestedReport) + { + $this->requestedReport = $requestedReport; + } + + /** + * Returns the report (the named collection of metrics) this Archive instance is + * currently going to query/process. + * + * @return string + */ + protected function getRequestedReport() + { + return self::getRequestedReportFor($this->requestedReport); + } + + /** + * Returns the name of the report (the named collection of metrics) that contains the + * specified metric. + * + * @param string $metric The metric whose report is being requested. If this does + * not belong to a known report, its assumed to be the report + * itself. + * @return string + */ + public static function getRequestedReportFor($metric) + { + // Core metrics are always processed in Core, for the requested date/period/segment + if (in_array($metric, Piwik_ArchiveProcessing::getCoreMetrics()) + || $metric == 'max_actions' + ) { + return 'VisitsSummary_CoreMetrics'; + } + // VisitFrequency metrics don't follow the same naming convention (HACK) + if (strpos($metric, '_returning') > 0 + // ignore Goal_visitor_returning_1_1_nb_conversions + && strpos($metric, 'Goal_') === false + ) { + return 'VisitFrequency_Metrics'; + } + // Goal_* metrics are processed by the Goals plugin (HACK) + if (strpos($metric, 'Goal_') === 0) { + return 'Goals_Metrics'; + } + // Actions metrics are processed by the Actions plugin (HACK) (3RD HACK IN FACT) (YES, THIS IS TOO MUCH HACKING) + // (FIXME PLEASE). + if (in_array($metric, Piwik_Archive::$actionsMetrics)) { + return 'Actions_Metrics'; + } + return $metric; + } + + /** + * Returns a DataTable that has the name '$name' from the current Archive. + * Also loads in memory all subDataTable for this DataTable. + * + * For example, if $name = 'Referers_keywordBySearchEngine' it will load all DataTable + * named 'Referers_keywordBySearchEngine_*' and they will be set as subDataTable to the + * rows. You can then go through the rows + * $rows = DataTable->getRows(); + * and for each row request the subDataTable (in this case the DataTable of the keywords for each search engines) + * $idSubTable = $row->getIdSubDataTable(); + * $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + * + * @param string $name + * @param int $idSubTable Optional subDataTable to load instead of loading the parent DataTable + * @return Piwik_DataTable + */ + public function getDataTableExpanded($name, $idSubTable = null) + { + $this->preFetchBlob($name); + $dataTableToLoad = $this->getDataTable($name, $idSubTable); + $this->loadSubDataTables($name, $dataTableToLoad, $addMetadataSubtableId = true); + $dataTableToLoad->enableRecursiveFilters(); + $this->freeBlob($name); + return $dataTableToLoad; + } + + /** + * Returns true if Piwik can launch the archiving process for this archive, + * false if otherwise. + * + * @return bool + */ + public function isArchivingDisabled() + { + return Piwik_ArchiveProcessing::isArchivingDisabledFor($this->segment, $this->period); + } } diff --git a/core/ArchiveProcessing.php b/core/ArchiveProcessing.php index 17d2a32246..7ffa394ce9 100644 --- a/core/ArchiveProcessing.php +++ b/core/ArchiveProcessing.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,561 +12,545 @@ /** * The ArchiveProcessing module is a module that reads the Piwik logs from the DB and * compute all the reports, which are then stored in the database. - * + * * The ArchiveProcessing class is used by the Archive object to make sure the given Archive is processed and available in the DB. - * + * * A record in the Database for a given report is defined by - * - idarchive = unique ID that is associated to all the data of this archive (idsite+period+date) - * - idsite = the ID of the website - * - date1 = starting day of the period - * - date2 = ending day of the period - * - period = integer that defines the period (day/week/etc.). @see period::getId() + * - idarchive = unique ID that is associated to all the data of this archive (idsite+period+date) + * - idsite = the ID of the website + * - date1 = starting day of the period + * - date2 = ending day of the period + * - period = integer that defines the period (day/week/etc.). @see period::getId() * - ts_archived = timestamp when the archive was processed (UTC) - * - name = the name of the report (ex: uniq_visitors or search_keywords_by_search_engines) - * - value = the actual data - * + * - name = the name of the report (ex: uniq_visitors or search_keywords_by_search_engines) + * - value = the actual data + * * @package Piwik * @subpackage Piwik_ArchiveProcessing */ abstract class Piwik_ArchiveProcessing { - /** - * Flag stored at the end of the archiving - * - * @var int - */ - const DONE_OK = 1; - - /** - * Flag stored at the start of the archiving - * When requesting an Archive, we make sure that non-finished archive are not considered valid - * - * @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; - - /** - * A row is created to lock an idarchive for the current archive being processed - * @var string - */ - const PREFIX_SQL_LOCK = "locked_"; - - /** - * Idarchive in the DB for the requested archive - * - * @var int - */ - protected $idArchive; - - /** - * Period id @see Piwik_Period::getId() - * - * @var int - */ - protected $periodId; - - /** - * Timestamp for the first date of the period - * - * @var int unix timestamp - */ - protected $timestampDateStart = null; - - /** - * Starting date of the archive - * - * @var Piwik_Date - */ - protected $dateStart; - - /** - * Ending date of the archive - * - * @var Piwik_Date - */ - protected $dateEnd; - - /** - * Object used to generate (depending on the $dateStart) the name of the DB table to use to store numeric values - * - * @var Piwik_TablePartitioning - */ - protected $tableArchiveNumeric; - - /** - * Object used to generate (depending on the $dateStart) the name of the DB table to use to store numeric values - * - * @var Piwik_TablePartitioning - */ - protected $tableArchiveBlob; - - /** - * Minimum timestamp looked at for processed archives - * - * @var int - */ - protected $minDatetimeArchiveProcessedUTC = false; - - /** - * Compress blobs - * - * @var bool - */ - 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) - * - * @var int - */ - public $idsite = null; - - /** - * Period of the current archive - * Can be accessed by plugins (that is why it's public) - * - * @var Piwik_Period - */ - public $period = null; - - /** - * Site of the current archive - * Can be accessed by plugins (that is why it's public) - * - * @var Piwik_Site - */ - public $site = null; - - /** - * @var Piwik_Segment - */ - protected $segment = null; - - /** - * Current time. - * This value is cached. - * - * @var int - */ - public $time = null; - - /** - * Starting datetime in UTC - * - * @var string - */ - public $startDatetimeUTC; - - /** - * Ending date in UTC - * - * @var string - */ - public $strDateEnd; - - /** - * Name of the DB table _log_visit - * - * @var string - */ - public $logTable; - - /** - * When set to true, we always archive, even if the archive is already available. - * You can change this settings automatically in the config/global.ini.php always_archive_data under the [Debug] section - * - * @var bool - */ - public $debugAlwaysArchive = false; - - /** - * If the archive has at least 1 visit, this is set to true. - * - * @var bool - */ - public $isThereSomeVisits = null; - - protected $startTimestampUTC; - protected $endTimestampUTC; - - /** - * Flag that will forcefully disable the archiving process. Only set by the tests. - */ - public static $forceDisableArchiving = false; - - /** - * Constructor - */ - public function __construct() - { - $this->time = time(); - } - - /** - * Returns the Piwik_ArchiveProcessing_Day or Piwik_ArchiveProcessing_Period object - * depending on $name period string - * - * @param string $name day|week|month|year - * @throws Exception - * @return Piwik_ArchiveProcessing Piwik_ArchiveProcessing_Day|Piwik_ArchiveProcessing_Period - */ - static function factory($name) - { - switch($name) - { - case 'day': - $process = new Piwik_ArchiveProcessing_Day(); - $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_day']; - break; - - case 'week': - case 'month': - case 'year': - $process = new Piwik_ArchiveProcessing_Period(); - $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_period']; - break; - - case 'range': - $process = new Piwik_ArchiveProcessing_Period(); - $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_range']; - break; - - default: - throw new Exception("Unknown Archiving period specified '$name'"); - break; - } - return $process; - } - - const OPTION_TODAY_ARCHIVE_TTL = 'todayArchiveTimeToLive'; - const OPTION_BROWSER_TRIGGER_ARCHIVING = 'enableBrowserTriggerArchiving'; - - static public function getCoreMetrics() - { - return array( - 'nb_uniq_visitors', - 'nb_visits', - 'nb_actions', - 'sum_visit_length', - 'bounce_count', - 'nb_visits_converted', - ); - } - - 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 Piwik_Config::getInstance()->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); - Piwik_Tracker_Cache::clearCacheGeneral(); - } - - static public function isBrowserTriggerArchivingEnabled() - { - $browserArchivingEnabled = Piwik_GetOption(self::OPTION_BROWSER_TRIGGER_ARCHIVING); - if($browserArchivingEnabled !== false) - { - return (bool)$browserArchivingEnabled; - } - return (bool)Piwik_Config::getInstance()->General['enable_browser_archiving_triggering']; - } - - public function getIdArchive() - { - return $this->idArchive; - } - - /** - * Sets object attributes that will be used throughout the process - */ - public function init() - { - $this->idsite = $this->site->getId(); - $this->periodId = $this->period->getId(); - - $this->initDates(); - - $this->tableArchiveNumeric = self::makeNumericArchiveTable($this->period); - $this->tableArchiveBlob = self::makeBlobArchiveTable($this->period); - - $this->minDatetimeArchiveProcessedUTC = $this->getMinTimeArchivedProcessed(); - $db = Zend_Registry::get('db'); - $this->compressBlob = $db->hasBlobDataType(); - } - - /** - * The archive processing classes have features that might be useful for live querying; - * In particular, Piwik_ArchiveProcessing_Day::query*. In order to reuse those methods - * outside the actual archiving or to reuse archiving code for live querying, an instance - * of archive processing has to be faked. - * - * For example, this code can be used in an API method: - * $archiveProcessing = new Piwik_ArchiveProcessing_Day(); - * $archiveProcessing->setSite(new Piwik_Site($idSite)); - * $archiveProcessing->setPeriod(Piwik_Period::advancedFactory($period, $date)); - * $archiveProcessing->setSegment(new Piwik_Segment($segment, $idSite)); - * $archiveProcessing->initForLiveUsage(); - * Then, either use $archiveProcessing->query* or pass the instance to the archiving - * code of the plugin. Note that even though we use Piwik_ArchiveProcessing_Day, this - * works for any $period and $date that has been passed to the API. - */ - public function initForLiveUsage() { - $this->idsite = $this->site->getId(); - $this->initDates(); - } - - private function initDates() { - $dateStartLocalTimezone = $this->period->getDateStart(); - $dateEndLocalTimezone = $this->period->getDateEnd(); - - $dateStartUTC = $dateStartLocalTimezone->setTimezone($this->site->getTimezone()); - $dateEndUTC = $dateEndLocalTimezone->setTimezone($this->site->getTimezone()); - $this->startDatetimeUTC = $dateStartUTC->getDateStartUTC(); - $this->endDatetimeUTC = $dateEndUTC->getDateEndUTC(); - $this->startTimestampUTC = $dateStartUTC->getTimestamp(); - $this->endTimestampUTC = strtotime($this->endDatetimeUTC); - } - - /** - * Utility function which creates a TablePartitioning instance for the numeric - * archive data of a given period. - * - * @param Piwik_Period $period The time period of the archive data. - * @return Piwik_TablePartitioning_Monthly - */ - public static function makeNumericArchiveTable($period) - { - $result = new Piwik_TablePartitioning_Monthly('archive_numeric'); - $result->setTimestamp($period->getDateStart()->getTimestamp()); - return $result; - } - - /** - * Utility function which creates a TablePartitioning instance for the blob - * archive data of a given period. - * - * @param Piwik_Period $period The time period of the archive data. - * @return Piwik_TablePartitioning_Monthly - */ - public static function makeBlobArchiveTable($period) - { - $result = new Piwik_TablePartitioning_Monthly('archive_blob'); - $result->setTimestamp($period->getDateStart()->getTimestamp()); - return $result; - } - - 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 minDatetimeArchiveProcessedUTC that defines the lifetime value of today's archive - if( $this->period->getNumberOfSubperiods() == 0 - && ($this->startTimestampUTC <= $this->time && $this->endTimestampUTC > $this->time) - ) - { - $this->temporaryArchive = true; - $minDatetimeArchiveProcessedUTC = $this->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; - } - } - // - if the period we are looking for is finished, we look for a ts_archived that - // is greater than the last day of the archive - elseif($this->endTimestampUTC <= $this->time) - { - $minDatetimeArchiveProcessedUTC = $this->endTimestampUTC; - } - // - if the period we're looking for is not finished, we look for a recent enough archive - else - { - $this->temporaryArchive = true; - - // We choose to only look at archives that are newer than the specified timeout - $minDatetimeArchiveProcessedUTC = $this->time - self::getTodayArchiveTimeToLive(); - - // However, if archiving is disabled for this request, we shall - // accept any archive that was processed today after 00:00:01 this morning - if($this->isArchivingDisabled()) - { - $timezone = $this->site->getTimezone(); - $minDatetimeArchiveProcessedUTC = Piwik_Date::factory(Piwik_Date::factory('now', $timezone)->getDateStartUTC())->setTimezone($timezone)->getTimestamp(); - } - } - return $minDatetimeArchiveProcessedUTC; - } - - /** - * This method returns the idArchive ; if necessary, it triggers the archiving process. - * - * If the archive was not processed yet, it will launch the archiving process. - * If the current archive needs sub-archives (eg. a month archive needs all the days archive) - * it will recursively launch the archiving (using this loadArchive() on the sub-periods) - * - * @return int|false The idarchive of the archive, false if the archive is not archived yet - */ - public function loadArchive() - { - $this->init(); - if($this->debugAlwaysArchive) - { - return false; - } - $this->idArchive = $this->isArchived(); - - if($this->idArchive === false) - { - return false; - } - return $this->idArchive; - } - - /** - * @see loadArchive() - */ - public function launchArchiving() - { - if (!Piwik::getArchiveProcessingLock($this->idsite, $this->period, $this->segment)) - { - Piwik::log('Unable to get lock for idSite = ' . $this->idsite - . ', period = ' . $this->period->getLabel() - . ', UTC datetime [' . $this->startDatetimeUTC . ' -> ' . $this->endDatetimeUTC . ' ]...'); - return; - } - - $this->initCompute(); - $this->compute(); - $this->postCompute(); - // we execute again the isArchived that does some initialization work - $this->idArchive = $this->isArchived(); - Piwik::releaseArchiveProcessingLock($this->idsite, $this->period, $this->segment); - } - - /** - * This methods reads the subperiods if necessary, - * and computes the archive of the current period. - */ - abstract protected function compute(); - - abstract public function isThereSomeVisits(); - - /** - * Returns the name of the archive field used to tell the status of an archive, (ie, - * whether the archive was created successfully or not). - * - * @param bool $flagArchiveAsAllPlugins - * @return string - */ - public function getDoneStringFlag($flagArchiveAsAllPlugins = false) - { - return self::getDoneStringFlagFor( - $this->getSegment(), $this->period, $this->getRequestedReport(), $flagArchiveAsAllPlugins); - } - - /** - * Returns the name of the archive field used to tell the status of an archive, (ie, - * whether the archive was created successfully or not). - * - * @param Piwik_Segment $segment - * @param Piwik_Period $period - * @param string $requestedReport - * @param bool $flagArchiveAsAllPlugins - * @return string - */ - public static function getDoneStringFlagFor($segment, $period, $requestedReport, $flagArchiveAsAllPlugins = false) - { - $segmentHash = $segment->getHash(); - if(!self::shouldProcessReportsAllPluginsFor($segment, $period)) - { - $pluginProcessed = self::getPluginBeingProcessed($requestedReport); - if(!Piwik_PluginsManager::getInstance()->isPluginLoaded($pluginProcessed) - || $flagArchiveAsAllPlugins - ) - { - $pluginProcessed = 'all'; - } - $segmentHash .= '.'.$pluginProcessed; - } - return 'done' . $segmentHash; - } - - /** - * Init the object before launching the real archive processing - */ - protected function initCompute() - { - $this->loadNextIdarchive(); - $done = $this->getDoneStringFlag(); - $this->insertNumericRecord($done, Piwik_ArchiveProcessing::DONE_ERROR); - - // Can be removed when GeoIp is in core - $this->logTable = Piwik_Common::prefixTable('log_visit'); - - $temporary = 'definitive archive'; - if($this->isArchiveTemporary()) - { - $temporary = 'temporary archive'; - } + /** + * Flag stored at the end of the archiving + * + * @var int + */ + const DONE_OK = 1; + + /** + * Flag stored at the start of the archiving + * When requesting an Archive, we make sure that non-finished archive are not considered valid + * + * @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; + + /** + * A row is created to lock an idarchive for the current archive being processed + * @var string + */ + const PREFIX_SQL_LOCK = "locked_"; + + /** + * Idarchive in the DB for the requested archive + * + * @var int + */ + protected $idArchive; + + /** + * Period id @see Piwik_Period::getId() + * + * @var int + */ + protected $periodId; + + /** + * Timestamp for the first date of the period + * + * @var int unix timestamp + */ + protected $timestampDateStart = null; + + /** + * Starting date of the archive + * + * @var Piwik_Date + */ + protected $dateStart; + + /** + * Ending date of the archive + * + * @var Piwik_Date + */ + protected $dateEnd; + + /** + * Object used to generate (depending on the $dateStart) the name of the DB table to use to store numeric values + * + * @var Piwik_TablePartitioning + */ + protected $tableArchiveNumeric; + + /** + * Object used to generate (depending on the $dateStart) the name of the DB table to use to store numeric values + * + * @var Piwik_TablePartitioning + */ + protected $tableArchiveBlob; + + /** + * Minimum timestamp looked at for processed archives + * + * @var int + */ + protected $minDatetimeArchiveProcessedUTC = false; + + /** + * Compress blobs + * + * @var bool + */ + 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) + * + * @var int + */ + public $idsite = null; + + /** + * Period of the current archive + * Can be accessed by plugins (that is why it's public) + * + * @var Piwik_Period + */ + public $period = null; + + /** + * Site of the current archive + * Can be accessed by plugins (that is why it's public) + * + * @var Piwik_Site + */ + public $site = null; + + /** + * @var Piwik_Segment + */ + protected $segment = null; + + /** + * Current time. + * This value is cached. + * + * @var int + */ + public $time = null; + + /** + * Starting datetime in UTC + * + * @var string + */ + public $startDatetimeUTC; + + /** + * Ending date in UTC + * + * @var string + */ + public $strDateEnd; + + /** + * Name of the DB table _log_visit + * + * @var string + */ + public $logTable; + + /** + * When set to true, we always archive, even if the archive is already available. + * You can change this settings automatically in the config/global.ini.php always_archive_data under the [Debug] section + * + * @var bool + */ + public $debugAlwaysArchive = false; + + /** + * If the archive has at least 1 visit, this is set to true. + * + * @var bool + */ + public $isThereSomeVisits = null; + + protected $startTimestampUTC; + protected $endTimestampUTC; + + /** + * Flag that will forcefully disable the archiving process. Only set by the tests. + */ + public static $forceDisableArchiving = false; + + /** + * Constructor + */ + public function __construct() + { + $this->time = time(); + } + + /** + * Returns the Piwik_ArchiveProcessing_Day or Piwik_ArchiveProcessing_Period object + * depending on $name period string + * + * @param string $name day|week|month|year + * @throws Exception + * @return Piwik_ArchiveProcessing Piwik_ArchiveProcessing_Day|Piwik_ArchiveProcessing_Period + */ + static function factory($name) + { + switch ($name) { + case 'day': + $process = new Piwik_ArchiveProcessing_Day(); + $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_day']; + break; + + case 'week': + case 'month': + case 'year': + $process = new Piwik_ArchiveProcessing_Period(); + $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_period']; + break; + + case 'range': + $process = new Piwik_ArchiveProcessing_Period(); + $process->debugAlwaysArchive = Piwik_Config::getInstance()->Debug['always_archive_data_range']; + break; + + default: + throw new Exception("Unknown Archiving period specified '$name'"); + break; + } + return $process; + } + + const OPTION_TODAY_ARCHIVE_TTL = 'todayArchiveTimeToLive'; + const OPTION_BROWSER_TRIGGER_ARCHIVING = 'enableBrowserTriggerArchiving'; + + static public function getCoreMetrics() + { + return array( + 'nb_uniq_visitors', + 'nb_visits', + 'nb_actions', + 'sum_visit_length', + 'bounce_count', + 'nb_visits_converted', + ); + } + + 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 Piwik_Config::getInstance()->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); + Piwik_Tracker_Cache::clearCacheGeneral(); + } + + static public function isBrowserTriggerArchivingEnabled() + { + $browserArchivingEnabled = Piwik_GetOption(self::OPTION_BROWSER_TRIGGER_ARCHIVING); + if ($browserArchivingEnabled !== false) { + return (bool)$browserArchivingEnabled; + } + return (bool)Piwik_Config::getInstance()->General['enable_browser_archiving_triggering']; + } + + public function getIdArchive() + { + return $this->idArchive; + } + + /** + * Sets object attributes that will be used throughout the process + */ + public function init() + { + $this->idsite = $this->site->getId(); + $this->periodId = $this->period->getId(); + + $this->initDates(); + + $this->tableArchiveNumeric = self::makeNumericArchiveTable($this->period); + $this->tableArchiveBlob = self::makeBlobArchiveTable($this->period); + + $this->minDatetimeArchiveProcessedUTC = $this->getMinTimeArchivedProcessed(); + $db = Zend_Registry::get('db'); + $this->compressBlob = $db->hasBlobDataType(); + } + + /** + * The archive processing classes have features that might be useful for live querying; + * In particular, Piwik_ArchiveProcessing_Day::query*. In order to reuse those methods + * outside the actual archiving or to reuse archiving code for live querying, an instance + * of archive processing has to be faked. + * + * For example, this code can be used in an API method: + * $archiveProcessing = new Piwik_ArchiveProcessing_Day(); + * $archiveProcessing->setSite(new Piwik_Site($idSite)); + * $archiveProcessing->setPeriod(Piwik_Period::advancedFactory($period, $date)); + * $archiveProcessing->setSegment(new Piwik_Segment($segment, $idSite)); + * $archiveProcessing->initForLiveUsage(); + * Then, either use $archiveProcessing->query* or pass the instance to the archiving + * code of the plugin. Note that even though we use Piwik_ArchiveProcessing_Day, this + * works for any $period and $date that has been passed to the API. + */ + public function initForLiveUsage() + { + $this->idsite = $this->site->getId(); + $this->initDates(); + } + + private function initDates() + { + $dateStartLocalTimezone = $this->period->getDateStart(); + $dateEndLocalTimezone = $this->period->getDateEnd(); + + $dateStartUTC = $dateStartLocalTimezone->setTimezone($this->site->getTimezone()); + $dateEndUTC = $dateEndLocalTimezone->setTimezone($this->site->getTimezone()); + $this->startDatetimeUTC = $dateStartUTC->getDateStartUTC(); + $this->endDatetimeUTC = $dateEndUTC->getDateEndUTC(); + $this->startTimestampUTC = $dateStartUTC->getTimestamp(); + $this->endTimestampUTC = strtotime($this->endDatetimeUTC); + } + + /** + * Utility function which creates a TablePartitioning instance for the numeric + * archive data of a given period. + * + * @param Piwik_Period $period The time period of the archive data. + * @return Piwik_TablePartitioning_Monthly + */ + public static function makeNumericArchiveTable($period) + { + $result = new Piwik_TablePartitioning_Monthly('archive_numeric'); + $result->setTimestamp($period->getDateStart()->getTimestamp()); + return $result; + } + + /** + * Utility function which creates a TablePartitioning instance for the blob + * archive data of a given period. + * + * @param Piwik_Period $period The time period of the archive data. + * @return Piwik_TablePartitioning_Monthly + */ + public static function makeBlobArchiveTable($period) + { + $result = new Piwik_TablePartitioning_Monthly('archive_blob'); + $result->setTimestamp($period->getDateStart()->getTimestamp()); + return $result; + } + + 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 minDatetimeArchiveProcessedUTC that defines the lifetime value of today's archive + if ($this->period->getNumberOfSubperiods() == 0 + && ($this->startTimestampUTC <= $this->time && $this->endTimestampUTC > $this->time) + ) { + $this->temporaryArchive = true; + $minDatetimeArchiveProcessedUTC = $this->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; + } + } // - if the period we are looking for is finished, we look for a ts_archived that + // is greater than the last day of the archive + elseif ($this->endTimestampUTC <= $this->time) { + $minDatetimeArchiveProcessedUTC = $this->endTimestampUTC; + } // - if the period we're looking for is not finished, we look for a recent enough archive + else { + $this->temporaryArchive = true; + + // We choose to only look at archives that are newer than the specified timeout + $minDatetimeArchiveProcessedUTC = $this->time - self::getTodayArchiveTimeToLive(); + + // However, if archiving is disabled for this request, we shall + // accept any archive that was processed today after 00:00:01 this morning + if ($this->isArchivingDisabled()) { + $timezone = $this->site->getTimezone(); + $minDatetimeArchiveProcessedUTC = Piwik_Date::factory(Piwik_Date::factory('now', $timezone)->getDateStartUTC())->setTimezone($timezone)->getTimestamp(); + } + } + return $minDatetimeArchiveProcessedUTC; + } + + /** + * This method returns the idArchive ; if necessary, it triggers the archiving process. + * + * If the archive was not processed yet, it will launch the archiving process. + * If the current archive needs sub-archives (eg. a month archive needs all the days archive) + * it will recursively launch the archiving (using this loadArchive() on the sub-periods) + * + * @return int|false The idarchive of the archive, false if the archive is not archived yet + */ + public function loadArchive() + { + $this->init(); + if ($this->debugAlwaysArchive) { + return false; + } + $this->idArchive = $this->isArchived(); + + if ($this->idArchive === false) { + return false; + } + return $this->idArchive; + } + + /** + * @see loadArchive() + */ + public function launchArchiving() + { + if (!Piwik::getArchiveProcessingLock($this->idsite, $this->period, $this->segment)) { + Piwik::log('Unable to get lock for idSite = ' . $this->idsite + . ', period = ' . $this->period->getLabel() + . ', UTC datetime [' . $this->startDatetimeUTC . ' -> ' . $this->endDatetimeUTC . ' ]...'); + return; + } + + $this->initCompute(); + $this->compute(); + $this->postCompute(); + // we execute again the isArchived that does some initialization work + $this->idArchive = $this->isArchived(); + Piwik::releaseArchiveProcessingLock($this->idsite, $this->period, $this->segment); + } + + /** + * This methods reads the subperiods if necessary, + * and computes the archive of the current period. + */ + abstract protected function compute(); + + abstract public function isThereSomeVisits(); + + /** + * Returns the name of the archive field used to tell the status of an archive, (ie, + * whether the archive was created successfully or not). + * + * @param bool $flagArchiveAsAllPlugins + * @return string + */ + public function getDoneStringFlag($flagArchiveAsAllPlugins = false) + { + return self::getDoneStringFlagFor( + $this->getSegment(), $this->period, $this->getRequestedReport(), $flagArchiveAsAllPlugins); + } + + /** + * Returns the name of the archive field used to tell the status of an archive, (ie, + * whether the archive was created successfully or not). + * + * @param Piwik_Segment $segment + * @param Piwik_Period $period + * @param string $requestedReport + * @param bool $flagArchiveAsAllPlugins + * @return string + */ + public static function getDoneStringFlagFor($segment, $period, $requestedReport, $flagArchiveAsAllPlugins = false) + { + $segmentHash = $segment->getHash(); + if (!self::shouldProcessReportsAllPluginsFor($segment, $period)) { + $pluginProcessed = self::getPluginBeingProcessed($requestedReport); + if (!Piwik_PluginsManager::getInstance()->isPluginLoaded($pluginProcessed) + || $flagArchiveAsAllPlugins + ) { + $pluginProcessed = 'all'; + } + $segmentHash .= '.' . $pluginProcessed; + } + return 'done' . $segmentHash; + } + + /** + * Init the object before launching the real archive processing + */ + protected function initCompute() + { + $this->loadNextIdarchive(); + $done = $this->getDoneStringFlag(); + $this->insertNumericRecord($done, Piwik_ArchiveProcessing::DONE_ERROR); + + // Can be removed when GeoIp is in core + $this->logTable = Piwik_Common::prefixTable('log_visit'); + + $temporary = 'definitive archive'; + if ($this->isArchiveTemporary()) { + $temporary = 'temporary archive'; + } Piwik::log(sprintf("'%s, idSite = %d (%s), segment '%s', report = '%s', UTC datetime [%s -> %s]", $this->period->getLabel(), $this->idsite, @@ -576,518 +560,491 @@ abstract class Piwik_ArchiveProcessing $this->startDatetimeUTC, $this->endTimestampUTC )); - } - - /** - * Post processing called at the end of the main archive processing. - * Makes sure the new archive is marked as "successful" in the DB - * - * We also try to delete some stuff from memory but really there is still a lot... - */ - protected function postCompute() - { - // delete the first done = ERROR - $done = $this->getDoneStringFlag(); - Piwik_Query("DELETE FROM ".$this->tableArchiveNumeric->getTableName()." - WHERE idarchive = ? AND (name = '".$done."' OR name LIKE '".self::PREFIX_SQL_LOCK."%')", - array($this->idArchive) - ); - - $flag = Piwik_ArchiveProcessing::DONE_OK; - if($this->isArchiveTemporary()) - { - $flag = Piwik_ArchiveProcessing::DONE_OK_TEMPORARY; - } - $this->insertNumericRecord($done, $flag); - } - - /** - * Returns the name of the numeric table where the archive numeric values are stored - * - * @return string - */ - public function getTableArchiveNumericName() - { - return $this->tableArchiveNumeric->getTableName(); - } - - /** - * Returns the name of the blob table where the archive blob values are stored - * - * @return string - */ - public function getTableArchiveBlobName() - { - return $this->tableArchiveBlob->getTableName(); - } - - /** - * Set the period - * - * @param Piwik_Period $period - */ - public function setPeriod( Piwik_Period $period ) - { - $this->period = $period; - } - - public function setSegment( Piwik_Segment $segment) - { - $this->segment = $segment; - } - - public function getSegment() - { - return $this->segment; - } - /** - * Set the site - * - * @param Piwik_Site $site - */ - public function setSite( Piwik_Site $site ) - { - $this->site = $site; - } - - public function setRequestedReport($requestedReport) - { - $this->requestedReport = $requestedReport; - } - - protected function getRequestedReport() - { - return $this->requestedReport; - } - - static public function getPluginBeingProcessed( $requestedReport ) - { - $plugin = substr($requestedReport, 0, strpos($requestedReport, '_')); - if(empty($plugin) - || !Piwik_PluginsManager::getInstance()->isPluginActivated($plugin)) - { - $pluginStr = empty($plugin) ? '' : "($plugin)"; - throw new Exception("Error: The report '$requestedReport' was requested but it is not available at this stage. You may also disable the related plugin $pluginStr to avoid this error."); - } - return $plugin; - } - - /** - * Returns the timestamp of the first date of the period - * - * @return int - */ - public function getTimestampStartDate() - { - return $this->timestampDateStart; - } - - // exposing the number of visits publicly (number used to compute conversions rates) - protected $nb_visits = null; - protected $nb_visits_converted = null; - - protected function setNumberOfVisits($nb_visits) - { - $this->nb_visits = $nb_visits; - } - public function getNumberOfVisits() - { - return $this->nb_visits; - } - protected function setNumberOfVisitsConverted($nb_visits_converted) - { - $this->nb_visits_converted = $nb_visits_converted; - } - public function getNumberOfVisitsConverted() - { - return $this->nb_visits_converted; - } - - - /** - * Returns the idArchive we will use for the current archive - * - * @return int IdArchive to use when saving the current Archive - */ - protected function loadNextIdarchive() - { - $table = $this->tableArchiveNumeric->getTableName(); - $dbLockName = "loadNextIdArchive.$table"; - - $db = Zend_Registry::get('db'); - $locked = self::PREFIX_SQL_LOCK . Piwik_Common::generateUniqId(); - $date = date("Y-m-d H:i:s"); - - if (Piwik_GetDbLock($dbLockName, $maxRetries = 30) === false) - { - throw new Exception("loadNextIdArchive: Cannot get named lock for table $table."); - } - $db->exec("INSERT INTO $table " - ." SELECT ifnull(max(idarchive),0)+1, - '".$locked."', - ".(int)$this->idsite.", - '".$date."', - '".$date."', + } + + /** + * Post processing called at the end of the main archive processing. + * Makes sure the new archive is marked as "successful" in the DB + * + * We also try to delete some stuff from memory but really there is still a lot... + */ + protected function postCompute() + { + // delete the first done = ERROR + $done = $this->getDoneStringFlag(); + Piwik_Query("DELETE FROM " . $this->tableArchiveNumeric->getTableName() . " + WHERE idarchive = ? AND (name = '" . $done . "' OR name LIKE '" . self::PREFIX_SQL_LOCK . "%')", + array($this->idArchive) + ); + + $flag = Piwik_ArchiveProcessing::DONE_OK; + if ($this->isArchiveTemporary()) { + $flag = Piwik_ArchiveProcessing::DONE_OK_TEMPORARY; + } + $this->insertNumericRecord($done, $flag); + } + + /** + * Returns the name of the numeric table where the archive numeric values are stored + * + * @return string + */ + public function getTableArchiveNumericName() + { + return $this->tableArchiveNumeric->getTableName(); + } + + /** + * Returns the name of the blob table where the archive blob values are stored + * + * @return string + */ + public function getTableArchiveBlobName() + { + return $this->tableArchiveBlob->getTableName(); + } + + /** + * Set the period + * + * @param Piwik_Period $period + */ + public function setPeriod(Piwik_Period $period) + { + $this->period = $period; + } + + public function setSegment(Piwik_Segment $segment) + { + $this->segment = $segment; + } + + public function getSegment() + { + return $this->segment; + } + + /** + * Set the site + * + * @param Piwik_Site $site + */ + public function setSite(Piwik_Site $site) + { + $this->site = $site; + } + + public function setRequestedReport($requestedReport) + { + $this->requestedReport = $requestedReport; + } + + protected function getRequestedReport() + { + return $this->requestedReport; + } + + static public function getPluginBeingProcessed($requestedReport) + { + $plugin = substr($requestedReport, 0, strpos($requestedReport, '_')); + if (empty($plugin) + || !Piwik_PluginsManager::getInstance()->isPluginActivated($plugin) + ) { + $pluginStr = empty($plugin) ? '' : "($plugin)"; + throw new Exception("Error: The report '$requestedReport' was requested but it is not available at this stage. You may also disable the related plugin $pluginStr to avoid this error."); + } + return $plugin; + } + + /** + * Returns the timestamp of the first date of the period + * + * @return int + */ + public function getTimestampStartDate() + { + return $this->timestampDateStart; + } + + // exposing the number of visits publicly (number used to compute conversions rates) + protected $nb_visits = null; + protected $nb_visits_converted = null; + + protected function setNumberOfVisits($nb_visits) + { + $this->nb_visits = $nb_visits; + } + + public function getNumberOfVisits() + { + return $this->nb_visits; + } + + protected function setNumberOfVisitsConverted($nb_visits_converted) + { + $this->nb_visits_converted = $nb_visits_converted; + } + + public function getNumberOfVisitsConverted() + { + return $this->nb_visits_converted; + } + + + /** + * Returns the idArchive we will use for the current archive + * + * @return int IdArchive to use when saving the current Archive + */ + protected function loadNextIdarchive() + { + $table = $this->tableArchiveNumeric->getTableName(); + $dbLockName = "loadNextIdArchive.$table"; + + $db = Zend_Registry::get('db'); + $locked = self::PREFIX_SQL_LOCK . Piwik_Common::generateUniqId(); + $date = date("Y-m-d H:i:s"); + + if (Piwik_GetDbLock($dbLockName, $maxRetries = 30) === false) { + throw new Exception("loadNextIdArchive: Cannot get named lock for table $table."); + } + $db->exec("INSERT INTO $table " + . " SELECT ifnull(max(idarchive),0)+1, + '" . $locked . "', + " . (int)$this->idsite . ", + '" . $date . "', + '" . $date . "', 0, - '".$date."', + '" . $date . "', 0 " - ." FROM $table as tb1"); - Piwik_ReleaseDbLock($dbLockName); + . " FROM $table as tb1"); + Piwik_ReleaseDbLock($dbLockName); $id = $db->fetchOne("SELECT idarchive FROM $table WHERE name = ? LIMIT 1", $locked); - $this->idArchive = $id; - } - - /** - * @param string $name - * @param int|float $value - */ - public function insertNumericRecord($name, $value) - { - $value = round($value, 2); - return $this->insertRecord($name, $value); - } - - /** - * @param string $name - * @param string|array $values - * @return bool|array - */ - public function insertBlobRecord($name, $values) - { - if(is_array($values)) - { - $clean = array(); - foreach($values as $id => $value) - { - // for the parent Table we keep the name - // for example for the Table of searchEngines we keep the name 'referer_search_engine' - // but for the child table of 'Google' which has the ID = 9 the name would be 'referer_search_engine_9' - $newName = $name; - if($id != 0) - { - $newName = $name . '_' . $id; - } - - if($this->compressBlob) - { - $value = $this->compress($value); - } - $clean[] = array($newName, $value); - } - return $this->insertBulkRecords($clean); - } - - if($this->compressBlob) - { - $values = $this->compress($values); - } - - $this->insertRecord($name, $values); - return array($name => $values); - } - - protected function compress($data) - { - return gzcompress($data); - } - - protected function insertBulkRecords($records) - { - // Using standard plain INSERT if there is only one record to insert - if($DEBUG_DO_NOT_USE_BULK_INSERT = false - || count($records) == 1) - { - foreach($records as $record) - { - $this->insertRecord($record[0], $record[1]); - } - return ; - } - $bindSql = $this->getBindArray(); - $values = array(); - - foreach($records as $record) - { - // don't record zero - if(empty($record[1])) continue; - - $bind = $bindSql; - $bind[] = $record[0]; // name - $bind[] = $record[1]; // value - $values[] = $bind; - - } - if(empty($values)) return ; - - if(is_numeric($record[1])) - { - $table = $this->tableArchiveNumeric; - } - else - { - $table = $this->tableArchiveBlob; - } - - Piwik::tableInsertBatch($table->getTableName(), $this->getInsertFields(), $values); - return true; - } - - protected function getBindArray() - { - return array( $this->idArchive, - $this->idsite, - $this->period->getDateStart()->toString('Y-m-d'), - $this->period->getDateEnd()->toString('Y-m-d'), - $this->periodId, - date("Y-m-d H:i:s")); - } - - protected function getInsertFields() - { - return array('idarchive', 'idsite', 'date1', 'date2', 'period', 'ts_archived', 'name', 'value'); - } - - /** - * Inserts a record in the right table (either NUMERIC or BLOB) - * @param $name - * @param $value - * @return - */ - protected function insertRecord($name, $value) - { - // table to use to save the data - if(is_numeric($value)) - { - // We choose not to record records with a value of 0 - if($value == 0) - { - return; - } - $table = $this->tableArchiveNumeric; - } - else - { - $table = $this->tableArchiveBlob; - } - - // duplicate idarchives are Ignored, see http://dev.piwik.org/trac/ticket/987 - - $query = "INSERT IGNORE INTO ".$table->getTableName()." - (". implode(", ", $this->getInsertFields()).") + $this->idArchive = $id; + } + + /** + * @param string $name + * @param int|float $value + */ + public function insertNumericRecord($name, $value) + { + $value = round($value, 2); + return $this->insertRecord($name, $value); + } + + /** + * @param string $name + * @param string|array $values + * @return bool|array + */ + public function insertBlobRecord($name, $values) + { + if (is_array($values)) { + $clean = array(); + foreach ($values as $id => $value) { + // for the parent Table we keep the name + // for example for the Table of searchEngines we keep the name 'referer_search_engine' + // but for the child table of 'Google' which has the ID = 9 the name would be 'referer_search_engine_9' + $newName = $name; + if ($id != 0) { + $newName = $name . '_' . $id; + } + + if ($this->compressBlob) { + $value = $this->compress($value); + } + $clean[] = array($newName, $value); + } + return $this->insertBulkRecords($clean); + } + + if ($this->compressBlob) { + $values = $this->compress($values); + } + + $this->insertRecord($name, $values); + return array($name => $values); + } + + protected function compress($data) + { + return gzcompress($data); + } + + protected function insertBulkRecords($records) + { + // Using standard plain INSERT if there is only one record to insert + if ($DEBUG_DO_NOT_USE_BULK_INSERT = false + || count($records) == 1 + ) { + foreach ($records as $record) { + $this->insertRecord($record[0], $record[1]); + } + return; + } + $bindSql = $this->getBindArray(); + $values = array(); + + foreach ($records as $record) { + // don't record zero + if (empty($record[1])) continue; + + $bind = $bindSql; + $bind[] = $record[0]; // name + $bind[] = $record[1]; // value + $values[] = $bind; + + } + if (empty($values)) return; + + if (is_numeric($record[1])) { + $table = $this->tableArchiveNumeric; + } else { + $table = $this->tableArchiveBlob; + } + + Piwik::tableInsertBatch($table->getTableName(), $this->getInsertFields(), $values); + return true; + } + + protected function getBindArray() + { + return array($this->idArchive, + $this->idsite, + $this->period->getDateStart()->toString('Y-m-d'), + $this->period->getDateEnd()->toString('Y-m-d'), + $this->periodId, + date("Y-m-d H:i:s")); + } + + protected function getInsertFields() + { + return array('idarchive', 'idsite', 'date1', 'date2', 'period', 'ts_archived', 'name', 'value'); + } + + /** + * Inserts a record in the right table (either NUMERIC or BLOB) + * @param $name + * @param $value + * @return + */ + protected function insertRecord($name, $value) + { + // table to use to save the data + if (is_numeric($value)) { + // We choose not to record records with a value of 0 + if ($value == 0) { + return; + } + $table = $this->tableArchiveNumeric; + } else { + $table = $this->tableArchiveBlob; + } + + // duplicate idarchives are Ignored, see http://dev.piwik.org/trac/ticket/987 + + $query = "INSERT IGNORE INTO " . $table->getTableName() . " + (" . implode(", ", $this->getInsertFields()) . ") VALUES (?,?,?,?,?,?,?,?)"; - $bindSql = $this->getBindArray(); - $bindSql[] = $name; - $bindSql[] = $value; - Piwik_Query($query, $bindSql); - } - - /** - * Returns the idArchive if the archive is available in the database. - * Returns false if the archive needs to be computed. - * - * An archive is available if - * - 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 - * - * @return int|false - */ - protected function isArchived() - { - $bindSQL = array( $this->idsite, - $this->period->getDateStart()->toString('Y-m-d'), - $this->period->getDateEnd()->toString('Y-m-d'), - $this->periodId, - ); - - $timeStampWhere = ''; - - if($this->minDatetimeArchiveProcessedUTC) - { - $timeStampWhere = " AND ts_archived >= ? "; - $bindSQL[] = Piwik_Date::factory($this->minDatetimeArchiveProcessedUTC)->getDatetime(); - } - - // When a Segment is specified, we try and only process the requested report in the archive - // As a limitation, we don't know all the time which plugin should process which report - // There is a catch all flag 'all' appended to archives containing all reports already - // We look for this 'done.ABCDEFG.all', or for an archive that contains only our plugin data 'done.ABDCDEFG.Referers' - $done = $this->getDoneStringFlag(); - $doneAllPluginsProcessed = $this->getDoneStringFlag($flagArchiveAsAllPlugins = true); - - $sqlSegmentsFindArchiveAllPlugins = ''; - - if($done != $doneAllPluginsProcessed) - { - $sqlSegmentsFindArchiveAllPlugins = "OR (name = '".$doneAllPluginsProcessed."' AND value = ".Piwik_ArchiveProcessing::DONE_OK.") - OR (name = '".$doneAllPluginsProcessed."' AND value = ".Piwik_ArchiveProcessing::DONE_OK_TEMPORARY.")"; - } - $sqlQuery = " SELECT idarchive, value, name, date1 as startDate - FROM ".$this->tableArchiveNumeric->getTableName()." + $bindSql = $this->getBindArray(); + $bindSql[] = $name; + $bindSql[] = $value; + Piwik_Query($query, $bindSql); + } + + /** + * Returns the idArchive if the archive is available in the database. + * Returns false if the archive needs to be computed. + * + * An archive is available if + * - 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 + * + * @return int|false + */ + protected function isArchived() + { + $bindSQL = array($this->idsite, + $this->period->getDateStart()->toString('Y-m-d'), + $this->period->getDateEnd()->toString('Y-m-d'), + $this->periodId, + ); + + $timeStampWhere = ''; + + if ($this->minDatetimeArchiveProcessedUTC) { + $timeStampWhere = " AND ts_archived >= ? "; + $bindSQL[] = Piwik_Date::factory($this->minDatetimeArchiveProcessedUTC)->getDatetime(); + } + + // When a Segment is specified, we try and only process the requested report in the archive + // As a limitation, we don't know all the time which plugin should process which report + // There is a catch all flag 'all' appended to archives containing all reports already + // We look for this 'done.ABCDEFG.all', or for an archive that contains only our plugin data 'done.ABDCDEFG.Referers' + $done = $this->getDoneStringFlag(); + $doneAllPluginsProcessed = $this->getDoneStringFlag($flagArchiveAsAllPlugins = true); + + $sqlSegmentsFindArchiveAllPlugins = ''; + + if ($done != $doneAllPluginsProcessed) { + $sqlSegmentsFindArchiveAllPlugins = "OR (name = '" . $doneAllPluginsProcessed . "' AND value = " . Piwik_ArchiveProcessing::DONE_OK . ") + OR (name = '" . $doneAllPluginsProcessed . "' AND value = " . Piwik_ArchiveProcessing::DONE_OK_TEMPORARY . ")"; + } + $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.") + AND ( (name = '" . $done . "' AND value = " . Piwik_ArchiveProcessing::DONE_OK . ") + OR (name = '" . $done . "' AND value = " . Piwik_ArchiveProcessing::DONE_OK_TEMPORARY . ") $sqlSegmentsFindArchiveAllPlugins OR name = 'nb_visits') $timeStampWhere ORDER BY idarchive DESC"; - $results = Piwik_FetchAll($sqlQuery, $bindSQL ); - if(empty($results)) - { - return false; - } - - $idarchive = false; - // we look for the more recent idarchive - foreach($results as $result) - { - if($result['name'] == $done - || $result['name'] == $doneAllPluginsProcessed) - { - $idarchive = $result['idarchive']; - $this->timestampDateStart = Piwik_Date::factory($result['startDate'])->getTimestamp(); - break; - } - } - - // case when we have a nb_visits entry in the archive, but the process is not finished yet or failed to finish - // therefore we don't have the done=OK - if($idarchive === false) - { - return false; - } - - if($this->getPluginBeingProcessed($this->getRequestedReport()) == 'VisitsSummary') - { - $this->isThereSomeVisits = false; - } - - // we look for the nb_visits result for this most recent archive - foreach($results as $result) - { - if($result['name'] == 'nb_visits' - && $result['idarchive'] == $idarchive) - { - $this->isThereSomeVisits = ($result['value'] > 0); - $this->setNumberOfVisits($result['value']); - break; - } - } - return $idarchive; - } - - /** - * Returns true if, for some reasons, triggering the archiving is disabled. - * Note that when a segment is passed to the function, archiving will always occur - * (since segments are by default not pre-processed) - * - * @return bool - */ - public function isArchivingDisabled() - { - return self::isArchivingDisabledFor($this->getSegment(), $this->period); - } - - public static function isArchivingDisabledFor($segment, $period) - { - if($period->getLabel() == 'range') { + $results = Piwik_FetchAll($sqlQuery, $bindSQL); + if (empty($results)) { + return false; + } + + $idarchive = false; + // we look for the more recent idarchive + foreach ($results as $result) { + if ($result['name'] == $done + || $result['name'] == $doneAllPluginsProcessed + ) { + $idarchive = $result['idarchive']; + $this->timestampDateStart = Piwik_Date::factory($result['startDate'])->getTimestamp(); + break; + } + } + + // case when we have a nb_visits entry in the archive, but the process is not finished yet or failed to finish + // therefore we don't have the done=OK + if ($idarchive === false) { + return false; + } + + if ($this->getPluginBeingProcessed($this->getRequestedReport()) == 'VisitsSummary') { + $this->isThereSomeVisits = false; + } + + // we look for the nb_visits result for this most recent archive + foreach ($results as $result) { + if ($result['name'] == 'nb_visits' + && $result['idarchive'] == $idarchive + ) { + $this->isThereSomeVisits = ($result['value'] > 0); + $this->setNumberOfVisits($result['value']); + break; + } + } + return $idarchive; + } + + /** + * Returns true if, for some reasons, triggering the archiving is disabled. + * Note that when a segment is passed to the function, archiving will always occur + * (since segments are by default not pre-processed) + * + * @return bool + */ + public function isArchivingDisabled() + { + return self::isArchivingDisabledFor($this->getSegment(), $this->period); + } + + public static function isArchivingDisabledFor($segment, $period) + { + if ($period->getLabel() == 'range') { return false; } $processOneReportOnly = !self::shouldProcessReportsAllPluginsFor($segment, $period); $isArchivingDisabled = !self::isRequestAuthorizedToArchive(); - if($processOneReportOnly) - { + if ($processOneReportOnly) { // When there is a segment, archiving is not necessary allowed // If browser archiving is allowed, then archiving is enabled // if browser archiving is not allowed, then archiving is disabled - if(!$segment->isEmpty() + if (!$segment->isEmpty() && $isArchivingDisabled && Piwik_Config::getInstance()->General['browser_archiving_disabled_enforce'] - ) - { + ) { Piwik::log("Archiving is disabled because of config setting browser_archiving_disabled_enforce=1"); return true; } return false; } - return $isArchivingDisabled; - } - - protected static function isRequestAuthorizedToArchive() - { - return !self::$forceDisableArchiving && - (self::isBrowserTriggerArchivingEnabled() - || Piwik_Common::isPhpCliMode() - || (Piwik::isUserIsSuperUser() - && Piwik_Common::isArchivePhpTriggered())) - ; - } - - /** - * Returns true when - * - there is no segment and period is not range - * - there is a segment that is part of the preprocessed [Segments] list - * @param Piwik_Segment $segment - * @param Piwik_Period $period - * @return bool - */ - protected function shouldProcessReportsAllPlugins($segment, $period) - { - return self::shouldProcessReportsAllPluginsFor($segment, $period); - } - - /** - * @param Piwik_Segment $segment - * @param Piwik_Period $period - * @return bool - */ - protected static function shouldProcessReportsAllPluginsFor($segment, $period) - { - if($segment->isEmpty() && $period->getLabel() != 'range') - { - return true; - } - - $segmentsToProcess = Piwik::getKnownSegmentsToArchive(); - if(!empty($segmentsToProcess)) - { - // If the requested segment is one of the segments to pre-process - // we ensure that any call to the API will trigger archiving of all reports for this segment - $segment = $segment->getString(); - if(in_array($segment, $segmentsToProcess)) - { - return true; - } - } - return false; - } - - /** - * When a segment is set, we shall only process the requested report (no more). - * The requested data set will return a lot faster if we only process these reports rather than all plugins. - * Similarly, when a period=range is requested, we shall only process the requested report for the range itself. - * - * @param string $pluginName - * @return bool - */ - public function shouldProcessReportsForPlugin($pluginName) - { - if($this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period)) - { - return true; - } - - // If any other segment, only process if the requested report belong to this plugin - // or process all plugins if the requested report plugin couldn't be guessed - $pluginBeingProcessed = self::getPluginBeingProcessed($this->getRequestedReport()); - return $pluginBeingProcessed == $pluginName - || !Piwik_PluginsManager::getInstance()->isPluginLoaded($pluginBeingProcessed) - ; - } - + return $isArchivingDisabled; + } + + protected static function isRequestAuthorizedToArchive() + { + return !self::$forceDisableArchiving && + (self::isBrowserTriggerArchivingEnabled() + || Piwik_Common::isPhpCliMode() + || (Piwik::isUserIsSuperUser() + && Piwik_Common::isArchivePhpTriggered())); + } + + /** + * Returns true when + * - there is no segment and period is not range + * - there is a segment that is part of the preprocessed [Segments] list + * @param Piwik_Segment $segment + * @param Piwik_Period $period + * @return bool + */ + protected function shouldProcessReportsAllPlugins($segment, $period) + { + return self::shouldProcessReportsAllPluginsFor($segment, $period); + } + + /** + * @param Piwik_Segment $segment + * @param Piwik_Period $period + * @return bool + */ + protected static function shouldProcessReportsAllPluginsFor($segment, $period) + { + if ($segment->isEmpty() && $period->getLabel() != 'range') { + return true; + } + + $segmentsToProcess = Piwik::getKnownSegmentsToArchive(); + if (!empty($segmentsToProcess)) { + // If the requested segment is one of the segments to pre-process + // we ensure that any call to the API will trigger archiving of all reports for this segment + $segment = $segment->getString(); + if (in_array($segment, $segmentsToProcess)) { + return true; + } + } + return false; + } + + /** + * When a segment is set, we shall only process the requested report (no more). + * The requested data set will return a lot faster if we only process these reports rather than all plugins. + * Similarly, when a period=range is requested, we shall only process the requested report for the range itself. + * + * @param string $pluginName + * @return bool + */ + public function shouldProcessReportsForPlugin($pluginName) + { + if ($this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period)) { + return true; + } + + // If any other segment, only process if the requested report belong to this plugin + // or process all plugins if the requested report plugin couldn't be guessed + $pluginBeingProcessed = self::getPluginBeingProcessed($this->getRequestedReport()); + return $pluginBeingProcessed == $pluginName + || !Piwik_PluginsManager::getInstance()->isPluginLoaded($pluginBeingProcessed); + } + } diff --git a/core/ArchiveProcessing/Day.php b/core/ArchiveProcessing/Day.php index 0e4198a36f..07921e9a50 100644 --- a/core/ArchiveProcessing/Day.php +++ b/core/ArchiveProcessing/Day.php @@ -21,65 +21,62 @@ */ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing { - /** - * Constructor - */ - function __construct() - { - parent::__construct(); - $this->db = Zend_Registry::get('db'); - } - - /** - * Main method to process logs for a day. The only logic done here is computing the number of visits, actions, etc. - * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Day.compute'. - * See some of the plugins for an example eg. 'Provider' - */ - protected function compute() - { - if(!$this->isThereSomeVisits()) - { - return; - } - Piwik_PostEvent('ArchiveProcessing_Day.compute', $this); - } - - /** - * Returns true if there are logs for the current archive. - * - * If the current archive is for a specific plugin (for example, Referers), - * (for example when a Segment is defined and the Keywords report is requested) - * Then the function will create the Archive for the Core metrics 'VisitsSummary' which will in turn process the number of visits - * - * If there is no specified segment, the SQL query will always run. - * - * @return bool|null - */ - public function isThereSomeVisits() - { - if (!is_null($this->isThereSomeVisits)) - { - if ($this->isThereSomeVisits && is_null($this->nb_visits)) - { - debug_print_backtrace(); - exit; - } - return $this->isThereSomeVisits; - } - - // prepare segmentation - $segment = $this->getSegment(); - - // We check if there is visits for the requested date / site / segment - // If no specified Segment - // Or if a segment is passed and we specifically process VisitsSummary - // Then we check the logs. This is to ensure that this query is ran only once for this day/site/segment (rather than running it for every plugin) - $reportType = self::getPluginBeingProcessed($this->getRequestedReport()); - if ($this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period) - || ($reportType == 'VisitsSummary')) - { - // build query parts - $select = "count(distinct log_visit.idvisitor) as nb_uniq_visitors, + /** + * Constructor + */ + function __construct() + { + parent::__construct(); + $this->db = Zend_Registry::get('db'); + } + + /** + * Main method to process logs for a day. The only logic done here is computing the number of visits, actions, etc. + * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Day.compute'. + * See some of the plugins for an example eg. 'Provider' + */ + protected function compute() + { + if (!$this->isThereSomeVisits()) { + return; + } + Piwik_PostEvent('ArchiveProcessing_Day.compute', $this); + } + + /** + * Returns true if there are logs for the current archive. + * + * If the current archive is for a specific plugin (for example, Referers), + * (for example when a Segment is defined and the Keywords report is requested) + * Then the function will create the Archive for the Core metrics 'VisitsSummary' which will in turn process the number of visits + * + * If there is no specified segment, the SQL query will always run. + * + * @return bool|null + */ + public function isThereSomeVisits() + { + if (!is_null($this->isThereSomeVisits)) { + if ($this->isThereSomeVisits && is_null($this->nb_visits)) { + debug_print_backtrace(); + exit; + } + return $this->isThereSomeVisits; + } + + // prepare segmentation + $segment = $this->getSegment(); + + // We check if there is visits for the requested date / site / segment + // If no specified Segment + // Or if a segment is passed and we specifically process VisitsSummary + // Then we check the logs. This is to ensure that this query is ran only once for this day/site/segment (rather than running it for every plugin) + $reportType = self::getPluginBeingProcessed($this->getRequestedReport()); + if ($this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period) + || ($reportType == 'VisitsSummary') + ) { + // build query parts + $select = "count(distinct log_visit.idvisitor) as nb_uniq_visitors, count(*) as nb_visits, sum(log_visit.visit_total_actions) as nb_actions, max(log_visit.visit_total_actions) as max_actions, @@ -87,545 +84,503 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as bounce_count, sum(case log_visit.visit_goal_converted when 1 then 1 else 0 end) as nb_visits_converted "; - $from = "log_visit"; - $where = "log_visit.visit_last_action_time >= ? + $from = "log_visit"; + $where = "log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? AND log_visit.idsite = ? "; - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - $query = $segment->getSelectQuery($select, $from, $where, $bind); - - $bind = $query['bind']; - $sql = $query['sql']; - - $data = $this->db->fetchRow($sql, $bind); - - // no visits found - if (!is_array($data) || $data['nb_visits'] == 0) - { - return $this->isThereSomeVisits = false; - } - - // visits found: set attribtues - foreach ($data as $name => $value) - { - $this->insertNumericRecord($name, $value); - } - - $this->setNumberOfVisits($data['nb_visits']); - $this->setNumberOfVisitsConverted($data['nb_visits_converted']); - - return $this->isThereSomeVisits = true; - } - - return $this->redirectRequestToVisitsSummary(); - } - - /** - * If a segment is specified but a plugin other than 'VisitsSummary' is being requested, - * we create an archive for processing VisitsSummary Core Metrics, which will in turn - * execute the query above (in isThereSomeVisits) - * - * @return bool|null - */ - private function redirectRequestToVisitsSummary() - { - $archive = new Piwik_Archive_Single(); - $archive->setSite($this->site); - $archive->setPeriod($this->period); - $archive->setSegment($this->getSegment()); - $archive->setRequestedReport('VisitsSummary'); - - $nbVisits = $archive->getNumeric('nb_visits'); - $this->isThereSomeVisits = $nbVisits > 0; - - if ($this->isThereSomeVisits) - { - $nbVisitsConverted = $archive->getNumeric('nb_visits_converted'); - $this->setNumberOfVisits($nbVisits); - $this->setNumberOfVisitsConverted($nbVisitsConverted); - } - - return $this->isThereSomeVisits; - } - - /** - * Creates and returns an array of SQL SELECT expressions that will summarize - * the data in a column of a specified table, over a set of ranges. - * - * The SELECT expressions will count the number of column values that are - * within each range. - * - * @param string $column The column of the log_conversion table to reduce. - * @param array $ranges The ranges to reduce data over. - * @param string $table The table the SELECTs should use. - * @param string $selectColumnPrefix The prefix when specifying what a SELECT - * expression will be selected AS. - * @param bool|string $extraCondition An extra condition to be appended to 'case when' - * expressions. Must start with the logical operator, - * ie (AND, OR, etc.). - * @return array An array of SQL SELECT expressions. - */ - public static function buildReduceByRangeSelect( - $column, $ranges, $table, $selectColumnPrefix = '', $extraCondition = false) - { - $selects = array(); - - foreach($ranges as $gap) - { - if (count($gap) == 2) - { - $lowerBound = $gap[0]; - $upperBound = $gap[1]; - - $selectAs = "$selectColumnPrefix$lowerBound-$upperBound"; - - $selects[] = "sum(case when $table.$column between $lowerBound and $upperBound $extraCondition". - " then 1 else 0 end) as `$selectAs`"; - } - else - { - $lowerBound = $gap[0]; - - $selectAs = $selectColumnPrefix.($lowerBound + 1).urlencode('+'); - - $selects[] = "sum(case when $table.$column > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`"; - } - } - - return $selects; - } - - /** - * Converts a database SELECT result into a whole DataTable with two columns and as many - * rows as elements in $row. - * - * The key of each element in $row is used as the value of the first column, and the - * value of each element is used as the second column. - * - * NOTE: $selectAsPrefix can be used to make sure only SOME of the data in $row is used. - * - * @param array $row The database row to convert. - * @param mixed $labelCount The label to use for the second column of the DataTable result. - * @param string $selectAsPrefix A string that identifies which elements of $row to use - * in the result. Every key of $row that starts with this - * value is used. - * @return Piwik_DataTable - */ - public function getSimpleDataTableFromRow($row, $labelCount, $selectAsPrefix = '') - { - // the labels in $row can have prefixes that need to be removed before creating a table - $cleanRow = array(); - - foreach($row as $label => $count) - { - if (empty($selectAsPrefix) || strpos($label, $selectAsPrefix) === 0) - { - $cleanLabel = substr($label, strlen($selectAsPrefix)); - - $cleanRow[$cleanLabel] = array($labelCount => $count); - } - } - - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($cleanRow); - return $table; - } - - /** - * Performs a simple query on the log_visit table within the time range this archive - * represents. - * - * @param string $select The SELECT clause. - * @param string|bool $orderBy The ORDER BY clause (without the 'ORDER BY' part). Set to - * false to specify no ORDER BY. - * @param array|bool $groupByCols An array of column names to group by. Set to false to - * specify no GROUP BY. - * @param bool $oneResultRow Whether only one row is expected or not. If set to true, - * this function returns one row, if false, an array of rows. - * @return array - */ - public function queryVisitsSimple($select, $orderBy = false, $groupByCols = false, $oneResultRow = true) - { - $from = "log_visit"; - $where = "log_visit.visit_last_action_time >= ? + + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + $query = $segment->getSelectQuery($select, $from, $where, $bind); + + $bind = $query['bind']; + $sql = $query['sql']; + + $data = $this->db->fetchRow($sql, $bind); + + // no visits found + if (!is_array($data) || $data['nb_visits'] == 0) { + return $this->isThereSomeVisits = false; + } + + // visits found: set attribtues + foreach ($data as $name => $value) { + $this->insertNumericRecord($name, $value); + } + + $this->setNumberOfVisits($data['nb_visits']); + $this->setNumberOfVisitsConverted($data['nb_visits_converted']); + + return $this->isThereSomeVisits = true; + } + + return $this->redirectRequestToVisitsSummary(); + } + + /** + * If a segment is specified but a plugin other than 'VisitsSummary' is being requested, + * we create an archive for processing VisitsSummary Core Metrics, which will in turn + * execute the query above (in isThereSomeVisits) + * + * @return bool|null + */ + private function redirectRequestToVisitsSummary() + { + $archive = new Piwik_Archive_Single(); + $archive->setSite($this->site); + $archive->setPeriod($this->period); + $archive->setSegment($this->getSegment()); + $archive->setRequestedReport('VisitsSummary'); + + $nbVisits = $archive->getNumeric('nb_visits'); + $this->isThereSomeVisits = $nbVisits > 0; + + if ($this->isThereSomeVisits) { + $nbVisitsConverted = $archive->getNumeric('nb_visits_converted'); + $this->setNumberOfVisits($nbVisits); + $this->setNumberOfVisitsConverted($nbVisitsConverted); + } + + return $this->isThereSomeVisits; + } + + /** + * Creates and returns an array of SQL SELECT expressions that will summarize + * the data in a column of a specified table, over a set of ranges. + * + * The SELECT expressions will count the number of column values that are + * within each range. + * + * @param string $column The column of the log_conversion table to reduce. + * @param array $ranges The ranges to reduce data over. + * @param string $table The table the SELECTs should use. + * @param string $selectColumnPrefix The prefix when specifying what a SELECT + * expression will be selected AS. + * @param bool|string $extraCondition An extra condition to be appended to 'case when' + * expressions. Must start with the logical operator, + * ie (AND, OR, etc.). + * @return array An array of SQL SELECT expressions. + */ + public static function buildReduceByRangeSelect( + $column, $ranges, $table, $selectColumnPrefix = '', $extraCondition = false) + { + $selects = array(); + + foreach ($ranges as $gap) { + if (count($gap) == 2) { + $lowerBound = $gap[0]; + $upperBound = $gap[1]; + + $selectAs = "$selectColumnPrefix$lowerBound-$upperBound"; + + $selects[] = "sum(case when $table.$column between $lowerBound and $upperBound $extraCondition" . + " then 1 else 0 end) as `$selectAs`"; + } else { + $lowerBound = $gap[0]; + + $selectAs = $selectColumnPrefix . ($lowerBound + 1) . urlencode('+'); + + $selects[] = "sum(case when $table.$column > $lowerBound $extraCondition then 1 else 0 end) as `$selectAs`"; + } + } + + return $selects; + } + + /** + * Converts a database SELECT result into a whole DataTable with two columns and as many + * rows as elements in $row. + * + * The key of each element in $row is used as the value of the first column, and the + * value of each element is used as the second column. + * + * NOTE: $selectAsPrefix can be used to make sure only SOME of the data in $row is used. + * + * @param array $row The database row to convert. + * @param mixed $labelCount The label to use for the second column of the DataTable result. + * @param string $selectAsPrefix A string that identifies which elements of $row to use + * in the result. Every key of $row that starts with this + * value is used. + * @return Piwik_DataTable + */ + public function getSimpleDataTableFromRow($row, $labelCount, $selectAsPrefix = '') + { + // the labels in $row can have prefixes that need to be removed before creating a table + $cleanRow = array(); + + foreach ($row as $label => $count) { + if (empty($selectAsPrefix) || strpos($label, $selectAsPrefix) === 0) { + $cleanLabel = substr($label, strlen($selectAsPrefix)); + + $cleanRow[$cleanLabel] = array($labelCount => $count); + } + } + + $table = new Piwik_DataTable(); + $table->addRowsFromArrayWithIndexLabel($cleanRow); + return $table; + } + + /** + * Performs a simple query on the log_visit table within the time range this archive + * represents. + * + * @param string $select The SELECT clause. + * @param string|bool $orderBy The ORDER BY clause (without the 'ORDER BY' part). Set to + * false to specify no ORDER BY. + * @param array|bool $groupByCols An array of column names to group by. Set to false to + * specify no GROUP BY. + * @param bool $oneResultRow Whether only one row is expected or not. If set to true, + * this function returns one row, if false, an array of rows. + * @return array + */ + public function queryVisitsSimple($select, $orderBy = false, $groupByCols = false, $oneResultRow = true) + { + $from = "log_visit"; + $where = "log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? AND log_visit.idsite = ?"; - - $groupBy = false; - if ($groupByCols and !empty($groupByCols)) - { - $groupBy = implode(',', $groupByCols); - } - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); - - if ($oneResultRow) - { - return $this->db->fetchRow($query['sql'], $query['bind']); - } - else - { - return $this->db->fetchAll($query['sql'], $query['bind']); - } - } - - /** - * Helper function that returns a DataTable containing the $select fields / value pairs. - * IMPORTANT: The $select must return only one row!! - * - * Example $select = "count(distinct( config_os )) as countDistinctOs, - * sum( config_flash ) / count(distinct(idvisit)) as percentFlash " - * $labelCount = "test_column_name" - * will return a dataTable that looks like - * label test_column_name - * CountDistinctOs 9 - * PercentFlash 0.5676 - * - * - * @param string $select - * @param string $labelCount - * @return Piwik_DataTable - */ - public function getSimpleDataTableFromSelect($select, $labelCount) - { - $data = $this->queryVisitsSimple($select); - return $this->getSimpleDataTableFromRow($data, $labelCount); - } - - /** - * Returns the actions by the given dimension - * - * - The basic use case is to use $label and optionally $where. - * - If you want to apply a limit and group the others, use $orderBy to sort the way you - * want the limit to be applied and pass a pre-configured instance of Piwik_RankingQuery. - * The ranking query instance has to have a limit and at least one label column. - * See Piwik_RankingQuery::setLimit() and Piwik_RankingQuery::addLabelColumn(). - * If $rankingQuery is set, the return value is the array returned by - * Piwik_RankingQuery::execute(). - * - By default, the method only queries log_link_visit_action. If you need data from - * log_action (e.g. to partition the result from the ranking query into the different - * action types), use $joinLogActionOnColumn and $addSelect to join log_action and select - * the column you need from log_action. - * - * - * @param array|string $label the dimensions(s) you're interested in - * @param string $where where clause - * @param bool|array $metrics Set this if you want to limit the columns that are returned. - * The possible values in the array are Piwik_Archive::INDEX_*. - * @param bool|string $orderBy order by clause - * @param Piwik_RankingQuery $rankingQuery pre-configured ranking query instance - * @param bool|string $joinLogActionOnColumn column from log_link_visit_action that - * log_action should be joined on. - * can be an array to join multiple times. - * @param bool|string $addSelect additional select clause - * @return mixed - */ - public function queryActionsByDimension($label, $where = '', $metrics = false, $orderBy = false, - $rankingQuery = null, $joinLogActionOnColumn = false, $addSelect = false) - { - if(is_array($label)) - { - $label2 = $label; - foreach($label2 as &$field) { $field = 'log_link_visit_action.'. $field; } - $groupBy = implode(", ", $label2); - foreach($label2 as $id => &$field) { $field = "$field AS ".$label[$id]; } - $select = implode(", ", $label2); - - // IF we query Custom Variables scope "page" either: Product SKU, Product Name, - // then we also query the "Product page view" price which was possibly recorded. - if(in_array(reset($label), array('custom_var_k3','custom_var_k4','custom_var_k5'))) - { - $select .= ", ".self::getSqlRevenue("AVG(log_link_visit_action.custom_var_v2)")." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED ."`"; - } - } - else - { - $select = $label . " AS label "; - $groupBy = 'label'; - } - - if(!empty($where)) - { - $where = sprintf($where, "log_link_visit_action", "log_link_visit_action"); - $where = ' AND '.$where; - } - - $pre = ", \n\t\t\t"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS, $metrics)) - $select .= $pre . "count(distinct log_link_visit_action.idvisit) as `". Piwik_Archive::INDEX_NB_VISITS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS, $metrics)) - $select .= $pre . "count(distinct log_link_visit_action.idvisitor) as `". Piwik_Archive::INDEX_NB_UNIQ_VISITORS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_ACTIONS, $metrics)) - $select .= $pre . "count(*) as `". Piwik_Archive::INDEX_NB_ACTIONS ."`"; - - $from = "log_link_visit_action"; - - if ($joinLogActionOnColumn !== false) - { - $multiJoin = is_array($joinLogActionOnColumn); - if (!$multiJoin) - { - $joinLogActionOnColumn = array($joinLogActionOnColumn); - } - - $from = array($from); - - foreach ($joinLogActionOnColumn as $i => $joinColumn) - { - $tableAlias = 'log_action'.($multiJoin ? $i + 1 : ''); - if (strpos($joinColumn, ' ') === false) { - $joinOn = $tableAlias.'.idaction = log_link_visit_action.'.$joinColumn; - } else { - // more complex join column like IF(...) - $joinOn = $tableAlias.'.idaction = '.$joinColumn; - } - $from[] = array( - 'table' => 'log_action', - 'tableAlias' => $tableAlias, - 'joinOn' => $joinOn - ); - } - } - - if ($addSelect !== false) - { - $select .= ', '.$addSelect; - } - - $where = "log_link_visit_action.server_time >= ? + + $groupBy = false; + if ($groupByCols and !empty($groupByCols)) { + $groupBy = implode(',', $groupByCols); + } + + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + + $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); + + if ($oneResultRow) { + return $this->db->fetchRow($query['sql'], $query['bind']); + } else { + return $this->db->fetchAll($query['sql'], $query['bind']); + } + } + + /** + * Helper function that returns a DataTable containing the $select fields / value pairs. + * IMPORTANT: The $select must return only one row!! + * + * Example $select = "count(distinct( config_os )) as countDistinctOs, + * sum( config_flash ) / count(distinct(idvisit)) as percentFlash " + * $labelCount = "test_column_name" + * will return a dataTable that looks like + * label test_column_name + * CountDistinctOs 9 + * PercentFlash 0.5676 + * + * + * @param string $select + * @param string $labelCount + * @return Piwik_DataTable + */ + public function getSimpleDataTableFromSelect($select, $labelCount) + { + $data = $this->queryVisitsSimple($select); + return $this->getSimpleDataTableFromRow($data, $labelCount); + } + + /** + * Returns the actions by the given dimension + * + * - The basic use case is to use $label and optionally $where. + * - If you want to apply a limit and group the others, use $orderBy to sort the way you + * want the limit to be applied and pass a pre-configured instance of Piwik_RankingQuery. + * The ranking query instance has to have a limit and at least one label column. + * See Piwik_RankingQuery::setLimit() and Piwik_RankingQuery::addLabelColumn(). + * If $rankingQuery is set, the return value is the array returned by + * Piwik_RankingQuery::execute(). + * - By default, the method only queries log_link_visit_action. If you need data from + * log_action (e.g. to partition the result from the ranking query into the different + * action types), use $joinLogActionOnColumn and $addSelect to join log_action and select + * the column you need from log_action. + * + * + * @param array|string $label the dimensions(s) you're interested in + * @param string $where where clause + * @param bool|array $metrics Set this if you want to limit the columns that are returned. + * The possible values in the array are Piwik_Archive::INDEX_*. + * @param bool|string $orderBy order by clause + * @param Piwik_RankingQuery $rankingQuery pre-configured ranking query instance + * @param bool|string $joinLogActionOnColumn column from log_link_visit_action that + * log_action should be joined on. + * can be an array to join multiple times. + * @param bool|string $addSelect additional select clause + * @return mixed + */ + public function queryActionsByDimension($label, $where = '', $metrics = false, $orderBy = false, + $rankingQuery = null, $joinLogActionOnColumn = false, $addSelect = false) + { + if (is_array($label)) { + $label2 = $label; + foreach ($label2 as &$field) { + $field = 'log_link_visit_action.' . $field; + } + $groupBy = implode(", ", $label2); + foreach ($label2 as $id => &$field) { + $field = "$field AS " . $label[$id]; + } + $select = implode(", ", $label2); + + // IF we query Custom Variables scope "page" either: Product SKU, Product Name, + // then we also query the "Product page view" price which was possibly recorded. + if (in_array(reset($label), array('custom_var_k3', 'custom_var_k4', 'custom_var_k5'))) { + $select .= ", " . self::getSqlRevenue("AVG(log_link_visit_action.custom_var_v2)") . " as `" . Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED . "`"; + } + } else { + $select = $label . " AS label "; + $groupBy = 'label'; + } + + if (!empty($where)) { + $where = sprintf($where, "log_link_visit_action", "log_link_visit_action"); + $where = ' AND ' . $where; + } + + $pre = ", \n\t\t\t"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS, $metrics)) + $select .= $pre . "count(distinct log_link_visit_action.idvisit) as `" . Piwik_Archive::INDEX_NB_VISITS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS, $metrics)) + $select .= $pre . "count(distinct log_link_visit_action.idvisitor) as `" . Piwik_Archive::INDEX_NB_UNIQ_VISITORS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_ACTIONS, $metrics)) + $select .= $pre . "count(*) as `" . Piwik_Archive::INDEX_NB_ACTIONS . "`"; + + $from = "log_link_visit_action"; + + if ($joinLogActionOnColumn !== false) { + $multiJoin = is_array($joinLogActionOnColumn); + if (!$multiJoin) { + $joinLogActionOnColumn = array($joinLogActionOnColumn); + } + + $from = array($from); + + foreach ($joinLogActionOnColumn as $i => $joinColumn) { + $tableAlias = 'log_action' . ($multiJoin ? $i + 1 : ''); + if (strpos($joinColumn, ' ') === false) { + $joinOn = $tableAlias . '.idaction = log_link_visit_action.' . $joinColumn; + } else { + // more complex join column like IF(...) + $joinOn = $tableAlias . '.idaction = ' . $joinColumn; + } + $from[] = array( + 'table' => 'log_action', + 'tableAlias' => $tableAlias, + 'joinOn' => $joinOn + ); + } + } + + if ($addSelect !== false) { + $select .= ', ' . $addSelect; + } + + $where = "log_link_visit_action.server_time >= ? AND log_link_visit_action.server_time <= ? AND log_link_visit_action.idsite = ? $where"; - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); - - if ($rankingQuery !== null) - { - $sumColumns = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS, - Piwik_Archive::INDEX_NB_VISITS, - Piwik_Archive::INDEX_NB_ACTIONS - ); - if ($metrics) - { - foreach ($sumColumns as $i => $column) - { - if (!in_array($column, $metrics)) - { - unset($sumColumns[$i]); - } - } - $sumColumns = array_values($sumColumns); - } - $rankingQuery->addColumn($sumColumns, 'sum'); - return $rankingQuery->execute($query['sql'], $query['bind']); - } - - return $this->db->query($query['sql'], $query['bind']); - } - - /** - * Query visits by dimension - * - * @param array|string $label Can be a string, eg. "referer_name", will be aliased as 'label' in the returned rows - * Can also be an array of strings, when the dimension spans multiple fields, - * eg. array("referer_name", "referer_keyword") - * @param string $where Additional condition for WHERE clause - * @param bool|array $metrics Set this if you want to limit the columns that are returned. - * The possible values in the array are Piwik_Archive::INDEX_*. - * @param bool|string $orderBy ORDER BY clause. This is needed in combination with $rankingQuery. - * @param Piwik_RankingQuery $rankingQuery - * A pre-configured ranking query instance that is used to limit the result. - * If set, the return value is the array returned by Piwik_RankingQuery::execute(). - * @param string $addSelect Additional SELECT clause - * @param bool $addSelectGeneratesLabelColumn - * Set to true if the $label column is generated in $addSelect. - * @return mixed - */ - public function queryVisitsByDimension($label, $where = '', $metrics = false, $orderBy = false, - $rankingQuery = null, $addSelect = false, $addSelectGeneratesLabelColumn = false) - { - if(is_array($label)) - { - $groupBy = "log_visit.".implode(", log_visit.", $label); - foreach($label as &$field) - { - $field = 'log_visit.'.$field.' AS '.$field; - } - $select = implode(", ", $label); - } - else if ($addSelectGeneratesLabelColumn) - { - $select = $addSelect; - $groupBy = $label; - } - else - { - $select = $label . " AS label "; - $groupBy = 'label'; - } - - if(!empty($where)) - { - $where = sprintf($where, "log_visit", "log_visit"); - $where = ' AND '.$where; - } - - $pre = ", \n\t\t\t"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS, $metrics)) - $select .= $pre . "count(distinct log_visit.idvisitor) as `". Piwik_Archive::INDEX_NB_UNIQ_VISITORS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS, $metrics)) - $select .= $pre . "count(*) as `". Piwik_Archive::INDEX_NB_VISITS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_ACTIONS, $metrics)) - $select .= $pre . "sum(log_visit.visit_total_actions) as `". Piwik_Archive::INDEX_NB_ACTIONS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) - $select .= $pre . "max(log_visit.visit_total_actions) as `". Piwik_Archive::INDEX_MAX_ACTIONS ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_SUM_VISIT_LENGTH, $metrics)) - $select .= $pre . "sum(log_visit.visit_total_time) as `". Piwik_Archive::INDEX_SUM_VISIT_LENGTH ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_BOUNCE_COUNT, $metrics)) - $select .= $pre . "sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `". Piwik_Archive::INDEX_BOUNCE_COUNT ."`"; - if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS_CONVERTED, $metrics)) - $select .= $pre . "sum(case log_visit.visit_goal_converted when 1 then 1 else 0 end) as `". Piwik_Archive::INDEX_NB_VISITS_CONVERTED ."`"; - - if ($addSelect && !$addSelectGeneratesLabelColumn) { - $select .= ', '.$addSelect; - } - - $from = "log_visit"; - - $where = "log_visit.visit_last_action_time >= ? + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + + $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); + + if ($rankingQuery !== null) { + $sumColumns = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS, + Piwik_Archive::INDEX_NB_VISITS, + Piwik_Archive::INDEX_NB_ACTIONS + ); + if ($metrics) { + foreach ($sumColumns as $i => $column) { + if (!in_array($column, $metrics)) { + unset($sumColumns[$i]); + } + } + $sumColumns = array_values($sumColumns); + } + $rankingQuery->addColumn($sumColumns, 'sum'); + return $rankingQuery->execute($query['sql'], $query['bind']); + } + + return $this->db->query($query['sql'], $query['bind']); + } + + /** + * Query visits by dimension + * + * @param array|string $label Can be a string, eg. "referer_name", will be aliased as 'label' in the returned rows + * Can also be an array of strings, when the dimension spans multiple fields, + * eg. array("referer_name", "referer_keyword") + * @param string $where Additional condition for WHERE clause + * @param bool|array $metrics Set this if you want to limit the columns that are returned. + * The possible values in the array are Piwik_Archive::INDEX_*. + * @param bool|string $orderBy ORDER BY clause. This is needed in combination with $rankingQuery. + * @param Piwik_RankingQuery $rankingQuery + * A pre-configured ranking query instance that is used to limit the result. + * If set, the return value is the array returned by Piwik_RankingQuery::execute(). + * @param string $addSelect Additional SELECT clause + * @param bool $addSelectGeneratesLabelColumn + * Set to true if the $label column is generated in $addSelect. + * @return mixed + */ + public function queryVisitsByDimension($label, $where = '', $metrics = false, $orderBy = false, + $rankingQuery = null, $addSelect = false, $addSelectGeneratesLabelColumn = false) + { + if (is_array($label)) { + $groupBy = "log_visit." . implode(", log_visit.", $label); + foreach ($label as &$field) { + $field = 'log_visit.' . $field . ' AS ' . $field; + } + $select = implode(", ", $label); + } else if ($addSelectGeneratesLabelColumn) { + $select = $addSelect; + $groupBy = $label; + } else { + $select = $label . " AS label "; + $groupBy = 'label'; + } + + if (!empty($where)) { + $where = sprintf($where, "log_visit", "log_visit"); + $where = ' AND ' . $where; + } + + $pre = ", \n\t\t\t"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS, $metrics)) + $select .= $pre . "count(distinct log_visit.idvisitor) as `" . Piwik_Archive::INDEX_NB_UNIQ_VISITORS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS, $metrics)) + $select .= $pre . "count(*) as `" . Piwik_Archive::INDEX_NB_VISITS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_ACTIONS, $metrics)) + $select .= $pre . "sum(log_visit.visit_total_actions) as `" . Piwik_Archive::INDEX_NB_ACTIONS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) + $select .= $pre . "max(log_visit.visit_total_actions) as `" . Piwik_Archive::INDEX_MAX_ACTIONS . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_SUM_VISIT_LENGTH, $metrics)) + $select .= $pre . "sum(log_visit.visit_total_time) as `" . Piwik_Archive::INDEX_SUM_VISIT_LENGTH . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_BOUNCE_COUNT, $metrics)) + $select .= $pre . "sum(case log_visit.visit_total_actions when 1 then 1 when 0 then 1 else 0 end) as `" . Piwik_Archive::INDEX_BOUNCE_COUNT . "`"; + if (!$metrics || in_array(Piwik_Archive::INDEX_NB_VISITS_CONVERTED, $metrics)) + $select .= $pre . "sum(case log_visit.visit_goal_converted when 1 then 1 else 0 end) as `" . Piwik_Archive::INDEX_NB_VISITS_CONVERTED . "`"; + + if ($addSelect && !$addSelectGeneratesLabelColumn) { + $select .= ', ' . $addSelect; + } + + $from = "log_visit"; + + $where = "log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? AND log_visit.idsite = ? $where"; - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); - - if ($rankingQuery !== null) - { - $sumColumns = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS, Piwik_Archive::INDEX_NB_VISITS, - Piwik_Archive::INDEX_NB_ACTIONS, Piwik_Archive::INDEX_SUM_VISIT_LENGTH, - Piwik_Archive::INDEX_BOUNCE_COUNT, Piwik_Archive::INDEX_NB_VISITS_CONVERTED - ); - if ($metrics) - { - foreach ($sumColumns as $i => $column) - { - if (!in_array($column, $metrics)) - { - unset($sumColumns[$i]); - } - } - $sumColumns = array_values($sumColumns); - } - $rankingQuery->addColumn($sumColumns, 'sum'); - if (!$metrics || in_array(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) - { - $rankingQuery->addColumn(Piwik_Archive::INDEX_MAX_ACTIONS, 'max'); - } - return $rankingQuery->execute($query['sql'], $query['bind']); - } - - return $this->db->query($query['sql'], $query['bind']); - } - - /** - * @see queryVisitsByDimension() Similar to this function, - * but queries metrics for the requested dimensions, - * for each Goal conversion - * - * @param string|array $label - * @param string $where - * @param array $aggregateLabels - * @return - */ - public function queryConversionsByDimension($label, $where = '', $aggregateLabels = array()) - { - if(empty($label)) - { - $select = ""; - $groupBy = ""; - } - elseif(is_array($label)) - { - $groupBy = "log_conversion.".implode(", log_conversion.", $label); - foreach($label as &$field) - { - $field = 'log_conversion.'.$field.' AS '.$field ; - } - $select = implode(", ", $label) . ", "; - } - else - { - $select = $label . " AS label, "; - $groupBy = 'label'; - } - if(!empty($aggregateLabels)) - { - $select .= implode(", ", $aggregateLabels) . ", "; - } - if(!empty($where)) - { - $where = sprintf($where, "log_conversion", "log_conversion"); - $where = ' AND '.$where; - } - - $select .= self::getSqlRevenue('SUM(log_conversion.revenue_subtotal)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL ."`,". - self::getSqlRevenue('SUM(log_conversion.revenue_tax)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX ."`,". - self::getSqlRevenue('SUM(log_conversion.revenue_shipping)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING ."`,". - self::getSqlRevenue('SUM(log_conversion.revenue_discount)')." as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT ."`,". - "SUM(log_conversion.items) as `". Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS ."`, "; - - $groupBy = !empty($groupBy) ? ", $groupBy" : ''; - - $select = "$select + + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + + $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy, $groupBy); + + if ($rankingQuery !== null) { + $sumColumns = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS, Piwik_Archive::INDEX_NB_VISITS, + Piwik_Archive::INDEX_NB_ACTIONS, Piwik_Archive::INDEX_SUM_VISIT_LENGTH, + Piwik_Archive::INDEX_BOUNCE_COUNT, Piwik_Archive::INDEX_NB_VISITS_CONVERTED + ); + if ($metrics) { + foreach ($sumColumns as $i => $column) { + if (!in_array($column, $metrics)) { + unset($sumColumns[$i]); + } + } + $sumColumns = array_values($sumColumns); + } + $rankingQuery->addColumn($sumColumns, 'sum'); + if (!$metrics || in_array(Piwik_Archive::INDEX_MAX_ACTIONS, $metrics)) { + $rankingQuery->addColumn(Piwik_Archive::INDEX_MAX_ACTIONS, 'max'); + } + return $rankingQuery->execute($query['sql'], $query['bind']); + } + + return $this->db->query($query['sql'], $query['bind']); + } + + /** + * @see queryVisitsByDimension() Similar to this function, + * but queries metrics for the requested dimensions, + * for each Goal conversion + * + * @param string|array $label + * @param string $where + * @param array $aggregateLabels + * @return + */ + public function queryConversionsByDimension($label, $where = '', $aggregateLabels = array()) + { + if (empty($label)) { + $select = ""; + $groupBy = ""; + } elseif (is_array($label)) { + $groupBy = "log_conversion." . implode(", log_conversion.", $label); + foreach ($label as &$field) { + $field = 'log_conversion.' . $field . ' AS ' . $field; + } + $select = implode(", ", $label) . ", "; + } else { + $select = $label . " AS label, "; + $groupBy = 'label'; + } + if (!empty($aggregateLabels)) { + $select .= implode(", ", $aggregateLabels) . ", "; + } + if (!empty($where)) { + $where = sprintf($where, "log_conversion", "log_conversion"); + $where = ' AND ' . $where; + } + + $select .= self::getSqlRevenue('SUM(log_conversion.revenue_subtotal)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL . "`," . + self::getSqlRevenue('SUM(log_conversion.revenue_tax)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX . "`," . + self::getSqlRevenue('SUM(log_conversion.revenue_shipping)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING . "`," . + self::getSqlRevenue('SUM(log_conversion.revenue_discount)') . " as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT . "`," . + "SUM(log_conversion.items) as `" . Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS . "`, "; + + $groupBy = !empty($groupBy) ? ", $groupBy" : ''; + + $select = "$select log_conversion.idgoal, - count(*) as `". Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS ."`, - ".self::getSqlRevenue('SUM(log_conversion.revenue)')." as `". Piwik_Archive::INDEX_GOAL_REVENUE ."`, - count(distinct log_conversion.idvisit) as `". Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED."`"; - - $from = "log_conversion"; - - $where = "log_conversion.server_time >= ? + count(*) as `" . Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS . "`, + " . self::getSqlRevenue('SUM(log_conversion.revenue)') . " as `" . Piwik_Archive::INDEX_GOAL_REVENUE . "`, + count(distinct log_conversion.idvisit) as `" . Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED . "`"; + + $from = "log_conversion"; + + $where = "log_conversion.server_time >= ? AND log_conversion.server_time <= ? AND log_conversion.idsite = ? $where"; - - $groupBy = "log_conversion.idgoal $groupBy"; - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy=false, $groupBy); - + + $groupBy = "log_conversion.idgoal $groupBy"; + + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + + $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind, $orderBy = false, $groupBy); + return $this->db->query($query['sql'], $query['bind']); - } - - /** - * Returns the ecommerce items - * - * @param string $field - * @return string - */ - public function queryEcommerceItems($field) - { - $query = "SELECT + } + + /** + * Returns the ecommerce items + * + * @param string $field + * @return string + */ + public function queryEcommerceItems($field) + { + $query = "SELECT name as label, - ".self::getSqlRevenue('SUM(quantity * price)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE ."`, - ".self::getSqlRevenue('SUM(quantity)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY ."`, - ".self::getSqlRevenue('SUM(price)')." as `". Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE ."`, - count(distinct idorder) as `". Piwik_Archive::INDEX_ECOMMERCE_ORDERS."`, - count(idvisit) as `". Piwik_Archive::INDEX_NB_VISITS."`, - case idorder when '0' then ".Piwik_Tracker_GoalManager::IDGOAL_CART." else ".Piwik_Tracker_GoalManager::IDGOAL_ORDER." end as ecommerceType - FROM ".Piwik_Common::prefixTable('log_conversion_item')." - LEFT JOIN ".Piwik_Common::prefixTable('log_action')." + " . self::getSqlRevenue('SUM(quantity * price)') . " as `" . Piwik_Archive::INDEX_ECOMMERCE_ITEM_REVENUE . "`, + " . self::getSqlRevenue('SUM(quantity)') . " as `" . Piwik_Archive::INDEX_ECOMMERCE_ITEM_QUANTITY . "`, + " . self::getSqlRevenue('SUM(price)') . " as `" . Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE . "`, + count(distinct idorder) as `" . Piwik_Archive::INDEX_ECOMMERCE_ORDERS . "`, + count(idvisit) as `" . Piwik_Archive::INDEX_NB_VISITS . "`, + case idorder when '0' then " . Piwik_Tracker_GoalManager::IDGOAL_CART . " else " . Piwik_Tracker_GoalManager::IDGOAL_ORDER . " end as ecommerceType + FROM " . Piwik_Common::prefixTable('log_conversion_item') . " + LEFT JOIN " . Piwik_Common::prefixTable('log_action') . " ON $field = idaction WHERE server_time >= ? AND server_time <= ? @@ -633,433 +588,406 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing AND deleted = 0 GROUP BY ecommerceType, $field ORDER BY NULL"; - - $bind = array( $this->getStartDatetimeUTC(), - $this->getEndDatetimeUTC(), - $this->idsite + + $bind = array($this->getStartDatetimeUTC(), + $this->getEndDatetimeUTC(), + $this->idsite + ); + $query = $this->db->query($query, $bind); + return $query; + } + + /** + * @param string $field + * @return string + */ + static public function getSqlRevenue($field) + { + return "ROUND(" . $field . "," . Piwik_Tracker_GoalManager::REVENUE_PRECISION . ")"; + } + + /** + * Converts the given array to a datatable + * @param array $array + * @return Piwik_DataTable + */ + static public function getDataTableFromArray($array) + { + $table = new Piwik_DataTable(); + $table->addRowsFromArrayWithIndexLabel($array); + return $table; + } + + /** + * Output: + * array( + * LABEL => array( + * Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, + * Piwik_Archive::INDEX_NB_VISITS => 0 + * ), + * LABEL2 => array( + * [...] + * ) + * ) + * + * Helper function that returns an array with common statistics for a given database field distinct values. + * + * The statistics returned are: + * - number of unique visitors + * - number of visits + * - number of actions + * - maximum number of action for a visit + * - sum of the visits' length in sec + * - count of bouncing visits (visits with one page view) + * + * For example if $label = 'config_os' it will return the statistics for every distinct Operating systems + * The returned array will have a row per distinct operating systems, + * and a column per stat (nb of visits, max actions, etc) + * + * 'label' Piwik_Archive::INDEX_NB_UNIQ_VISITORS Piwik_Archive::INDEX_NB_VISITS etc. + * Linux 27 66 ... + * Windows XP 12 ... + * Mac OS 15 36 ... + * + * @param string $label Table log_visit field name to be use to compute common stats + * @return array + */ + public function getArrayInterestForLabel($label) + { + $query = $this->queryVisitsByDimension($label); + $interest = array(); + while ($row = $query->fetch()) { + if (!isset($interest[$row['label']])) $interest[$row['label']] = $this->getNewInterestRow(); + $this->updateInterestStats($row, $interest[$row['label']]); + } + return $interest; + } + + /** + * Generates a dataTable given a multidimensional PHP array that associates LABELS to Piwik_DataTableRows + * This is used for the "Actions" DataTable, where a line is the aggregate of all the subtables + * Example: the category /blog has 3 visits because it has /blog/index (2 visits) + /blog/about (1 visit) + * + * @param array $table + * @param array $parents + * @return Piwik_DataTable + */ + static public function generateDataTable($table, $parents = array()) + { + $dataTableToReturn = new Piwik_DataTable(); + foreach ($table as $label => $maybeDatatableRow) { + // case the aInfo is a subtable-like array + // it means that we have to go recursively and process it + // then we build the row that is an aggregate of all the children + // and we associate this row to the subtable + if (!($maybeDatatableRow instanceof Piwik_DataTable_Row)) { + array_push($parents, array($dataTableToReturn->getId(), $label)); + + $subTable = self::generateDataTable($maybeDatatableRow, $parents); + $subTable->setParents($parents); + $row = new Piwik_DataTable_Row_DataTableSummary($subTable); + $row->setColumns(array('label' => $label) + $row->getColumns()); + $row->addSubtable($subTable); + + array_pop($parents); + } // if aInfo is a simple Row we build it + else { + $row = $maybeDatatableRow; + } + + if ($row->getMetadata('issummaryrow') == true) { + $row->deleteMetadata('issummaryrow'); + $dataTableToReturn->addSummaryRow($row); + } else { + $dataTableToReturn->addRow($row); + } + } + return $dataTableToReturn; + } + + /** + * Helper function that returns the serialized DataTable of the given PHP array. + * The array must have the format of Piwik_DataTable::addRowsFromArrayWithIndexLabel() + * Example: array ( + * LABEL => array(col1 => X, col2 => Y), + * LABEL2 => array(col1 => X, col2 => Y), + * ) + * + * @param array $array at the given format + * @return array Array with one element: the serialized data table string + */ + public function getDataTableSerialized($array) + { + $table = new Piwik_DataTable(); + $table->addRowsFromArrayWithIndexLabel($array); + $toReturn = $table->getSerialized(); + return $toReturn; + } + + + /** + * Helper function that returns the multiple serialized DataTable of the given PHP array. + * The DataTable here associates a subtable to every row of the level 0 array. + * This is used for example for search engines. + * Every search engine (level 0) has a subtable containing the keywords. + * + * The $arrayLevel0 must have the format + * Example: array ( + * // Yahoo.com => array( kwd1 => stats, kwd2 => stats ) + * LABEL => array(col1 => X, col2 => Y), + * LABEL2 => array(col1 => X, col2 => Y), + * ) + * + * The $subArrayLevel1ByKey must have the format + * Example: array( + * // Yahoo.com => array( stats ) + * LABEL => #Piwik_DataTable_ForLABEL, + * LABEL2 => #Piwik_DataTable_ForLABEL2, + * ) + * + * + * @param array $arrayLevel0 + * @param array $subArrayLevel1ByKey Array of Piwik_DataTable + * @return array Array with N elements: the strings of the datatable serialized + */ + public function getDataTableWithSubtablesFromArraysIndexedByLabel($arrayLevel0, $subArrayLevel1ByKey) + { + $parentTableLevel0 = new Piwik_DataTable(); + + $tablesByLabel = array(); + foreach ($arrayLevel0 as $label => $aAllRowsForThisLabel) { + $table = new Piwik_DataTable(); + $table->addRowsFromArrayWithIndexLabel($aAllRowsForThisLabel); + $tablesByLabel[$label] = $table; + } + $parentTableLevel0->addRowsFromArrayWithIndexLabel($subArrayLevel1ByKey, $tablesByLabel); + + return $parentTableLevel0; + } + + /** + * Returns an empty row containing default values for the common stat + * + * @param bool $onlyMetricsAvailableInActionsTable + * @param bool $doNotSumVisits + * @return array + */ + public function getNewInterestRow($onlyMetricsAvailableInActionsTable = false, $doNotSumVisits = false) + { + if ($onlyMetricsAvailableInActionsTable) { + if ($doNotSumVisits) { + return array(Piwik_Archive::INDEX_NB_ACTIONS => 0); + } + return array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, + Piwik_Archive::INDEX_NB_VISITS => 0, + Piwik_Archive::INDEX_NB_ACTIONS => 0); + } + return array(Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, + Piwik_Archive::INDEX_NB_VISITS => 0, + Piwik_Archive::INDEX_NB_ACTIONS => 0, + Piwik_Archive::INDEX_MAX_ACTIONS => 0, + Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 0, + Piwik_Archive::INDEX_BOUNCE_COUNT => 0, + Piwik_Archive::INDEX_NB_VISITS_CONVERTED => 0, + ); + } + + + /** + * Returns a Piwik_DataTable_Row containing default values for common stat, + * plus a column 'label' with the value $label + * + * @param string $label + * @return Piwik_DataTable_Row + */ + public function getNewInterestRowLabeled($label) + { + return new Piwik_DataTable_Row( + array( + Piwik_DataTable_Row::COLUMNS => array('label' => $label) + + $this->getNewInterestRow() + ) + ); + } + + /** + * Adds the given row $newRowToAdd to the existing $oldRowToUpdate passed by reference + * + * The rows are php arrays Name => value + * + * @param array $newRowToAdd + * @param array $oldRowToUpdate + * @param bool $onlyMetricsAvailableInActionsTable + * @param bool $doNotSumVisits + * @return + */ + public function updateInterestStats($newRowToAdd, &$oldRowToUpdate, $onlyMetricsAvailableInActionsTable = false, $doNotSumVisits = false) + { + // Pre 1.2 format: string indexed rows are returned from the DB + // Left here for Backward compatibility with plugins doing custom SQL queries using these metrics as string + if (!isset($newRowToAdd[Piwik_Archive::INDEX_NB_VISITS])) { + if (!$doNotSumVisits) { + $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd['nb_uniq_visitors']; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd['nb_visits']; + } + $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd['nb_actions']; + if ($onlyMetricsAvailableInActionsTable) { + return; + } + $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd['max_actions'], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); + $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd['sum_visit_length']; + $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd['bounce_count']; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd['nb_visits_converted']; + return; + } + if (!$doNotSumVisits) { + $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd[Piwik_Archive::INDEX_NB_UNIQ_VISITORS]; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS]; + } + $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd[Piwik_Archive::INDEX_NB_ACTIONS]; + + // Hack for Price tracking on Ecommerce product/category pages + // The price is not summed, but AVG is taken in the SQL query + $index = Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED; + if (!empty($newRowToAdd[$index])) { + $oldRowToUpdate[$index] = (float)$newRowToAdd[$index]; + } + + if ($onlyMetricsAvailableInActionsTable) { + return; + } + $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd[Piwik_Archive::INDEX_MAX_ACTIONS], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); + $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd[Piwik_Archive::INDEX_SUM_VISIT_LENGTH]; + $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd[Piwik_Archive::INDEX_BOUNCE_COUNT]; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]; + + } + + /** + * 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 array $interestByLabel Passed by reference, it will be modified as follows: + * Input: + * array( + * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X, + * Piwik_Archive::INDEX_GOALS => array( + * idgoal1 => array( [...] ), + * idgoal2 => array( [...] ), + * ), + * [...] ), + * 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( [...] ), + * ), + * [...] ), + * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] ) + * ); + * ) + * + * @param array $interestByLabel Passed by reference, will be modified + */ + function enrichConversionsByLabelArray(&$interestByLabel) + { + foreach ($interestByLabel as $label => &$values) { + if (isset($values[Piwik_Archive::INDEX_GOALS])) { + // When per goal metrics are processed, general 'visits converted' is not meaningful because + // it could differ from the sum of each goal conversions + unset($values[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]); + $revenue = $conversions = 0; + foreach ($values[Piwik_Archive::INDEX_GOALS] as $idgoal => $goalValues) { + // Do not sum Cart revenue since it is a lost revenue + if ($idgoal >= Piwik_Tracker_GoalManager::IDGOAL_ORDER) { + $revenue += $goalValues[Piwik_Archive::INDEX_GOAL_REVENUE]; + $conversions += $goalValues[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; + } + } + $values[Piwik_Archive::INDEX_NB_CONVERSIONS] = $conversions; + + // 25.00 recorded as 25 + if (round($revenue) == $revenue) { + $revenue = round($revenue); + } + $values[Piwik_Archive::INDEX_REVENUE] = $revenue; + } + } + } + + /** + * + * @param array $interestByLabelAndSubLabel Passed by reference, will be modified + */ + function enrichConversionsByLabelArrayHasTwoLevels(&$interestByLabelAndSubLabel) + { + foreach ($interestByLabelAndSubLabel as $mainLabel => &$interestBySubLabel) { + $this->enrichConversionsByLabelArray($interestBySubLabel); + } + } + + /** + * + * @param $newRowToAdd + * @param $oldRowToUpdate + */ + function updateGoalStats($newRowToAdd, &$oldRowToUpdate) + { + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED]; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_REVENUE] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_REVENUE]; + + // Cart & Order + if (isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS])) { + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS]; + + // Order only + if (isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL])) { + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL]; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX]; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING]; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT]; + } + } + } + + /** + * + * @param $idGoal + * @return array + */ + function getNewGoalRow($idGoal) + { + if ($idGoal > Piwik_Tracker_GoalManager::IDGOAL_ORDER) { + return array(Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, + Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, + Piwik_Archive::INDEX_GOAL_REVENUE => 0, + ); + } + if ($idGoal == Piwik_Tracker_GoalManager::IDGOAL_ORDER) { + return array(Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, + Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, + Piwik_Archive::INDEX_GOAL_REVENUE => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0, + ); + } + // $row['idgoal'] == Piwik_Tracker_GoalManager::IDGOAL_CART + return array(Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, + Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, + Piwik_Archive::INDEX_GOAL_REVENUE => 0, + Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0, ); - $query = $this->db->query($query, $bind); - return $query; - } - - /** - * @param string $field - * @return string - */ - static public function getSqlRevenue($field) - { - return "ROUND(".$field.",".Piwik_Tracker_GoalManager::REVENUE_PRECISION.")"; - } - - /** - * Converts the given array to a datatable - * @param array $array - * @return Piwik_DataTable - */ - static public function getDataTableFromArray( $array ) - { - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($array); - return $table; - } - - /** - * Output: - * array( - * LABEL => array( - * Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, - * Piwik_Archive::INDEX_NB_VISITS => 0 - * ), - * LABEL2 => array( - * [...] - * ) - * ) - * - * Helper function that returns an array with common statistics for a given database field distinct values. - * - * The statistics returned are: - * - number of unique visitors - * - number of visits - * - number of actions - * - maximum number of action for a visit - * - sum of the visits' length in sec - * - count of bouncing visits (visits with one page view) - * - * For example if $label = 'config_os' it will return the statistics for every distinct Operating systems - * The returned array will have a row per distinct operating systems, - * and a column per stat (nb of visits, max actions, etc) - * - * 'label' Piwik_Archive::INDEX_NB_UNIQ_VISITORS Piwik_Archive::INDEX_NB_VISITS etc. - * Linux 27 66 ... - * Windows XP 12 ... - * Mac OS 15 36 ... - * - * @param string $label Table log_visit field name to be use to compute common stats - * @return array - */ - public function getArrayInterestForLabel($label) - { - $query = $this->queryVisitsByDimension($label); - $interest = array(); - while($row = $query->fetch()) - { - if(!isset($interest[$row['label']])) $interest[$row['label']]= $this->getNewInterestRow(); - $this->updateInterestStats( $row, $interest[$row['label']]); - } - return $interest; - } - - /** - * Generates a dataTable given a multidimensional PHP array that associates LABELS to Piwik_DataTableRows - * This is used for the "Actions" DataTable, where a line is the aggregate of all the subtables - * Example: the category /blog has 3 visits because it has /blog/index (2 visits) + /blog/about (1 visit) - * - * @param array $table - * @param array $parents - * @return Piwik_DataTable - */ - static public function generateDataTable( $table, $parents=array() ) - { - $dataTableToReturn = new Piwik_DataTable(); - foreach($table as $label => $maybeDatatableRow) - { - // case the aInfo is a subtable-like array - // it means that we have to go recursively and process it - // then we build the row that is an aggregate of all the children - // and we associate this row to the subtable - if( !($maybeDatatableRow instanceof Piwik_DataTable_Row) ) - { - array_push($parents, array($dataTableToReturn->getId(), $label)); - - $subTable = self::generateDataTable($maybeDatatableRow, $parents); - $subTable->setParents($parents); - $row = new Piwik_DataTable_Row_DataTableSummary( $subTable ); - $row->setColumns( array('label' => $label) + $row->getColumns()); - $row->addSubtable($subTable); - - array_pop($parents); - } - // if aInfo is a simple Row we build it - else - { - $row = $maybeDatatableRow; - } - - if ($row->getMetadata('issummaryrow') == true) - { - $row->deleteMetadata('issummaryrow'); - $dataTableToReturn->addSummaryRow($row); - } - else - { - $dataTableToReturn->addRow($row); - } - } - return $dataTableToReturn; - } - - /** - * Helper function that returns the serialized DataTable of the given PHP array. - * The array must have the format of Piwik_DataTable::addRowsFromArrayWithIndexLabel() - * Example: array ( - * LABEL => array(col1 => X, col2 => Y), - * LABEL2 => array(col1 => X, col2 => Y), - * ) - * - * @param array $array at the given format - * @return array Array with one element: the serialized data table string - */ - public function getDataTableSerialized( $array ) - { - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($array ); - $toReturn = $table->getSerialized(); - return $toReturn; - } - - - /** - * Helper function that returns the multiple serialized DataTable of the given PHP array. - * The DataTable here associates a subtable to every row of the level 0 array. - * This is used for example for search engines. - * Every search engine (level 0) has a subtable containing the keywords. - * - * The $arrayLevel0 must have the format - * Example: array ( - * // Yahoo.com => array( kwd1 => stats, kwd2 => stats ) - * LABEL => array(col1 => X, col2 => Y), - * LABEL2 => array(col1 => X, col2 => Y), - * ) - * - * The $subArrayLevel1ByKey must have the format - * Example: array( - * // Yahoo.com => array( stats ) - * LABEL => #Piwik_DataTable_ForLABEL, - * LABEL2 => #Piwik_DataTable_ForLABEL2, - * ) - * - * - * @param array $arrayLevel0 - * @param array $subArrayLevel1ByKey Array of Piwik_DataTable - * @return array Array with N elements: the strings of the datatable serialized - */ - public function getDataTableWithSubtablesFromArraysIndexedByLabel( $arrayLevel0, $subArrayLevel1ByKey ) - { - $parentTableLevel0 = new Piwik_DataTable(); - - $tablesByLabel = array(); - foreach($arrayLevel0 as $label => $aAllRowsForThisLabel) - { - $table = new Piwik_DataTable(); - $table->addRowsFromArrayWithIndexLabel($aAllRowsForThisLabel); - $tablesByLabel[$label] = $table; - } - $parentTableLevel0->addRowsFromArrayWithIndexLabel($subArrayLevel1ByKey, $tablesByLabel); - - return $parentTableLevel0; - } - - /** - * Returns an empty row containing default values for the common stat - * - * @param bool $onlyMetricsAvailableInActionsTable - * @param bool $doNotSumVisits - * @return array - */ - public function getNewInterestRow($onlyMetricsAvailableInActionsTable = false, $doNotSumVisits = false) - { - if($onlyMetricsAvailableInActionsTable) - { - if($doNotSumVisits) - { - return array(Piwik_Archive::INDEX_NB_ACTIONS => 0 ); - } - return array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, - Piwik_Archive::INDEX_NB_VISITS => 0, - Piwik_Archive::INDEX_NB_ACTIONS => 0 ); - } - return array( Piwik_Archive::INDEX_NB_UNIQ_VISITORS => 0, - Piwik_Archive::INDEX_NB_VISITS => 0, - Piwik_Archive::INDEX_NB_ACTIONS => 0, - Piwik_Archive::INDEX_MAX_ACTIONS => 0, - Piwik_Archive::INDEX_SUM_VISIT_LENGTH => 0, - Piwik_Archive::INDEX_BOUNCE_COUNT => 0, - Piwik_Archive::INDEX_NB_VISITS_CONVERTED=> 0, - ); - } - - - /** - * Returns a Piwik_DataTable_Row containing default values for common stat, - * plus a column 'label' with the value $label - * - * @param string $label - * @return Piwik_DataTable_Row - */ - public function getNewInterestRowLabeled( $label ) - { - return new Piwik_DataTable_Row( - array( - Piwik_DataTable_Row::COLUMNS => array( 'label' => $label) - + $this->getNewInterestRow() - ) - ); - } - - /** - * Adds the given row $newRowToAdd to the existing $oldRowToUpdate passed by reference - * - * The rows are php arrays Name => value - * - * @param array $newRowToAdd - * @param array $oldRowToUpdate - * @param bool $onlyMetricsAvailableInActionsTable - * @param bool $doNotSumVisits - * @return - */ - public function updateInterestStats( $newRowToAdd, &$oldRowToUpdate, $onlyMetricsAvailableInActionsTable = false, $doNotSumVisits = false) - { - // Pre 1.2 format: string indexed rows are returned from the DB - // Left here for Backward compatibility with plugins doing custom SQL queries using these metrics as string - if(!isset($newRowToAdd[Piwik_Archive::INDEX_NB_VISITS])) - { - if(!$doNotSumVisits) - { - $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd['nb_uniq_visitors']; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd['nb_visits']; - } - $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd['nb_actions']; - if($onlyMetricsAvailableInActionsTable) - { - return; - } - $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd['max_actions'], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); - $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd['sum_visit_length']; - $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd['bounce_count']; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd['nb_visits_converted']; - return; - } - if(!$doNotSumVisits) - { - $oldRowToUpdate[Piwik_Archive::INDEX_NB_UNIQ_VISITORS] += $newRowToAdd[Piwik_Archive::INDEX_NB_UNIQ_VISITORS]; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS]; - } - $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd[Piwik_Archive::INDEX_NB_ACTIONS]; - - // Hack for Price tracking on Ecommerce product/category pages - // The price is not summed, but AVG is taken in the SQL query - $index = Piwik_Archive::INDEX_ECOMMERCE_ITEM_PRICE_VIEWED; - if(!empty($newRowToAdd[$index])) - { - $oldRowToUpdate[$index] = (float)$newRowToAdd[$index]; - } - - if($onlyMetricsAvailableInActionsTable) - { - return; - } - $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS] = (float)max($newRowToAdd[Piwik_Archive::INDEX_MAX_ACTIONS], $oldRowToUpdate[Piwik_Archive::INDEX_MAX_ACTIONS]); - $oldRowToUpdate[Piwik_Archive::INDEX_SUM_VISIT_LENGTH] += $newRowToAdd[Piwik_Archive::INDEX_SUM_VISIT_LENGTH]; - $oldRowToUpdate[Piwik_Archive::INDEX_BOUNCE_COUNT] += $newRowToAdd[Piwik_Archive::INDEX_BOUNCE_COUNT]; - $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]; - - } - - /** - * 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 array $interestByLabel Passed by reference, it will be modified as follows: - * Input: - * array( - * LABEL => array( Piwik_Archive::INDEX_NB_VISITS => X, - * Piwik_Archive::INDEX_GOALS => array( - * idgoal1 => array( [...] ), - * idgoal2 => array( [...] ), - * ), - * [...] ), - * 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( [...] ), - * ), - * [...] ), - * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] ) - * ); - * ) - * - * @param array $interestByLabel Passed by reference, will be modified - */ - function enrichConversionsByLabelArray(&$interestByLabel) - { - foreach($interestByLabel as $label => &$values) - { - if(isset($values[Piwik_Archive::INDEX_GOALS])) - { - // When per goal metrics are processed, general 'visits converted' is not meaningful because - // it could differ from the sum of each goal conversions - unset($values[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]); - $revenue = $conversions = 0; - foreach($values[Piwik_Archive::INDEX_GOALS] as $idgoal => $goalValues) - { - // Do not sum Cart revenue since it is a lost revenue - if($idgoal >= Piwik_Tracker_GoalManager::IDGOAL_ORDER) - { - $revenue += $goalValues[Piwik_Archive::INDEX_GOAL_REVENUE]; - $conversions += $goalValues[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; - } - } - $values[Piwik_Archive::INDEX_NB_CONVERSIONS] = $conversions; - - // 25.00 recorded as 25 - if(round($revenue) == $revenue) - { - $revenue = round($revenue); - } - $values[Piwik_Archive::INDEX_REVENUE] = $revenue; - } - } - } - - /** - * - * @param array $interestByLabelAndSubLabel Passed by reference, will be modified - */ - function enrichConversionsByLabelArrayHasTwoLevels(&$interestByLabelAndSubLabel) - { - foreach($interestByLabelAndSubLabel as $mainLabel => &$interestBySubLabel) - { - $this->enrichConversionsByLabelArray($interestBySubLabel); - } - } - - /** - * - * @param $newRowToAdd - * @param $oldRowToUpdate - */ - function updateGoalStats($newRowToAdd, &$oldRowToUpdate) - { - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_REVENUE] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_REVENUE]; - - // Cart & Order - if(isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS])) - { - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS]; - - // Order only - if(isset($oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL])) - { - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING]; - $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT] += $newRowToAdd[Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT]; - } - } - } - - /** - * - * @param $idGoal - * @return array - */ - function getNewGoalRow($idGoal) - { - if($idGoal > Piwik_Tracker_GoalManager::IDGOAL_ORDER) - { - return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, - Piwik_Archive::INDEX_GOAL_REVENUE => 0, - ); - } - if($idGoal == Piwik_Tracker_GoalManager::IDGOAL_ORDER) - { - return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, - Piwik_Archive::INDEX_GOAL_REVENUE => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SUBTOTAL => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_TAX => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_SHIPPING => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_REVENUE_DISCOUNT => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0, - ); - } - // $row['idgoal'] == Piwik_Tracker_GoalManager::IDGOAL_CART - return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, - Piwik_Archive::INDEX_GOAL_NB_VISITS_CONVERTED => 0, - Piwik_Archive::INDEX_GOAL_REVENUE => 0, - Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS => 0, - ); - } + } } diff --git a/core/ArchiveProcessing/Period.php b/core/ArchiveProcessing/Period.php index c6d38a512c..0ca9ce171b 100644 --- a/core/ArchiveProcessing/Period.php +++ b/core/ArchiveProcessing/Period.php @@ -1,472 +1,444 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Handles the archiving process for a period - * + * * This class provides generic methods to archive data for a period (week / month / year). - * + * * These methods are called by the plugins that do the logic of archiving their own data. \ * They hook on the event 'ArchiveProcessing_Period.compute' - * + * * @package Piwik * @subpackage Piwik_ArchiveProcessing */ class Piwik_ArchiveProcessing_Period extends Piwik_ArchiveProcessing { - /** - * Array of (column name before => column name renamed) of the columns for which sum operation is invalid. - * The summed value is not accurate and these columns will be renamed accordingly. + /** + * Array of (column name before => column name renamed) of the columns for which sum operation is invalid. + * The summed value is not accurate and these columns will be renamed accordingly. * @var array - */ - static public $invalidSummedColumnNameToRenamedName = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS - ); - - /** - * @var Piwik_Archive_Single[] - */ - public $archives = array(); - - /** - * Sums all values for the given field names $aNames over the period - * See @archiveNumericValuesGeneral for more information - * - * @param string|array $aNames - * @return array - */ - public function archiveNumericValuesSum( $aNames ) - { - return $this->archiveNumericValuesGeneral($aNames, 'sum'); - } - - /** - * Get the maximum value for all values for the given field names $aNames over the period - * See @archiveNumericValuesGeneral for more information - * - * @param string|array $aNames - * @return array - */ - public function archiveNumericValuesMax( $aNames ) - { - return $this->archiveNumericValuesGeneral($aNames, 'max'); - } - - /** - * Given a list of fields names, the method will fetch all their values over the period, and archive them using the given operation. - * - * For example if $operationToApply = 'sum' and $aNames = array('nb_visits', 'sum_time_visit') - * it will sum all values of nb_visits for the period (for example give the number of visits for the month by summing the visits of every day) - * - * @param array|string $aNames Array of strings or string containg the field names to select - * @param string $operationToApply Available operations = sum, max, min - * @throws Exception - * @return array - */ - private function archiveNumericValuesGeneral($aNames, $operationToApply) - { - $this->loadSubPeriods(); - if(!is_array($aNames)) - { - $aNames = array($aNames); - } - - // fetch the numeric values and apply the operation on them - $results = array(); - foreach($this->archives as $id => $archive) - { - foreach($aNames as $name) - { - if(!isset($results[$name])) - { - $results[$name] = 0; - } - if($name == 'nb_uniq_visitors') continue; - - $valueToSum = $archive->getNumeric($name); - - if($valueToSum !== false) - { - switch ($operationToApply) { - case 'sum': - $results[$name] += $valueToSum; - break; - case 'max': - $results[$name] = max($results[$name], $valueToSum); - break; - case 'min': - $results[$name] = min($results[$name], $valueToSum); - break; - default: - throw new Exception("Operation not applicable."); - break; - } - } - } - } - - if(!Piwik::isUniqueVisitorsEnabled($this->period->getLabel())) - { - unset($results['nb_uniq_visitors']); - } - - foreach($results as $name => $value) - { - if($name == 'nb_uniq_visitors') - { - $value = (float) $this->computeNbUniqVisitors(); - } - $this->insertRecord($name, $value); - } - - // if asked for only one field to sum - if(count($results) == 1) - { - return $results[$name]; - } - - // returns the array of records once summed - return $results; - } - - /** - * This method will compute the sum of DataTables over the period for the given fields $aRecordName. - * The resulting DataTable will be then added to queue of data to be recorded in the database. - * It will usually be called in a plugin that listens to the hook 'ArchiveProcessing_Period.compute' - * - * For example if $aRecordName = 'UserCountry_country' the method will select all UserCountry_country DataTable for the period - * (eg. the 31 dataTable of the last month), sum them, then record it in the DB - * - * - * This method works on recursive dataTable. For example for the 'Actions' it will select all subtables of all dataTable of all the sub periods - * and get the sum. - * - * It returns an array that gives information about the "final" DataTable. The array gives for every field name, the number of rows in the - * final DataTable (ie. the number of distinct LABEL over the period) (eg. the number of distinct keywords over the last month) - * - * @param string|array $aRecordName Field name(s) of DataTable to select so we can get the sum - * @param array $invalidSummedColumnNameToRenamedName (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 $maximumRowsInDataTableLevelZero Max row count of parent datatable to archive - * @param int $maximumRowsInSubDataTable Max row count of children datatable(s) to archive - * @param string $columnToSortByBeforeTruncation 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, - * ) - */ - public function archiveDataTable( $aRecordName, - $invalidSummedColumnNameToRenamedName = null, - $maximumRowsInDataTableLevelZero = null, - $maximumRowsInSubDataTable = null, - $columnToSortByBeforeTruncation = null ) - { - // We clean up below all tables created during this function call (and recursive calls) - $latestUsedTableId = Piwik_DataTable_Manager::getInstance()->getMostRecentTableId(); - - $this->loadSubPeriods(); - if(!is_array($aRecordName)) - { - $aRecordName = array($aRecordName); - } - - $nameToCount = array(); - foreach($aRecordName as $recordName) - { - $table = $this->getRecordDataTableSum($recordName, $invalidSummedColumnNameToRenamedName); - - $nameToCount[$recordName]['level0'] = $table->getRowsCount(); - $nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive(); - - $blob = $table->getSerialized( $maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation ); - destroy($table); - $this->insertBlobRecord($recordName, $blob); - } - Piwik_DataTable_Manager::getInstance()->deleteAll( $latestUsedTableId ); - - return $nameToCount; - } - - /** - * This method selects all DataTables that have the name $name over the period. - * It calls the appropriate methods that sum all these tables together. - * The resulting DataTable is returned. - * - * @param string $name - * @param array $invalidSummedColumnNameToRenamedName columns in the array (old name, new name) to be renamed as the sum operation is not valid on them (eg. nb_uniq_visitors->sum_daily_nb_uniq_visitors) - * @return Piwik_DataTable - */ - protected function getRecordDataTableSum( $name, $invalidSummedColumnNameToRenamedName ) - { - $table = new Piwik_DataTable(); - foreach($this->archives as $archive) - { - $archive->preFetchBlob($name); - $datatableToSum = $archive->getDataTable($name); - $archive->loadSubDataTables($name, $datatableToSum); - $table->addDataTable($datatableToSum); - $archive->freeBlob($name); - } - - if(is_null($invalidSummedColumnNameToRenamedName)) - { - $invalidSummedColumnNameToRenamedName = self::$invalidSummedColumnNameToRenamedName; - } - foreach($invalidSummedColumnNameToRenamedName as $oldName => $newName) - { - $table->renameColumn($oldName, $newName); - } - return $table; - } - - protected function initCompute() - { - parent::initCompute(); - } - - /** - * Returns the ID of the archived subperiods. - * - * @return array Array of the idArchive of the subperiods - */ - protected function loadSubperiodsArchive() - { - $periods = array(); - - // we first compute every subperiod of the archive - foreach($this->period->getSubperiods() as $period) - { - $archivePeriod = new Piwik_Archive_Single(); - $archivePeriod->setSite( $this->site ); - $archivePeriod->setPeriod( $period ); - $archivePeriod->setSegment( $this->getSegment() ); - $archivePeriod->setRequestedReport($this->getRequestedReport()); - - $periods[] = $archivePeriod; - } - return $periods; - } - - /** - * Main method to process logs for a period. - * The only logic done here is computing the number of visits, actions, etc. - * - * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Period.compute'. - * See some of the plugins for an example. - */ - protected function compute() - { - if(!$this->isThereSomeVisits()) - { - return; - } - Piwik_PostEvent('ArchiveProcessing_Period.compute', $this); - } - - protected function loadSubPeriods() - { - if(empty($this->archives)) - { - $this->archives = $this->loadSubperiodsArchive(); - } - } - - /** - * - * @see Piwik_ArchiveProcessing_Day::isThereSomeVisits() - * @return bool|null - */ - public function isThereSomeVisits() - { - if(!is_null($this->isThereSomeVisits)) - { - return $this->isThereSomeVisits; - } - - $this->loadSubPeriods(); - if(self::getPluginBeingProcessed($this->getRequestedReport()) == 'VisitsSummary' - || $this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period) - ) - { - $toSum = self::getCoreMetrics(); - $record = $this->archiveNumericValuesSum($toSum); - $this->archiveNumericValuesMax( 'max_actions' ); - - $nbVisitsConverted = $record['nb_visits_converted']; - $nbVisits = $record['nb_visits']; - } - else - { - $archive = new Piwik_Archive_Single(); - $archive->setSite( $this->site ); - $archive->setPeriod( $this->period ); - $archive->setSegment( $this->getSegment() ); - - $nbVisits = $archive->getNumeric('nb_visits'); - $nbVisitsConverted = 0; - if($nbVisits > 0) - { - $nbVisitsConverted = $archive->getNumeric('nb_visits_converted'); - } - } - - $this->setNumberOfVisits($nbVisits); - $this->setNumberOfVisitsConverted($nbVisitsConverted); - $this->isThereSomeVisits = ($nbVisits > 0); - return $this->isThereSomeVisits; - } - - /** - * Processes number of unique visitors for the given period - * - * This is the only metric we process from the logs directly, - * since unique visitors cannot be summed like other metrics. - * - * @return int - */ - protected function computeNbUniqVisitors() - { - $select = "count(distinct log_visit.idvisitor) as nb_uniq_visitors"; - $from = "log_visit"; - $where = "log_visit.visit_last_action_time >= ? + */ + static public $invalidSummedColumnNameToRenamedName = array( + Piwik_Archive::INDEX_NB_UNIQ_VISITORS => Piwik_Archive::INDEX_SUM_DAILY_NB_UNIQ_VISITORS + ); + + /** + * @var Piwik_Archive_Single[] + */ + public $archives = array(); + + /** + * Sums all values for the given field names $aNames over the period + * See @archiveNumericValuesGeneral for more information + * + * @param string|array $aNames + * @return array + */ + public function archiveNumericValuesSum($aNames) + { + return $this->archiveNumericValuesGeneral($aNames, 'sum'); + } + + /** + * Get the maximum value for all values for the given field names $aNames over the period + * See @archiveNumericValuesGeneral for more information + * + * @param string|array $aNames + * @return array + */ + public function archiveNumericValuesMax($aNames) + { + return $this->archiveNumericValuesGeneral($aNames, 'max'); + } + + /** + * Given a list of fields names, the method will fetch all their values over the period, and archive them using the given operation. + * + * For example if $operationToApply = 'sum' and $aNames = array('nb_visits', 'sum_time_visit') + * it will sum all values of nb_visits for the period (for example give the number of visits for the month by summing the visits of every day) + * + * @param array|string $aNames Array of strings or string containg the field names to select + * @param string $operationToApply Available operations = sum, max, min + * @throws Exception + * @return array + */ + private function archiveNumericValuesGeneral($aNames, $operationToApply) + { + $this->loadSubPeriods(); + if (!is_array($aNames)) { + $aNames = array($aNames); + } + + // fetch the numeric values and apply the operation on them + $results = array(); + foreach ($this->archives as $id => $archive) { + foreach ($aNames as $name) { + if (!isset($results[$name])) { + $results[$name] = 0; + } + if ($name == 'nb_uniq_visitors') continue; + + $valueToSum = $archive->getNumeric($name); + + if ($valueToSum !== false) { + switch ($operationToApply) { + case 'sum': + $results[$name] += $valueToSum; + break; + case 'max': + $results[$name] = max($results[$name], $valueToSum); + break; + case 'min': + $results[$name] = min($results[$name], $valueToSum); + break; + default: + throw new Exception("Operation not applicable."); + break; + } + } + } + } + + if (!Piwik::isUniqueVisitorsEnabled($this->period->getLabel())) { + unset($results['nb_uniq_visitors']); + } + + foreach ($results as $name => $value) { + if ($name == 'nb_uniq_visitors') { + $value = (float)$this->computeNbUniqVisitors(); + } + $this->insertRecord($name, $value); + } + + // if asked for only one field to sum + if (count($results) == 1) { + return $results[$name]; + } + + // returns the array of records once summed + return $results; + } + + /** + * This method will compute the sum of DataTables over the period for the given fields $aRecordName. + * The resulting DataTable will be then added to queue of data to be recorded in the database. + * It will usually be called in a plugin that listens to the hook 'ArchiveProcessing_Period.compute' + * + * For example if $aRecordName = 'UserCountry_country' the method will select all UserCountry_country DataTable for the period + * (eg. the 31 dataTable of the last month), sum them, then record it in the DB + * + * + * This method works on recursive dataTable. For example for the 'Actions' it will select all subtables of all dataTable of all the sub periods + * and get the sum. + * + * It returns an array that gives information about the "final" DataTable. The array gives for every field name, the number of rows in the + * final DataTable (ie. the number of distinct LABEL over the period) (eg. the number of distinct keywords over the last month) + * + * @param string|array $aRecordName Field name(s) of DataTable to select so we can get the sum + * @param array $invalidSummedColumnNameToRenamedName (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 $maximumRowsInDataTableLevelZero Max row count of parent datatable to archive + * @param int $maximumRowsInSubDataTable Max row count of children datatable(s) to archive + * @param string $columnToSortByBeforeTruncation 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, + * ) + */ + public function archiveDataTable($aRecordName, + $invalidSummedColumnNameToRenamedName = null, + $maximumRowsInDataTableLevelZero = null, + $maximumRowsInSubDataTable = null, + $columnToSortByBeforeTruncation = null) + { + // We clean up below all tables created during this function call (and recursive calls) + $latestUsedTableId = Piwik_DataTable_Manager::getInstance()->getMostRecentTableId(); + + $this->loadSubPeriods(); + if (!is_array($aRecordName)) { + $aRecordName = array($aRecordName); + } + + $nameToCount = array(); + foreach ($aRecordName as $recordName) { + $table = $this->getRecordDataTableSum($recordName, $invalidSummedColumnNameToRenamedName); + + $nameToCount[$recordName]['level0'] = $table->getRowsCount(); + $nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive(); + + $blob = $table->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation); + destroy($table); + $this->insertBlobRecord($recordName, $blob); + } + Piwik_DataTable_Manager::getInstance()->deleteAll($latestUsedTableId); + + return $nameToCount; + } + + /** + * This method selects all DataTables that have the name $name over the period. + * It calls the appropriate methods that sum all these tables together. + * The resulting DataTable is returned. + * + * @param string $name + * @param array $invalidSummedColumnNameToRenamedName columns in the array (old name, new name) to be renamed as the sum operation is not valid on them (eg. nb_uniq_visitors->sum_daily_nb_uniq_visitors) + * @return Piwik_DataTable + */ + protected function getRecordDataTableSum($name, $invalidSummedColumnNameToRenamedName) + { + $table = new Piwik_DataTable(); + foreach ($this->archives as $archive) { + $archive->preFetchBlob($name); + $datatableToSum = $archive->getDataTable($name); + $archive->loadSubDataTables($name, $datatableToSum); + $table->addDataTable($datatableToSum); + $archive->freeBlob($name); + } + + if (is_null($invalidSummedColumnNameToRenamedName)) { + $invalidSummedColumnNameToRenamedName = self::$invalidSummedColumnNameToRenamedName; + } + foreach ($invalidSummedColumnNameToRenamedName as $oldName => $newName) { + $table->renameColumn($oldName, $newName); + } + return $table; + } + + protected function initCompute() + { + parent::initCompute(); + } + + /** + * Returns the ID of the archived subperiods. + * + * @return array Array of the idArchive of the subperiods + */ + protected function loadSubperiodsArchive() + { + $periods = array(); + + // we first compute every subperiod of the archive + foreach ($this->period->getSubperiods() as $period) { + $archivePeriod = new Piwik_Archive_Single(); + $archivePeriod->setSite($this->site); + $archivePeriod->setPeriod($period); + $archivePeriod->setSegment($this->getSegment()); + $archivePeriod->setRequestedReport($this->getRequestedReport()); + + $periods[] = $archivePeriod; + } + return $periods; + } + + /** + * Main method to process logs for a period. + * The only logic done here is computing the number of visits, actions, etc. + * + * All the other reports are computed inside plugins listening to the event 'ArchiveProcessing_Period.compute'. + * See some of the plugins for an example. + */ + protected function compute() + { + if (!$this->isThereSomeVisits()) { + return; + } + Piwik_PostEvent('ArchiveProcessing_Period.compute', $this); + } + + protected function loadSubPeriods() + { + if (empty($this->archives)) { + $this->archives = $this->loadSubperiodsArchive(); + } + } + + /** + * + * @see Piwik_ArchiveProcessing_Day::isThereSomeVisits() + * @return bool|null + */ + public function isThereSomeVisits() + { + if (!is_null($this->isThereSomeVisits)) { + return $this->isThereSomeVisits; + } + + $this->loadSubPeriods(); + if (self::getPluginBeingProcessed($this->getRequestedReport()) == 'VisitsSummary' + || $this->shouldProcessReportsAllPlugins($this->getSegment(), $this->period) + ) { + $toSum = self::getCoreMetrics(); + $record = $this->archiveNumericValuesSum($toSum); + $this->archiveNumericValuesMax('max_actions'); + + $nbVisitsConverted = $record['nb_visits_converted']; + $nbVisits = $record['nb_visits']; + } else { + $archive = new Piwik_Archive_Single(); + $archive->setSite($this->site); + $archive->setPeriod($this->period); + $archive->setSegment($this->getSegment()); + + $nbVisits = $archive->getNumeric('nb_visits'); + $nbVisitsConverted = 0; + if ($nbVisits > 0) { + $nbVisitsConverted = $archive->getNumeric('nb_visits_converted'); + } + } + + $this->setNumberOfVisits($nbVisits); + $this->setNumberOfVisitsConverted($nbVisitsConverted); + $this->isThereSomeVisits = ($nbVisits > 0); + return $this->isThereSomeVisits; + } + + /** + * Processes number of unique visitors for the given period + * + * This is the only metric we process from the logs directly, + * since unique visitors cannot be summed like other metrics. + * + * @return int + */ + protected function computeNbUniqVisitors() + { + $select = "count(distinct log_visit.idvisitor) as nb_uniq_visitors"; + $from = "log_visit"; + $where = "log_visit.visit_last_action_time >= ? AND log_visit.visit_last_action_time <= ? AND log_visit.idsite = ?"; - - $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); - - $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind); - - return Zend_Registry::get('db')->fetchOne($query['sql'], $query['bind']); - } - - /** - * Called at the end of the archiving process. - * Does some cleaning job in the database. - */ - protected function postCompute() - { - parent::postCompute(); - - $numericTable = $this->tableArchiveNumeric->getTableName(); - self::doPurgeOutdatedArchives($numericTable, $this->isArchiveTemporary()); - - if(!isset($this->archives)) - { - return; - } - foreach($this->archives as $archive) - { - destroy($archive); - } - $this->archives = array(); - } - - const FLAG_TABLE_PURGED = 'lastPurge_'; - - // Used to disable Purge Outdated reports during test data setup - static public $enablePurgeOutdated = true; - - /** - * Given a monthly archive table, will delete all reports that are now outdated, - * or reports that ended with an error - */ - static public function doPurgeOutdatedArchives($numericTable) - { - if(!self::$enablePurgeOutdated) { - return; - } - $blobTable = str_replace("numeric", "blob", $numericTable); - $key = self::FLAG_TABLE_PURGED . $blobTable; - $timestamp = Piwik_GetOption($key); - - // we shall purge temporary archives after their timeout is finished, plus an extra 6 hours - // in case archiving is disabled or run once a day, we give it this extra time to run - // and re-process more recent records... - // TODO: Instead of hardcoding 6 we should put the actual number of hours between 2 archiving runs - $temporaryArchivingTimeout = self::getTodayArchiveTimeToLive(); - $purgeEveryNSeconds = max($temporaryArchivingTimeout, 6 * 3600); - - // we only delete archives if we are able to process them, otherwise, the browser might process reports - // when &segment= is specified (or custom date range) and would below, delete temporary archives that the - // browser is not able to process until next cron run (which could be more than 1 hour away) - if(self::isRequestAuthorizedToArchive() - && (!$timestamp - || $timestamp < time() - $purgeEveryNSeconds)) - { - Piwik_SetOption($key, time()); - - // If Browser Archiving is enabled, it is likely there are many more temporary archives - // We delete more often which is safe, since reports are re-processed on demand - if(self::isBrowserTriggerArchivingEnabled()) - { - $purgeArchivesOlderThan = Piwik_Date::factory(time() - 2 * $temporaryArchivingTimeout)->getDateTime(); - } - // If archive.php via Cron is building the reports, we should keep all temporary reports from today - else - { - $purgeArchivesOlderThan = Piwik_Date::factory('today')->getDateTime(); - } - $result = Piwik_FetchAll(" + + $bind = array($this->getStartDatetimeUTC(), $this->getEndDatetimeUTC(), $this->idsite); + + $query = $this->getSegment()->getSelectQuery($select, $from, $where, $bind); + + return Zend_Registry::get('db')->fetchOne($query['sql'], $query['bind']); + } + + /** + * Called at the end of the archiving process. + * Does some cleaning job in the database. + */ + protected function postCompute() + { + parent::postCompute(); + + $numericTable = $this->tableArchiveNumeric->getTableName(); + self::doPurgeOutdatedArchives($numericTable, $this->isArchiveTemporary()); + + if (!isset($this->archives)) { + return; + } + foreach ($this->archives as $archive) { + destroy($archive); + } + $this->archives = array(); + } + + const FLAG_TABLE_PURGED = 'lastPurge_'; + + // Used to disable Purge Outdated reports during test data setup + static public $enablePurgeOutdated = true; + + /** + * Given a monthly archive table, will delete all reports that are now outdated, + * or reports that ended with an error + */ + static public function doPurgeOutdatedArchives($numericTable) + { + if (!self::$enablePurgeOutdated) { + return; + } + $blobTable = str_replace("numeric", "blob", $numericTable); + $key = self::FLAG_TABLE_PURGED . $blobTable; + $timestamp = Piwik_GetOption($key); + + // we shall purge temporary archives after their timeout is finished, plus an extra 6 hours + // in case archiving is disabled or run once a day, we give it this extra time to run + // and re-process more recent records... + // TODO: Instead of hardcoding 6 we should put the actual number of hours between 2 archiving runs + $temporaryArchivingTimeout = self::getTodayArchiveTimeToLive(); + $purgeEveryNSeconds = max($temporaryArchivingTimeout, 6 * 3600); + + // we only delete archives if we are able to process them, otherwise, the browser might process reports + // when &segment= is specified (or custom date range) and would below, delete temporary archives that the + // browser is not able to process until next cron run (which could be more than 1 hour away) + if (self::isRequestAuthorizedToArchive() + && (!$timestamp + || $timestamp < time() - $purgeEveryNSeconds) + ) { + Piwik_SetOption($key, time()); + + // If Browser Archiving is enabled, it is likely there are many more temporary archives + // We delete more often which is safe, since reports are re-processed on demand + if (self::isBrowserTriggerArchivingEnabled()) { + $purgeArchivesOlderThan = Piwik_Date::factory(time() - 2 * $temporaryArchivingTimeout)->getDateTime(); + } // If archive.php via Cron is building the reports, we should keep all temporary reports from today + else { + $purgeArchivesOlderThan = Piwik_Date::factory('today')->getDateTime(); + } + $result = Piwik_FetchAll(" SELECT idarchive FROM $numericTable WHERE name LIKE 'done%' - AND (( value = ". Piwik_ArchiveProcessing::DONE_OK_TEMPORARY ." + AND (( value = " . Piwik_ArchiveProcessing::DONE_OK_TEMPORARY . " AND ts_archived < ?) - OR value = ". Piwik_ArchiveProcessing::DONE_ERROR .")", - array($purgeArchivesOlderThan) - ); - - $idArchivesToDelete = array(); - if(!empty($result)) - { - foreach($result as $row) { - $idArchivesToDelete[] = $row['idarchive']; - } - $query = "DELETE + OR value = " . Piwik_ArchiveProcessing::DONE_ERROR . ")", + array($purgeArchivesOlderThan) + ); + + $idArchivesToDelete = array(); + if (!empty($result)) { + foreach ($result as $row) { + $idArchivesToDelete[] = $row['idarchive']; + } + $query = "DELETE FROM %s - WHERE idarchive IN (".implode(',',$idArchivesToDelete).") + WHERE idarchive IN (" . implode(',', $idArchivesToDelete) . ") "; - - Piwik_Query(sprintf($query, $numericTable)); - - // Individual blob tables could be missing - try { - Piwik_Query(sprintf($query, $blobTable)); - } catch(Exception $e) { } - } - Piwik::log("Purging temporary archives: done [ purged archives older than $purgeArchivesOlderThan from $blobTable and $numericTable ] [Deleted IDs: ". implode(',',$idArchivesToDelete)."]"); - - // Deleting "Custom Date Range" reports after 1 day, since they can be re-processed - // and would take up unecessary space - $yesterday = Piwik_Date::factory('yesterday')->getDateTime(); - $query = "DELETE + + Piwik_Query(sprintf($query, $numericTable)); + + // Individual blob tables could be missing + try { + Piwik_Query(sprintf($query, $blobTable)); + } catch (Exception $e) { + } + } + Piwik::log("Purging temporary archives: done [ purged archives older than $purgeArchivesOlderThan from $blobTable and $numericTable ] [Deleted IDs: " . implode(',', $idArchivesToDelete) . "]"); + + // Deleting "Custom Date Range" reports after 1 day, since they can be re-processed + // and would take up unecessary space + $yesterday = Piwik_Date::factory('yesterday')->getDateTime(); + $query = "DELETE FROM %s WHERE period = ? AND ts_archived < ?"; - $bind = array(Piwik::$idPeriods['range'], $yesterday); - Piwik::log("Purging Custom Range archives: done [ purged archives older than $yesterday from $blobTable and $numericTable ]"); - - Piwik_Query(sprintf($query, $numericTable), $bind); - - // Individual blob tables could be missing - try { - Piwik_Query(sprintf($query, $blobTable), $bind); - } catch(Exception $e) { } - - // these tables will be OPTIMIZEd daily in a scheduled task, to claim lost space - } - else - { - Piwik::log("Purging temporary archives: skipped."); - } - } + $bind = array(Piwik::$idPeriods['range'], $yesterday); + Piwik::log("Purging Custom Range archives: done [ purged archives older than $yesterday from $blobTable and $numericTable ]"); + + Piwik_Query(sprintf($query, $numericTable), $bind); + + // Individual blob tables could be missing + try { + Piwik_Query(sprintf($query, $blobTable), $bind); + } catch (Exception $e) { + } + + // these tables will be OPTIMIZEd daily in a scheduled task, to claim lost space + } else { + Piwik::log("Purging temporary archives: skipped."); + } + } }
\ No newline at end of file diff --git a/core/AssetManager.php b/core/AssetManager.php index 3ef46bf0d3..1091e518f0 100644 --- a/core/AssetManager.php +++ b/core/AssetManager.php @@ -24,7 +24,7 @@ require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php'; * JavaScript and CSS files. * * It performs the following actions: - * - Identifies required assets + * - Identifies required assets * - Includes assets in the rendered HTML page * - Manages asset merging and minifying * - Manages server-side cache @@ -39,92 +39,89 @@ require_once PIWIK_INCLUDE_PATH . '/libs/jsmin/jsmin.php'; */ class Piwik_AssetManager { - const MERGED_CSS_FILE = "asset_manager_global_css.css"; - const MERGED_JS_FILE = "asset_manager_global_js.js"; - const CSS_IMPORT_EVENT = "AssetManager.getCssFiles"; - const JS_IMPORT_EVENT = "AssetManager.getJsFiles"; - const MERGED_FILE_DIR = "tmp/assets/"; - const CSS_IMPORT_DIRECTIVE = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />\n"; - const JS_IMPORT_DIRECTIVE = "<script type=\"text/javascript\" src=\"%s\"></script>\n"; - const GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss"; - const GET_JS_MODULE_ACTION = "index.php?module=Proxy&action=getJs"; - const MINIFIED_JS_RATIO = 100; - - /** - * Returns CSS file inclusion directive(s) using the markup <link> - * - * @return string - */ - public static function getCssAssets() - { - if ( self::getDisableMergedAssets() ) - { - // Individual includes mode - self::removeMergedAsset(self::MERGED_CSS_FILE); - return self::getIndividualCssIncludes(); - } - return sprintf ( self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION ); - } - - /** - * Returns JS file inclusion directive(s) using the markup <script> - * - * @return string - */ - public static function getJsAssets() - { - if ( self::getDisableMergedAssets() ) - { - // Individual includes mode - self::removeMergedAsset(self::MERGED_JS_FILE); - return self::getIndividualJsIncludes(); - } - return sprintf ( self::JS_IMPORT_DIRECTIVE, self::GET_JS_MODULE_ACTION ); - } - - /** - * Generate the merged css file. - * - * @throws Exception if a file can not be opened in write mode - */ - private static function generateMergedCssFile() - { - $mergedContent = ""; - - // absolute path to doc root - $rootDirectory = realpath(PIWIK_DOCUMENT_ROOT); - if($rootDirectory != '/' && substr_compare($rootDirectory, '/', -1)) - { - $rootDirectory .= '/'; - } - $rootDirectoryLen = strlen($rootDirectory); - - // Loop through each css file - $files = self::getCssFiles(); - foreach ($files as $file) { - - self::validateCssFile ( $file ); - - $fileLocation = self::getAbsoluteLocation($file); - $content = file_get_contents ($fileLocation); - - // Rewrite css url directives - // - assumes these are all relative paths - // - rewrite windows directory separator \\ to / - $baseDirectory = dirname($file); - $content = preg_replace_callback( - "/(url\(['\"]?)([^'\")]*)/", - create_function( - '$matches', - "return \$matches[1] . str_replace('\\\\', '/', substr(realpath(PIWIK_DOCUMENT_ROOT . '/$baseDirectory/' . \$matches[2]), $rootDirectoryLen));" - ), - $content - ); - $mergedContent = $mergedContent . $content; - } - - $mergedContent = cssmin::minify($mergedContent); - $mergedContent = str_replace("\n", "\r\n", $mergedContent); + const MERGED_CSS_FILE = "asset_manager_global_css.css"; + const MERGED_JS_FILE = "asset_manager_global_js.js"; + const CSS_IMPORT_EVENT = "AssetManager.getCssFiles"; + const JS_IMPORT_EVENT = "AssetManager.getJsFiles"; + const MERGED_FILE_DIR = "tmp/assets/"; + const CSS_IMPORT_DIRECTIVE = "<link rel=\"stylesheet\" type=\"text/css\" href=\"%s\" />\n"; + const JS_IMPORT_DIRECTIVE = "<script type=\"text/javascript\" src=\"%s\"></script>\n"; + const GET_CSS_MODULE_ACTION = "index.php?module=Proxy&action=getCss"; + const GET_JS_MODULE_ACTION = "index.php?module=Proxy&action=getJs"; + const MINIFIED_JS_RATIO = 100; + + /** + * Returns CSS file inclusion directive(s) using the markup <link> + * + * @return string + */ + public static function getCssAssets() + { + if (self::getDisableMergedAssets()) { + // Individual includes mode + self::removeMergedAsset(self::MERGED_CSS_FILE); + return self::getIndividualCssIncludes(); + } + return sprintf(self::CSS_IMPORT_DIRECTIVE, self::GET_CSS_MODULE_ACTION); + } + + /** + * Returns JS file inclusion directive(s) using the markup <script> + * + * @return string + */ + public static function getJsAssets() + { + if (self::getDisableMergedAssets()) { + // Individual includes mode + self::removeMergedAsset(self::MERGED_JS_FILE); + return self::getIndividualJsIncludes(); + } + return sprintf(self::JS_IMPORT_DIRECTIVE, self::GET_JS_MODULE_ACTION); + } + + /** + * Generate the merged css file. + * + * @throws Exception if a file can not be opened in write mode + */ + private static function generateMergedCssFile() + { + $mergedContent = ""; + + // absolute path to doc root + $rootDirectory = realpath(PIWIK_DOCUMENT_ROOT); + if ($rootDirectory != '/' && substr_compare($rootDirectory, '/', -1)) { + $rootDirectory .= '/'; + } + $rootDirectoryLen = strlen($rootDirectory); + + // Loop through each css file + $files = self::getCssFiles(); + foreach ($files as $file) { + + self::validateCssFile($file); + + $fileLocation = self::getAbsoluteLocation($file); + $content = file_get_contents($fileLocation); + + // Rewrite css url directives + // - assumes these are all relative paths + // - rewrite windows directory separator \\ to / + $baseDirectory = dirname($file); + $content = preg_replace_callback( + "/(url\(['\"]?)([^'\")]*)/", + create_function( + '$matches', + "return \$matches[1] . str_replace('\\\\', '/', substr(realpath(PIWIK_DOCUMENT_ROOT . '/$baseDirectory/' . \$matches[2]), $rootDirectoryLen));" + ), + $content + ); + $mergedContent = $mergedContent . $content; + } + + $mergedContent = cssmin::minify($mergedContent); + $mergedContent = str_replace("\n", "\r\n", $mergedContent); Piwik_PostEvent('AssetManager.filterMergedCss', $mergedContent); @@ -150,359 +147,347 @@ class Piwik_AssetManager } /** - * Returns individual CSS file inclusion directive(s) using the markup <link> - * - * @return string - */ - private static function getIndividualCssIncludes() - { - $cssIncludeString = ''; - - $cssFiles = self::getCssFiles(); - - foreach ($cssFiles as $cssFile) { - - self::validateCssFile ( $cssFile ); - $cssIncludeString = $cssIncludeString . sprintf ( self::CSS_IMPORT_DIRECTIVE, $cssFile ); - } - - return $cssIncludeString; - } - - /** - * Returns required CSS files - * - * @return Array - */ - private static function getCssFiles() - { - $cssFiles = array(); - Piwik_PostEvent(self::CSS_IMPORT_EVENT, $cssFiles); - $cssFiles = self::sortCssFiles($cssFiles); - return $cssFiles; - } - - /** - * Ensure CSS stylesheets are loaded in a particular order regardless of the order that plugins are loaded. - * - * @param array $cssFiles Array of CSS stylesheet files - * @return array - */ - private static function sortCssFiles($cssFiles) - { - $priorityCssOrdered = array( - 'themes/default/common.css', - 'themes/default/', - 'libs/', - 'plugins/', - ); - - return self::prioritySort($priorityCssOrdered, $cssFiles); - } - - /** - * Check the validity of the css file - * - * @param string $cssFile CSS file name - * @return boolean - * @throws Exception if a file can not be opened in write mode - */ - private static function validateCssFile ( $cssFile ) - { - if(!self::assetIsReadable($cssFile)) - { - throw new Exception("The css asset with 'href' = " . $cssFile . " is not readable"); - } - } - - /** - * Generate the merged js file. - * - * @throws Exception if a file can not be opened in write mode - */ - private static function generateMergedJsFile() - { - $mergedContent = ""; - - // Loop through each js file - $files = self::getJsFiles(); - foreach ($files as $file) { - - self::validateJsFile ( $file ); - - $fileLocation = self::getAbsoluteLocation($file); - $content = file_get_contents ($fileLocation); - - if ( !self::isMinifiedJs($content) ) - { - $content = JSMin::minify($content); - } - - $mergedContent = $mergedContent . PHP_EOL . $content; - } - $mergedContent = str_replace("\n", "\r\n", $mergedContent); + * Returns individual CSS file inclusion directive(s) using the markup <link> + * + * @return string + */ + private static function getIndividualCssIncludes() + { + $cssIncludeString = ''; + + $cssFiles = self::getCssFiles(); + + foreach ($cssFiles as $cssFile) { + + self::validateCssFile($cssFile); + $cssIncludeString = $cssIncludeString . sprintf(self::CSS_IMPORT_DIRECTIVE, $cssFile); + } + + return $cssIncludeString; + } + + /** + * Returns required CSS files + * + * @return Array + */ + private static function getCssFiles() + { + $cssFiles = array(); + Piwik_PostEvent(self::CSS_IMPORT_EVENT, $cssFiles); + $cssFiles = self::sortCssFiles($cssFiles); + return $cssFiles; + } + + /** + * Ensure CSS stylesheets are loaded in a particular order regardless of the order that plugins are loaded. + * + * @param array $cssFiles Array of CSS stylesheet files + * @return array + */ + private static function sortCssFiles($cssFiles) + { + $priorityCssOrdered = array( + 'themes/default/common.css', + 'themes/default/', + 'libs/', + 'plugins/', + ); + + return self::prioritySort($priorityCssOrdered, $cssFiles); + } + + /** + * Check the validity of the css file + * + * @param string $cssFile CSS file name + * @return boolean + * @throws Exception if a file can not be opened in write mode + */ + private static function validateCssFile($cssFile) + { + if (!self::assetIsReadable($cssFile)) { + throw new Exception("The css asset with 'href' = " . $cssFile . " is not readable"); + } + } + + /** + * Generate the merged js file. + * + * @throws Exception if a file can not be opened in write mode + */ + private static function generateMergedJsFile() + { + $mergedContent = ""; + + // Loop through each js file + $files = self::getJsFiles(); + foreach ($files as $file) { + + self::validateJsFile($file); + + $fileLocation = self::getAbsoluteLocation($file); + $content = file_get_contents($fileLocation); + + if (!self::isMinifiedJs($content)) { + $content = JSMin::minify($content); + } + + $mergedContent = $mergedContent . PHP_EOL . $content; + } + $mergedContent = str_replace("\n", "\r\n", $mergedContent); Piwik_PostEvent('AssetManager.filterMergedJs', $mergedContent); self::writeAssetToFile($mergedContent, self::MERGED_JS_FILE); - } - - /** - * Returns individual JS file inclusion directive(s) using the markup <script> - * - * @return string - */ - private static function getIndividualJsIncludes() - { - $jsFiles = self::getJsFiles(); - $jsIncludeString = ''; - foreach ($jsFiles as $jsFile) - { - self::validateJsFile( $jsFile ); - $jsIncludeString = $jsIncludeString . sprintf ( self::JS_IMPORT_DIRECTIVE, $jsFile ); - } - return $jsIncludeString; - } - - /** - * Returns required JS files - * - * @return Array - */ - private static function getJsFiles() - { - $jsFiles = array(); - Piwik_PostEvent(self::JS_IMPORT_EVENT, $jsFiles); - $jsFiles = self::sortJsFiles($jsFiles); - return $jsFiles; - } - - /** - * Ensure core JS (jQuery etc.) are loaded in a particular order regardless of the order that plugins are loaded. - * - * @param array $jsFiles Arry of JavaScript files - * @return array - */ - private static function sortJsFiles($jsFiles) - { - $priorityJsOrdered = array( - 'libs/jquery/jquery.js', - 'libs/jquery/jquery-ui.js', - 'libs/', - 'themes/default/common.js', - 'themes/default/', - 'plugins/CoreHome/templates/broadcast.js', - 'plugins/', - ); - - return self::prioritySort($priorityJsOrdered, $jsFiles); - } - - /** - * Check the validity of the js file - * - * @param string $jsFile JavaScript file name - * @return boolean - * @throws Exception if js file is not valid - */ - private static function validateJsFile ( $jsFile ) - { - if(!self::assetIsReadable($jsFile)) - { - throw new Exception("The js asset with 'src' = " . $jsFile . " is not readable"); - } - } - - /** - * Returns the global option disable_merged_assets - * - * @return string - */ - private static function getDisableMergedAssets() - { - return Piwik_Config::getInstance()->Debug['disable_merged_assets']; - } - - /** - * Returns the css merged file absolute location. - * If there is none, the generation process will be triggered. - * - * @return string The absolute location of the css merged file - */ - public static function getMergedCssFileLocation() - { - $isGenerated = self::isGenerated(self::MERGED_CSS_FILE); - - if ( !$isGenerated ) - { - self::generateMergedCssFile(); - } - - return self::getAbsoluteMergedFileLocation(self::MERGED_CSS_FILE); - } - - /** - * Returns the js merged file absolute location. - * If there is none, the generation process will be triggered. - * - * @return string The absolute location of the js merged file - */ - public static function getMergedJsFileLocation() - { - $isGenerated = self::isGenerated(self::MERGED_JS_FILE); - - if ( !$isGenerated ) - { - self::generateMergedJsFile(); - } - - return self::getAbsoluteMergedFileLocation(self::MERGED_JS_FILE); - } - - /** - * Check if the provided merged file is generated - * - * @param string $filename filename of the merged asset - * @return boolean true is file exists and is readable, false otherwise - */ - private static function isGenerated($filename) - { - return is_readable (self::getAbsoluteMergedFileLocation($filename)); - } - - /** - * Removes the previous merged file if it exists. - * Also tries to remove compressed version of the merged file. - * - * @param string $filename filename of the merged asset - * @see Piwik::serveStaticFile() - * @throws Exception if the file couldn't be deleted - */ - private static function removeMergedAsset($filename) - { - $isGenerated = self::isGenerated($filename); - - if ( $isGenerated ) - { - if ( !unlink ( self::getAbsoluteMergedFileLocation($filename) ) ) - { - throw Exception ("Unable to delete merged file : " . $filename . ". Please delete the file and refresh"); - } - - // Tries to remove compressed version of the merged file. - // See Piwik::serveStaticFile() for more info on static file compression - $compressedFileLocation = PIWIK_USER_PATH . Piwik::COMPRESSED_FILE_LOCATION . $filename; - - @unlink ( $compressedFileLocation . ".deflate"); - @unlink ( $compressedFileLocation . ".gz"); - } - } - - /** - * Remove previous merged assets - */ - public static function removeMergedAssets() - { - self::removeMergedAsset(self::MERGED_CSS_FILE); - self::removeMergedAsset(self::MERGED_JS_FILE); - } - - /** - * Check if asset is readable - * - * @param string $relativePath Relative path to file - * @return boolean - */ - private static function assetIsReadable ($relativePath) - { - return is_readable(self::getAbsoluteLocation($relativePath)); - } - - /** - * Check if the merged file directory exists and is writable. - * - * @return string The directory location - * @throws Exception if directory is not writable. - */ - private static function getMergedFileDirectory () - { - $mergedFileDirectory = PIWIK_USER_PATH . '/' . self::MERGED_FILE_DIR; - - if (!is_dir($mergedFileDirectory)) - { - Piwik_Common::mkdir($mergedFileDirectory); - } - - if (!is_writable($mergedFileDirectory)) - { - throw new Exception("Directory " . $mergedFileDirectory . " has to be writable."); - } - - return $mergedFileDirectory; - } - - /** - * Builds the absolute location of the requested merged file - * - * @param string $mergedFile Name of the merge file - * @return string absolute location of the merged file - */ - private static function getAbsoluteMergedFileLocation( $mergedFile ) - { - return self::getMergedFileDirectory() . $mergedFile; - } - - /** - * Returns the full path of an asset file - * - * @param string $relativePath Relative path to file - * @return string - */ - private static function getAbsoluteLocation ($relativePath) - { - // served by web server directly, so must be a public path - return PIWIK_DOCUMENT_ROOT . "/" . $relativePath; - } - - /** - * Indicates if the provided JavaScript content has already been minified or not. - * The heuristic is based on a custom ratio : (size of file) / (number of lines). - * The threshold (100) has been found empirically on existing files : - * - the ratio never exceeds 50 for non-minified content and - * - it never goes under 150 for minified content. - * - * @param string $content Contents of the JavaScript file - * @return boolean - */ - private static function isMinifiedJs ( $content ) - { - $lineCount = substr_count($content, "\n"); - if ( $lineCount == 0 ) - { - return true; - } - - $contentSize = strlen($content); - - $ratio = $contentSize / $lineCount; - - return $ratio > self::MINIFIED_JS_RATIO; - } - - /** - * Sort files according to priority order. Duplicates are also removed. - * - * @param array $priorityOrder Ordered array of paths (first to last) serving as buckets - * @param array $files Unsorted array of files - * @return array - */ - public static function prioritySort($priorityOrder, $files) - { - $newFiles = array(); - foreach($priorityOrder as $filePattern) - { - $newFiles = array_merge($newFiles, preg_grep('~^' . $filePattern . '~', $files)); - } - return array_keys(array_flip($newFiles)); - } + } + + /** + * Returns individual JS file inclusion directive(s) using the markup <script> + * + * @return string + */ + private static function getIndividualJsIncludes() + { + $jsFiles = self::getJsFiles(); + $jsIncludeString = ''; + foreach ($jsFiles as $jsFile) { + self::validateJsFile($jsFile); + $jsIncludeString = $jsIncludeString . sprintf(self::JS_IMPORT_DIRECTIVE, $jsFile); + } + return $jsIncludeString; + } + + /** + * Returns required JS files + * + * @return Array + */ + private static function getJsFiles() + { + $jsFiles = array(); + Piwik_PostEvent(self::JS_IMPORT_EVENT, $jsFiles); + $jsFiles = self::sortJsFiles($jsFiles); + return $jsFiles; + } + + /** + * Ensure core JS (jQuery etc.) are loaded in a particular order regardless of the order that plugins are loaded. + * + * @param array $jsFiles Arry of JavaScript files + * @return array + */ + private static function sortJsFiles($jsFiles) + { + $priorityJsOrdered = array( + 'libs/jquery/jquery.js', + 'libs/jquery/jquery-ui.js', + 'libs/', + 'themes/default/common.js', + 'themes/default/', + 'plugins/CoreHome/templates/broadcast.js', + 'plugins/', + ); + + return self::prioritySort($priorityJsOrdered, $jsFiles); + } + + /** + * Check the validity of the js file + * + * @param string $jsFile JavaScript file name + * @return boolean + * @throws Exception if js file is not valid + */ + private static function validateJsFile($jsFile) + { + if (!self::assetIsReadable($jsFile)) { + throw new Exception("The js asset with 'src' = " . $jsFile . " is not readable"); + } + } + + /** + * Returns the global option disable_merged_assets + * + * @return string + */ + private static function getDisableMergedAssets() + { + return Piwik_Config::getInstance()->Debug['disable_merged_assets']; + } + + /** + * Returns the css merged file absolute location. + * If there is none, the generation process will be triggered. + * + * @return string The absolute location of the css merged file + */ + public static function getMergedCssFileLocation() + { + $isGenerated = self::isGenerated(self::MERGED_CSS_FILE); + + if (!$isGenerated) { + self::generateMergedCssFile(); + } + + return self::getAbsoluteMergedFileLocation(self::MERGED_CSS_FILE); + } + + /** + * Returns the js merged file absolute location. + * If there is none, the generation process will be triggered. + * + * @return string The absolute location of the js merged file + */ + public static function getMergedJsFileLocation() + { + $isGenerated = self::isGenerated(self::MERGED_JS_FILE); + + if (!$isGenerated) { + self::generateMergedJsFile(); + } + + return self::getAbsoluteMergedFileLocation(self::MERGED_JS_FILE); + } + + /** + * Check if the provided merged file is generated + * + * @param string $filename filename of the merged asset + * @return boolean true is file exists and is readable, false otherwise + */ + private static function isGenerated($filename) + { + return is_readable(self::getAbsoluteMergedFileLocation($filename)); + } + + /** + * Removes the previous merged file if it exists. + * Also tries to remove compressed version of the merged file. + * + * @param string $filename filename of the merged asset + * @see Piwik::serveStaticFile() + * @throws Exception if the file couldn't be deleted + */ + private static function removeMergedAsset($filename) + { + $isGenerated = self::isGenerated($filename); + + if ($isGenerated) { + if (!unlink(self::getAbsoluteMergedFileLocation($filename))) { + throw Exception("Unable to delete merged file : " . $filename . ". Please delete the file and refresh"); + } + + // Tries to remove compressed version of the merged file. + // See Piwik::serveStaticFile() for more info on static file compression + $compressedFileLocation = PIWIK_USER_PATH . Piwik::COMPRESSED_FILE_LOCATION . $filename; + + @unlink($compressedFileLocation . ".deflate"); + @unlink($compressedFileLocation . ".gz"); + } + } + + /** + * Remove previous merged assets + */ + public static function removeMergedAssets() + { + self::removeMergedAsset(self::MERGED_CSS_FILE); + self::removeMergedAsset(self::MERGED_JS_FILE); + } + + /** + * Check if asset is readable + * + * @param string $relativePath Relative path to file + * @return boolean + */ + private static function assetIsReadable($relativePath) + { + return is_readable(self::getAbsoluteLocation($relativePath)); + } + + /** + * Check if the merged file directory exists and is writable. + * + * @return string The directory location + * @throws Exception if directory is not writable. + */ + private static function getMergedFileDirectory() + { + $mergedFileDirectory = PIWIK_USER_PATH . '/' . self::MERGED_FILE_DIR; + + if (!is_dir($mergedFileDirectory)) { + Piwik_Common::mkdir($mergedFileDirectory); + } + + if (!is_writable($mergedFileDirectory)) { + throw new Exception("Directory " . $mergedFileDirectory . " has to be writable."); + } + + return $mergedFileDirectory; + } + + /** + * Builds the absolute location of the requested merged file + * + * @param string $mergedFile Name of the merge file + * @return string absolute location of the merged file + */ + private static function getAbsoluteMergedFileLocation($mergedFile) + { + return self::getMergedFileDirectory() . $mergedFile; + } + + /** + * Returns the full path of an asset file + * + * @param string $relativePath Relative path to file + * @return string + */ + private static function getAbsoluteLocation($relativePath) + { + // served by web server directly, so must be a public path + return PIWIK_DOCUMENT_ROOT . "/" . $relativePath; + } + + /** + * Indicates if the provided JavaScript content has already been minified or not. + * The heuristic is based on a custom ratio : (size of file) / (number of lines). + * The threshold (100) has been found empirically on existing files : + * - the ratio never exceeds 50 for non-minified content and + * - it never goes under 150 for minified content. + * + * @param string $content Contents of the JavaScript file + * @return boolean + */ + private static function isMinifiedJs($content) + { + $lineCount = substr_count($content, "\n"); + if ($lineCount == 0) { + return true; + } + + $contentSize = strlen($content); + + $ratio = $contentSize / $lineCount; + + return $ratio > self::MINIFIED_JS_RATIO; + } + + /** + * Sort files according to priority order. Duplicates are also removed. + * + * @param array $priorityOrder Ordered array of paths (first to last) serving as buckets + * @param array $files Unsorted array of files + * @return array + */ + public static function prioritySort($priorityOrder, $files) + { + $newFiles = array(); + foreach ($priorityOrder as $filePattern) { + $newFiles = array_merge($newFiles, preg_grep('~^' . $filePattern . '~', $files)); + } + return array_keys(array_flip($newFiles)); + } } diff --git a/core/Auth.php b/core/Auth.php index 5e58eca147..252a31afbd 100644 --- a/core/Auth.php +++ b/core/Auth.php @@ -15,20 +15,21 @@ * @package Piwik * @subpackage Piwik_Auth */ -interface Piwik_Auth { - /** - * Authentication module's name, e.g., "Login" - * - * @return string - */ - public function getName(); +interface Piwik_Auth +{ + /** + * Authentication module's name, e.g., "Login" + * + * @return string + */ + public function getName(); - /** - * Authenticates user - * - * @return Piwik_Auth_Result - */ - public function authenticate(); + /** + * Authenticates user + * + * @return Piwik_Auth_Result + */ + public function authenticate(); } /** @@ -41,32 +42,32 @@ interface Piwik_Auth { */ class Piwik_Auth_Result extends Zend_Auth_Result { - /** - * token_auth parameter used to authenticate in the API - * - * @var string - */ - protected $_token_auth = null; - - const SUCCESS_SUPERUSER_AUTH_CODE = 42; + /** + * token_auth parameter used to authenticate in the API + * + * @var string + */ + protected $_token_auth = null; + + const SUCCESS_SUPERUSER_AUTH_CODE = 42; + + /** + * Constructor for Piwik_Auth_Result + * + * @param int $code + * @param string $login identity + * @param string $token_auth + * @param array $messages + */ + public function __construct($code, $login, $token_auth, array $messages = array()) + { + // Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE, Piwik_Auth_Result::SUCCESS, Piwik_Auth_Result::FAILURE + $this->_code = (int)$code; + $this->_identity = $login; + $this->_messages = $messages; + $this->_token_auth = $token_auth; + } - /** - * Constructor for Piwik_Auth_Result - * - * @param int $code - * @param string $login identity - * @param string $token_auth - * @param array $messages - */ - public function __construct($code, $login, $token_auth, array $messages = array()) - { - // Piwik_Auth_Result::SUCCESS_SUPERUSER_AUTH_CODE, Piwik_Auth_Result::SUCCESS, Piwik_Auth_Result::FAILURE - $this->_code = (int)$code; - $this->_identity = $login; - $this->_messages = $messages; - $this->_token_auth = $token_auth; - } - /** * Returns the token_auth to authenticate the current user in the API * @@ -74,6 +75,6 @@ class Piwik_Auth_Result extends Zend_Auth_Result */ public function getTokenAuth() { - return $this->_token_auth; + return $this->_token_auth; } } diff --git a/core/CacheFile.php b/core/CacheFile.php index 2ee1d11176..336945db91 100644 --- a/core/CacheFile.php +++ b/core/CacheFile.php @@ -21,155 +21,155 @@ */ class Piwik_CacheFile { - /** - * @var string - */ - protected $cachePath; - /** - * @var - */ - protected $cachePrefix; - - /** - * Minimum enforced TTL in seconds - */ - const MINIMUM_TTL = 60; - - /** - * @param string $directory directory to use - * @param int TTL - */ - function __construct($directory, $timeToLiveInSeconds = 300) - { - $this->cachePath = PIWIK_USER_PATH . '/tmp/cache/' . $directory . '/'; - if($timeToLiveInSeconds < self::MINIMUM_TTL) { - $timeToLiveInSeconds = self::MINIMUM_TTL; - } - $this->ttl = $timeToLiveInSeconds; - } - - /** - * Function to fetch a cache entry - * - * @param string $id The cache entry ID - * @return array|bool False on error, or array the cache content - */ - function get($id) - { - if(empty($id)) { - return false; - } - $id = $this->cleanupId($id); - - $cache_complete = false; - $content = ''; - $expires_on = false; - - // We are assuming that most of the time cache will exists - $ok = @include($this->cachePath . $id . '.php'); - - if ($ok && $cache_complete == true) { - - if(empty($expires_on) - || $expires_on < time()) { - return false; - } - return $content; - } - - return false; - } - - private function getExpiresTime() - { - return time() + $this->ttl; - } - - protected function cleanupId($id) - { - if(!Piwik_Common::isValidFilename($id)) { - throw new Exception("Invalid cache ID request $id"); - } - return $id; - } - - /** - * A function to store content a cache entry. - * - * @param string $id The cache entry ID - * @param array $content The cache content - * @return bool True if the entry was succesfully stored - */ - function set($id, $content) - { - if(empty($id)) { - return false; - } - if( !is_dir($this->cachePath)) - { - Piwik_Common::mkdir($this->cachePath); - } - if (!is_writable($this->cachePath)) { - return false; - } - $id = $this->cleanupId($id); - - $id = $this->cachePath . $id . '.php'; - - $cache_literal = "<"."?php\n"; - $cache_literal .= "$"."content = ".var_export($content, true).";\n"; - $cache_literal .= "$"."expires_on = ".$this->getExpiresTime().";\n"; - $cache_literal .= "$"."cache_complete = true;\n"; - $cache_literal .= "?".">"; - - // Write cache to a temp file, then rename it, overwriting the old cache - // On *nix systems this should guarantee atomicity - $tmp_filename = tempnam($this->cachePath, 'tmp_'); - @chmod($tmp_filename, 0640); - if ($fp = @fopen($tmp_filename, 'wb')) { - @fwrite ($fp, $cache_literal, strlen($cache_literal)); - @fclose ($fp); - - if (!@rename($tmp_filename, $id)) { - // On some systems rename() doesn't overwrite destination - @unlink($id); - if (!@rename($tmp_filename, $id)) { - // Make sure that no temporary file is left over - // if the destination is not writable - @unlink($tmp_filename); - } - } - return true; - } - return false; - } - - /** - * A function to delete a single cache entry - * - * @param string $id The cache entry ID - * @return bool True if the entry was succesfully deleted - */ - function delete($id) - { - if(empty($id)) { - return false; - } - $id = $this->cleanupId($id); - - $filename = $this->cachePath . $id . '.php'; - if (file_exists($filename)) { - @unlink ($filename); - return true; - } - return false; - } - - /** - * A function to delete all cache entries in the directory - */ - function deleteAll() - { - Piwik::unlinkRecursive($this->cachePath, $deleteRootToo = false); - } + /** + * @var string + */ + protected $cachePath; + /** + * @var + */ + protected $cachePrefix; + + /** + * Minimum enforced TTL in seconds + */ + const MINIMUM_TTL = 60; + + /** + * @param string $directory directory to use + * @param int TTL + */ + function __construct($directory, $timeToLiveInSeconds = 300) + { + $this->cachePath = PIWIK_USER_PATH . '/tmp/cache/' . $directory . '/'; + if ($timeToLiveInSeconds < self::MINIMUM_TTL) { + $timeToLiveInSeconds = self::MINIMUM_TTL; + } + $this->ttl = $timeToLiveInSeconds; + } + + /** + * Function to fetch a cache entry + * + * @param string $id The cache entry ID + * @return array|bool False on error, or array the cache content + */ + function get($id) + { + if (empty($id)) { + return false; + } + $id = $this->cleanupId($id); + + $cache_complete = false; + $content = ''; + $expires_on = false; + + // We are assuming that most of the time cache will exists + $ok = @include($this->cachePath . $id . '.php'); + + if ($ok && $cache_complete == true) { + + if (empty($expires_on) + || $expires_on < time() + ) { + return false; + } + return $content; + } + + return false; + } + + private function getExpiresTime() + { + return time() + $this->ttl; + } + + protected function cleanupId($id) + { + if (!Piwik_Common::isValidFilename($id)) { + throw new Exception("Invalid cache ID request $id"); + } + return $id; + } + + /** + * A function to store content a cache entry. + * + * @param string $id The cache entry ID + * @param array $content The cache content + * @return bool True if the entry was succesfully stored + */ + function set($id, $content) + { + if (empty($id)) { + return false; + } + if (!is_dir($this->cachePath)) { + Piwik_Common::mkdir($this->cachePath); + } + if (!is_writable($this->cachePath)) { + return false; + } + $id = $this->cleanupId($id); + + $id = $this->cachePath . $id . '.php'; + + $cache_literal = "<" . "?php\n"; + $cache_literal .= "$" . "content = " . var_export($content, true) . ";\n"; + $cache_literal .= "$" . "expires_on = " . $this->getExpiresTime() . ";\n"; + $cache_literal .= "$" . "cache_complete = true;\n"; + $cache_literal .= "?" . ">"; + + // Write cache to a temp file, then rename it, overwriting the old cache + // On *nix systems this should guarantee atomicity + $tmp_filename = tempnam($this->cachePath, 'tmp_'); + @chmod($tmp_filename, 0640); + if ($fp = @fopen($tmp_filename, 'wb')) { + @fwrite($fp, $cache_literal, strlen($cache_literal)); + @fclose($fp); + + if (!@rename($tmp_filename, $id)) { + // On some systems rename() doesn't overwrite destination + @unlink($id); + if (!@rename($tmp_filename, $id)) { + // Make sure that no temporary file is left over + // if the destination is not writable + @unlink($tmp_filename); + } + } + return true; + } + return false; + } + + /** + * A function to delete a single cache entry + * + * @param string $id The cache entry ID + * @return bool True if the entry was succesfully deleted + */ + function delete($id) + { + if (empty($id)) { + return false; + } + $id = $this->cleanupId($id); + + $filename = $this->cachePath . $id . '.php'; + if (file_exists($filename)) { + @unlink($filename); + return true; + } + 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 e728675c2b..b2bdbafd82 100644 --- a/core/Common.php +++ b/core/Common.php @@ -20,1668 +20,1520 @@ */ class Piwik_Common { - /** - * Const used to map the referer type to an integer in the log_visit table - */ - const REFERER_TYPE_DIRECT_ENTRY = 1; - const REFERER_TYPE_SEARCH_ENGINE = 2; - const REFERER_TYPE_WEBSITE = 3; - const REFERER_TYPE_CAMPAIGN = 6; - - /** - * Flag used with htmlspecialchar - * See php.net/htmlspecialchars - */ - const HTML_ENCODING_QUOTE_STYLE = ENT_QUOTES; - -/* - * Database - */ - - /** - * Hashes a string into an integer which should be very low collision risks - * @param string $string String to hash - * @return int Resulting int hash - */ - static public function hashStringToInt($string) - { - $stringHash = substr(md5($string), 0, 8); - return base_convert($stringHash, 16, 10); - } - - static public $cachedTablePrefix = null; - - /** - * Returns the table name prefixed by the table prefix. - * Works in both Tracker and UI mode. - * - * @param string $table The table name to prefix, ie "log_visit" - * @return string The table name prefixed, ie "piwik-production_log_visit" - */ - static public function prefixTable($table) - { - if(is_null(self::$cachedTablePrefix)) - { - self::$cachedTablePrefix = Piwik_Config::getInstance()->database['tables_prefix']; - } - return self::$cachedTablePrefix . $table; - } - - /** - * Returns an array containing the prefixed table names of every passed argument. - * - * @param string ... The table names to prefix, ie "log_visit" - * @return array The prefixed names in an array. - */ - static public function prefixTables() - { - $result = array(); - foreach (func_get_args() as $table) - { - $result[] = self::prefixTable($table); - } - return $result; - } - - /** - * Returns the table name, after removing the table prefix - * - * @param string $table - * @return string - */ - static public function unprefixTable($table) - { - static $prefixTable = null; - if(is_null($prefixTable)) - { - $prefixTable = Piwik_Config::getInstance()->database['tables_prefix']; - } - if(empty($prefixTable) - || strpos($table, $prefixTable) !== 0) - { - return $table; - } - $count = 1; - return str_replace($prefixTable, '', $table, $count); - } - -/* - * Tracker - */ - static public function isGoalPluginEnabled() - { - return Piwik_PluginsManager::getInstance()->isPluginActivated('Goals'); - } - -/* - * URLs - */ - - /** - * 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 - * - * @param string $url either http://piwik.org/test or / - * @return string - */ - static public function getPathAndQueryFromUrl($url) - { - $parsedUrl = parse_url( $url ); - $result = ''; - if(isset($parsedUrl['path'])) - { - $result .= substr($parsedUrl['path'], 1); - } - if(isset($parsedUrl['query'])) - { - $result .= '?'.$parsedUrl['query']; - } - return $result; - } - - /** - * Returns the value of a GET parameter $parameter in an URL query $urlQuery - * - * @param string $urlQuery result of parse_url()['query'] and htmlentitied (& is &) eg. module=test&action=toto or ?page=test - * @param string $parameter - * @return string|bool Parameter value if found (can be the empty string!), null if not found - */ - static public function getParameterFromQueryString( $urlQuery, $parameter) - { - $nameToValue = self::getArrayFromQueryString($urlQuery); - if(isset($nameToValue[$parameter])) - { - return $nameToValue[$parameter]; - } - return null; - } - - /** - * Returns an URL query string in an array format - * - * @param string $urlQuery - * @return array array( param1=> value1, param2=>value2) - */ - static public function getArrayFromQueryString( $urlQuery ) - { - if(strlen($urlQuery) == 0) - { - return array(); - } - if($urlQuery[0] == '?') - { - $urlQuery = substr($urlQuery, 1); - } - $separator = '&'; - - $urlQuery = $separator . $urlQuery; - // $urlQuery = str_replace(array('%20'), ' ', $urlQuery); - $refererQuery = trim($urlQuery); - - $values = explode($separator, $refererQuery); - - $nameToValue = array(); - - foreach($values as $value) - { - $pos = strpos($value, '='); - if($pos !== false) - { - $name = substr($value, 0, $pos); - $value = substr($value, $pos+1); - if ($value === false) - { - $value = ''; - } - } - else - { - $name = $value; - $value = false; - } - if(!empty($name)) - { - $name = Piwik_Common::sanitizeInputValue($name); - } - if(!empty($value)) - { - $value = Piwik_Common::sanitizeInputValue($value); - } - - // if array without indexes - $count = 0; - $tmp = preg_replace('/(\[|%5b)(]|%5d)$/i', '', $name, -1, $count); - if(!empty($tmp) && $count) - { - $name = $tmp; - if( isset($nameToValue[$name]) == false || is_array($nameToValue[$name]) == false ) - { - $nameToValue[$name] = array(); - } - array_push($nameToValue[$name], $value); - } - else if(!empty($name)) - { - $nameToValue[$name] = $value; - } - } - return $nameToValue; - } - - /** - * Builds a URL from the result of parse_url function - * Copied from the PHP comments at http://php.net/parse_url - * @param array $parsed - * @return bool|string - */ - static public function getParseUrlReverse($parsed) - { - if (!is_array($parsed)) - { - return false; - } - - $uri = !empty($parsed['scheme']) ? $parsed['scheme'].':'.(!strcasecmp($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 .= (!strncmp($parsed['path'], '/', 1)) - ? $parsed['path'] - : ((!empty($uri) ? '/' : '' ) . $parsed['path']); - } - - $uri .= !empty($parsed['query']) ? '?'.$parsed['query'] : ''; - $uri .= !empty($parsed['fragment']) ? '#'.$parsed['fragment'] : ''; - return $uri; - } - - /** - * Returns true if the string passed may be a URL. - * We don't need a precise test here because the value comes from the website - * tracked source code and the URLs may look very strange. - * - * @param string $url - * @return bool - */ - static public function isLookLikeUrl( $url ) - { - return preg_match('~^(ftp|news|http|https)?://(.*)$~D', $url, $matches) !== 0 - && strlen($matches[2]) > 0; - } - -/* - * File operations - */ - - /** - * ending WITHOUT slash - * - * @return string - */ - static public function getPathToPiwikRoot() - { - return realpath( dirname(__FILE__). "/.." ); - } - - /** - * Create directory if permitted - * - * @param string $path - * @param bool $denyAccess - */ - static public function mkdir( $path, $denyAccess = true ) - { - if(!is_dir($path)) - { - // the mode in mkdir is modified by the current umask - @mkdir($path, $mode = 0755, $recursive = true); - } - - // try to overcome restrictive umask (mis-)configuration - if(!is_writable($path)) - { - @chmod($path, 0755); - if(!is_writable($path)) - { - @chmod($path, 0775); - - // enough! we're not going to make the directory world-writeable - } - } - - if($denyAccess) - { - self::createHtAccess($path, $overwrite = false); - } - } - - /** - * Create .htaccess file in specified directory - * - * Apache-specific; for IIS @see web.config - * - * @param string $path without trailing slash - * @param bool $overwrite whether to overwrite an existing file or not - * @param string $content - */ - static public function createHtAccess( $path, $overwrite = true, $content = "<Files \"*\">\n<IfModule mod_access.c>\nDeny from all\n</IfModule>\n<IfModule !mod_access_compat>\n<IfModule mod_authz_host.c>\nDeny from all\n</IfModule>\n</IfModule>\n<IfModule mod_access_compat>\nDeny from all\n</IfModule>\n</Files>\n" ) - { - if(self::isApache()) - { - $file = $path . '/.htaccess'; - if ($overwrite || !file_exists($file)) - { - @file_put_contents($file, $content); - } - } - } - - /** - * Get canonicalized absolute path - * See http://php.net/realpath - * - * @param string $path - * @return string canonicalized absolute path - */ - static public function realpath($path) - { - if (file_exists($path)) - { - return realpath($path); - } - return $path; - } - - /** - * Returns true if the string is a valid filename - * File names that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), and dot(.) will be accepted. - * File names beginning with anything but a-Z or 0-9 will be rejected (including .htaccess for example). - * File names containing anything other than above mentioned will also be rejected (file names with spaces won't be accepted). - * - * @param string $filename - * @return bool - * - */ - static public function isValidFilename($filename) - { - return (0 !== preg_match('/(^[a-zA-Z0-9]+([a-zA-Z_0-9.-]*))$/D', $filename)); - } - -/* - * String operations - */ - - /** - * byte-oriented substr() - ASCII - * - * @param string $string - * @param int $start - * @param int ... optional length - * @return string - */ - static public function substr($string, $start) - { - // in case mbstring overloads substr function - $substr = function_exists('mb_orig_substr') ? 'mb_orig_substr' : 'substr'; - - $length = func_num_args() > 2 - ? func_get_arg(2) - : self::strlen($string); - - return $substr($string, $start, $length); - } - - /** - * byte-oriented strlen() - ASCII - * - * @param string $string - * @return int - */ - static public function strlen($string) - { - // in case mbstring overloads strlen function - $strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen'; - return $strlen($string); - } - - /** - * multi-byte substr() - UTF-8 - * - * @param string $string - * @param int $start - * @param int ... optional length - * @return string - */ - static public function mb_substr($string, $start) - { - $length = func_num_args() > 2 - ? func_get_arg(2) - : self::mb_strlen($string); - - if (function_exists('mb_substr')) - { - return mb_substr($string, $start, $length, 'UTF-8'); - } - - return substr($string, $start, $length); - } - - /** - * multi-byte strlen() - UTF-8 - * - * @param string $string - * @return int - */ - static public function mb_strlen($string) - { - if (function_exists('mb_strlen')) - { - return mb_strlen($string, 'UTF-8'); - } - - return strlen($string); - } - - /** - * multi-byte strtolower() - UTF-8 - * - * @param string $string - * @return string - */ - static public function mb_strtolower($string) - { - if (function_exists('mb_strtolower')) - { - return mb_strtolower($string, 'UTF-8'); - } - - return strtolower($string); - } - -/* - * Escaping input - */ - - /** - * Returns the variable after cleaning operations. - * NB: The variable still has to be escaped before going into a SQL Query! - * - * If an array is passed the cleaning is done recursively on all the sub-arrays. - * The array's keys are filtered as well! - * - * How this method works: - * - The variable returned has been htmlspecialchars to avoid the XSS security problem. - * - The single quotes are not protected so "Piwik's amazing" will still be "Piwik's amazing". - * - * - Transformations are: - * - '&' (ampersand) becomes '&' - * - '"'(double quote) becomes '"' - * - '<' (less than) becomes '<' - * - '>' (greater than) becomes '>' - * - It handles the magic_quotes setting. - * - A non string value is returned without modification - * - * @param mixed $value The variable to be cleaned - * @param bool $alreadyStripslashed - * @throws Exception - * @return mixed The variable after cleaning - */ - static public function sanitizeInputValues($value, $alreadyStripslashed = false) - { - if(is_numeric($value)) - { - return $value; - } - elseif(is_string($value)) - { - $value = self::sanitizeInputValue($value); - - if(!$alreadyStripslashed) // a JSON array was already stripslashed, don't do it again for each value - { - $value = self::undoMagicQuotes($value); - } - } - elseif (is_array($value)) - { - foreach (array_keys($value) as $key) - { - $newKey = $key; - $newKey = self::sanitizeInputValues($newKey, $alreadyStripslashed); - if ($key != $newKey) - { - $value[$newKey] = $value[$key]; - unset($value[$key]); - } - - $value[$newKey] = self::sanitizeInputValues($value[$newKey], $alreadyStripslashed); - } - } - elseif( !is_null($value) - && !is_bool($value)) - { - throw new Exception("The value to escape has not a supported type. Value = ".var_export($value, true)); - } - return $value; - } - - /** - * Sanitize a single input value - * - * @param string $value - * @return string sanitized input - */ - static public function sanitizeInputValue($value) - { - // $_GET and $_REQUEST already urldecode()'d - // decode - // note: before php 5.2.7, htmlspecialchars() double encodes &#x hex items - $value = html_entity_decode($value, Piwik_Common::HTML_ENCODING_QUOTE_STYLE, 'UTF-8'); - - // filter - $value = str_replace(array("\n", "\r", "\0"), '', $value); - - // escape - $tmp = @htmlspecialchars( $value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8' ); - - // note: php 5.2.5 and above, htmlspecialchars is destructive if input is not UTF-8 - if($value != '' && $tmp == '') - { - // convert and escape - $value = utf8_encode($value); - $tmp = htmlspecialchars( $value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8' ); - } - return $tmp; - } - - /** - * Unsanitize a single input value - * - * @param string $value - * @return string unsanitized input - */ - static public function unsanitizeInputValue($value) - { - return htmlspecialchars_decode($value, self::HTML_ENCODING_QUOTE_STYLE); - } - - /** - * Unsanitize one or more values. - * - * @param string|array $value - * @return string|array unsanitized input - */ - static public function unsanitizeInputValues($value) - { - if (is_array($value)) - { - $result = array(); - foreach ($value as $key => $arrayValue) - { - $result[$key] = self::unsanitizeInputValues($arrayValue); - } - return $result; - } - else - { - return self::unsanitizeInputValue($value); - } - } - - /** - * Undo the damage caused by magic_quotes; deprecated in php 5.3 but not removed until php 5.4 - * - * @param string - * @return string modified or not - */ - static public function undoMagicQuotes($value) - { - return version_compare(PHP_VERSION, '5.4', '<') - && get_magic_quotes_gpc() - ? stripslashes($value) - : $value; - } - - /** - * Returns a sanitized variable value from the $_GET and $_POST superglobal. - * If the variable doesn't have a value or an empty value, returns the defaultValue if specified. - * If the variable doesn't have neither a value nor a default value provided, an exception is raised. - * - * @see sanitizeInputValues() for the applied sanitization - * - * @param string $varName name of the variable - * @param string $varDefault default value. If '', and if the type doesn't match, exit() ! - * @param string $varType Expected type, the value must be one of the following: array, int, integer, string, json - * @param array $requestArrayToUse - * - * @throws Exception if the variable type is not known - * or if the variable we want to read doesn't have neither a value nor a default value specified - * - * @return mixed The variable after cleaning - */ - static public function getRequestVar($varName, $varDefault = null, $varType = null, $requestArrayToUse = null) - { - if(is_null($requestArrayToUse)) - { - $requestArrayToUse = $_GET + $_POST; - } - $varDefault = self::sanitizeInputValues( $varDefault ); - if($varType === 'int') - { - // settype accepts only integer - // 'int' is simply a shortcut for 'integer' - $varType = 'integer'; - } - - // there is no value $varName in the REQUEST so we try to use the default value - if(empty($varName) - || !isset($requestArrayToUse[$varName]) - || ( !is_array($requestArrayToUse[$varName]) - && strlen($requestArrayToUse[$varName]) === 0 - ) - ) - { - if( is_null($varDefault)) - { - throw new Exception("The parameter '$varName' isn't set in the Request, and a default value wasn't provided." ); - } - else - { - if( !is_null($varType) - && in_array($varType, array('string', 'integer', 'array')) - ) - { - settype($varDefault, $varType); - } - return $varDefault; - } - } - - // Normal case, there is a value available in REQUEST for the requested varName: - - // we deal w/ json differently - if ($varType == 'json') - { - $value = self::undoMagicQuotes($requestArrayToUse[$varName]); - $value = Piwik_Common::json_decode($value, $assoc = true); - return self::sanitizeInputValues($value, $alreadyStripslashed = true); - } - - $value = self::sanitizeInputValues( $requestArrayToUse[$varName] ); - if( !is_null($varType)) - { - $ok = false; - - if($varType === 'string') - { - if(is_string($value)) $ok = true; - } - elseif($varType === 'integer') - { - if($value == (string)(int)$value) $ok = true; - } - elseif($varType === 'float') - { - if($value == (string)(float)$value) $ok = true; - } - elseif($varType === 'array') - { - if(is_array($value)) $ok = true; - } - else - { - throw new Exception("\$varType specified is not known. It should be one of the following: array, int, integer, float, string"); - } - - // The type is not correct - if($ok === false) - { - if($varDefault === null) - { - throw new Exception("The parameter '$varName' doesn't have a correct type, and a default value wasn't provided."); - } - // we return the default value with the good type set - else - { - settype($varDefault, $varType); - return $varDefault; - } - } - settype($value, $varType); - } - return $value; - } - -/* - * Generating unique strings - */ - - /** - * Returns a 32 characters long uniq ID - * - * @return string 32 chars - */ - static public function generateUniqId() - { - return md5(uniqid(rand(), true)); - } - - /** - * Get salt from [superuser] section - * - * @return string - */ - static public function getSalt() - { - static $salt = null; - if(is_null($salt)) - { - $salt = @Piwik_Config::getInstance()->superuser['salt']; - } - return $salt; - } - - /** - * Configureable hash() algorithm (defaults to md5) - * - * @param string $str String to be hashed - * @param bool $raw_output - * @return string Hash string - */ - static function hash($str, $raw_output = false) - { - static $hashAlgorithm = null; - if(is_null($hashAlgorithm)) - { - $hashAlgorithm = @Piwik_Config::getInstance()->General['hash_algorithm']; - } - - if($hashAlgorithm) - { - $hash = @hash($hashAlgorithm, $str, $raw_output); - if($hash !== false) - return $hash; - } - - return md5($str, $raw_output); - } - - /** - * Generate random string - * - * @param int $length string length - * @param string $alphabet characters allowed in random string - * @return string random string with given length - */ - public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789") - { - $chars = $alphabet; - $str = ''; - - list($usec, $sec) = explode(" ", microtime()); - $seed = ((float)$sec+(float)$usec)*100000; - mt_srand($seed); - - for($i = 0; $i < $length; $i++) - { - $rand_key = mt_rand(0, strlen($chars)-1); - $str .= substr($chars, $rand_key, 1); - } - return str_shuffle($str); - } - -/* - * Conversions - */ - - /** - * Convert hexadecimal representation into binary data. - * !! Will emit warning if input string is not hex!! - * - * @see http://php.net/bin2hex - * - * @param string $str Hexadecimal representation - * @return string - */ - static public function hex2bin($str) - { - return pack("H*" , $str); - } - - /** - * This function will convert the input string to the binary representation of the ID - * but it will throw an Exception if the specified input ID is not correct - * - * This is used when building segments containing visitorId which could be an invalid string - * therefore throwing Unexpected PHP error [pack(): Type H: illegal hex digit i] severity [E_WARNING] - * - * It would be simply to silent fail the pack() call above but in all other cases, we don't expect an error, - * so better be safe and get the php error when something unexpected is happening - * @param string $id - * @throws Exception - * @return string binary string - */ - static public function convertVisitorIdToBin($id) - { - if(strlen($id) !== Piwik_Tracker::LENGTH_HEX_ID_STRING - || @bin2hex(self::hex2bin($id)) != $id) - { - throw new Exception("visitorId is expected to be a ".Piwik_Tracker::LENGTH_HEX_ID_STRING." hex char string"); - } - return self::hex2bin($id); - } - - /** - * Convert IP address (in network address format) to presentation format. - * This is a backward compatibility function for code that only expects - * IPv4 addresses (i.e., doesn't support IPv6). - * - * @see Piwik_IP::N2P() - * - * This function does not support the long (or its string representation) - * returned by the built-in ip2long() function, from Piwik 1.3 and earlier. - * - * @deprecated 1.4 - * - * @param string $ip IP address in network address format - * @return string - */ - static public function long2ip($ip) - { - return Piwik_IP::long2ip($ip); - } - - /** - * Should we use the replacement json_encode/json_decode functions? - * - * @return bool True if broken; false otherwise - */ - static private function useJsonLibrary() - { - static $useLib; - - if (!isset($useLib)) - { - /* - * 5.1.x - doesn't have json extension; we use lib/upgradephp instead - * 5.2 to 5.2.4 - broken in various ways, including: - * - * @see https://bugs.php.net/bug.php?id=38680 'json_decode cannot decode basic types' - * @see https://bugs.php.net/bug.php?id=41403 'json_decode cannot decode floats' - * @see https://bugs.php.net/bug.php?id=42785 'json_encode outputs numbers according to locale' - */ - $useLib = false; - if (version_compare(PHP_VERSION, '5.2.1') < 0) - { - $useLib = true; - } - else if (version_compare(PHP_VERSION, '5.2.5') < 0) - { - $info = localeconv(); - $useLib = $info['decimal_point'] != '.'; - } - } - - return $useLib; - } - - /** - * JSON encode wrapper - * - missing or broken in some php 5.x versions - * - * @param mixed $value - * @return string - */ - static public function json_encode($value) - { - if (self::useJsonLibrary()) - { - return _json_encode($value); - } - - return @json_encode($value); - } - - /** - * JSON decode wrapper - * - missing or broken in some php 5.x versions - * - * @param string $json - * @param bool $assoc - * @return mixed - */ - static public function json_decode($json, $assoc = false) - { - if (self::useJsonLibrary()) - { - return _json_decode($json, $assoc); - } - - return json_decode($json, $assoc); - } - -/* - * DataFiles - */ - - /** - * Returns list of continent codes - * - * @see core/DataFiles/Countries.php - * - * @return array Array of 3 letter continent codes - */ - static public function getContinentsList() - { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php'; - - $continentsList = $GLOBALS['Piwik_ContinentList']; - return $continentsList; - } - - /** - * Returns list of valid country codes - * - * @see core/DataFiles/Countries.php - * - * @param bool $includeInternalCodes - * @return array Array of (2 letter ISO codes => 3 letter continent code) - */ - static public function getCountriesList($includeInternalCodes = false) - { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php'; - - $countriesList = $GLOBALS['Piwik_CountryList']; - $extras = $GLOBALS['Piwik_CountryList_Extras']; - - if($includeInternalCodes) - { - return array_merge($countriesList, $extras); - } - return $countriesList; - } - - /** - * Returns list of valid language codes - * - * @see core/DataFiles/Languages.php - * - * @return array Array of 2 letter ISO codes => Language name (in English) - */ - static public function getLanguagesList() - { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Languages.php'; - - $languagesList = $GLOBALS['Piwik_LanguageList']; - return $languagesList; - } - - /** - * Returns list of language to country mappings - * - * @see core/DataFiles/LanguageToCountry.php - * - * @return array Array of ( 2 letter ISO language codes => 2 letter ISO country codes ) - */ - static public function getLanguageToCountryList() - { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/LanguageToCountry.php'; - - $languagesList = $GLOBALS['Piwik_LanguageToCountry']; - return $languagesList; - } - - /** - * Returns list of search engines by URL - * - * @see core/DataFiles/SearchEngines.php - * - * @return array Array of ( URL => array( searchEngineName, keywordParameter, path, charset ) ) - */ - static public function getSearchEngineUrls() - { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php'; - - $searchEngines = $GLOBALS['Piwik_SearchEngines']; - return $searchEngines; - } - - /** - * Returns list of search engines by name - * - * @see core/DataFiles/SearchEngines.php - * - * @return array Array of ( searchEngineName => URL ) - */ - static public function getSearchEngineNames() - { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php'; - - $searchEngines = $GLOBALS['Piwik_SearchEngines_NameToUrl']; - return $searchEngines; - } - -/* - * Language, country, continent - */ - - /** - * Returns the browser language code, eg. "en-gb,en;q=0.5" - * - * @param string $browserLang Optional browser language, otherwise taken from the request header - * @return string - */ - static public function getBrowserLanguage($browserLang = NULL) - { - static $replacementPatterns = array( - // extraneous bits of RFC 3282 that we ignore - '/(\\\\.)/', // quoted-pairs - '/(\s+)/', // CFWcS white space - '/(\([^)]*\))/', // CFWS comments - '/(;q=[0-9.]+)/', // quality - - // found in the LANG environment variable - '/\.(.*)/', // charset (e.g., en_CA.UTF-8) - '/^C$/', // POSIX 'C' locale - ); - - if(is_null($browserLang)) - { - $browserLang = self::sanitizeInputValues(@$_SERVER['HTTP_ACCEPT_LANGUAGE']); - if(empty($browserLang) && self::isPhpCliMode()) - { - $browserLang = @getenv('LANG'); - } - } - - if(is_null($browserLang)) - { - // a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build) - $browserLang = ""; - } - else - { - // language tags are case-insensitive per HTTP/1.1 s3.10 but the region may be capitalized per ISO3166-1; - // underscores are not permitted per RFC 4646 or 4647 (which obsolete RFC 1766 and 3066), - // but we guard against a bad user agent which naively uses its locale - $browserLang = strtolower(str_replace('_', '-', $browserLang)); - - // filters - $browserLang = preg_replace($replacementPatterns, '', $browserLang); - - $browserLang = preg_replace('/((^|,)chrome:.*)/', '', $browserLang, 1); // Firefox bug - $browserLang = preg_replace('/(,)(?:en-securid,)|(?:(^|,)en-securid(,|$))/', '$1', $browserLang, 1); // unregistered language tag - - $browserLang = str_replace('sr-sp', 'sr-rs', $browserLang); // unofficial (proposed) code in the wild - } - - return $browserLang; - } - - /** - * 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 $enableLanguageToCountryGuess 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, $ip ) - { - $country = null; - Piwik_PostEvent('Common.getCountry', $country, $ip); - if(!empty($country)) - { - return strtolower($country); - } - - if(empty($lang) || strlen($lang) < 2 || $lang == 'xx') - { - return 'xx'; - } - - $validCountries = self::getCountriesList(); - return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess); - } - - /** - * Returns list of valid country codes - * - * @param string $browserLanguage - * @param array $validCountries Array of valid countries - * @param bool $enableLanguageToCountryGuess (if true, will guess country based on language that lacks region information) - * @return array Array of 2 letter ISO codes - */ - static public function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess) - { - $langToCountry = self::getLanguageToCountryList(); - - if($enableLanguageToCountryGuess) - { - if(preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) - { - // match language (without region) to infer the country of origin - if(array_key_exists($matches[1], $langToCountry)) - { - return $langToCountry[$matches[1]]; - } - } - } - - if(!empty($validCountries) && preg_match_all('/[-]([a-z]{2})/', $browserLanguage, $matches, PREG_SET_ORDER)) - { - foreach($matches as $parts) - { - // match location; we don't make any inferences from the language - if(array_key_exists($parts[1], $validCountries)) - { - return $parts[1]; - } - } - } - return 'xx'; - } - - /** - * Returns the visitor language based only on the Browser 'accepted language' information - * - * @param $browserLanguage Browser's accepted langauge header - * @param $validLanguages array of valid language codes - * @return string 2 letter ISO 639 code - */ - static public function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages) - { - // assumes language preference is sorted; - // does not handle language-script-region tags or language range (*) - if(!empty($validLanguages) && preg_match_all('/(?:^|,)([a-z]{2,3})([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) - { - foreach($matches as $parts) - { - if(count($parts) == 3) - { - // match locale (language and location) - if(in_array($parts[1].$parts[2], $validLanguages)) - { - return $parts[1].$parts[2]; - } - } - // match language only (where no region provided) - if(in_array($parts[1], $validLanguages)) - { - return $parts[1]; - } - } - } - return 'xx'; - } - - /** - * Returns the continent of a given country - * - * @param string $country 2 letters isocode - * - * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce) - */ - static public function getContinent($country) - { - $countryList = self::getCountriesList(); - if(isset($countryList[$country])) - { - return $countryList[$country]; - } - return 'unk'; - } - -/* - * Campaign - */ - - /** - * Returns the list of Campaign parameter names that will be read to classify - * a visit as coming from a Campaign - * - * @return array array( - * 0 => array( ... ) // campaign names parameters - * 1 => array( ... ) // campaign keyword parameters - * ); - */ - static public function getCampaignParameters() - { - $return = array( - Piwik_Config::getInstance()->Tracker['campaign_var_name'], - Piwik_Config::getInstance()->Tracker['campaign_keyword_var_name'], - ); - - foreach($return as &$list) - { - if(strpos($list, ',') !== false) - { - $list = explode(',', $list); - } - else - { - $list = array($list); - } - } - - array_walk_recursive($return, 'trim'); - return $return; - } - -/* - * Referrer - */ - - /** - * Reduce URL to more minimal form. 2 letter country codes are - * replaced by '{}', while other parts are simply removed. - * - * Examples: - * www.example.com -> example.com - * search.example.com -> example.com - * m.example.com -> example.com - * de.example.com -> {}.example.com - * example.de -> example.{} - * example.co.uk -> example.{} - * - * @param string $url - * @return string - */ - static public function getLossyUrl($url) - { - static $countries; - if(!isset($countries)) - { - $countries = implode('|', array_keys(self::getCountriesList(true))); - } - - return preg_replace( - array( - '/^(w+[0-9]*|search)\./', - '/(^|\.)m\./', - '/(\.(com|org|net|co|it|edu))?\.('.$countries.')(\/|$)/', - '/(^|\.)('.$countries.')\./', - ), - array( - '', - '$1', - '.{}$4', - '$1{}.', - ), - $url); - } - - /** - * Extracts a keyword from a raw not encoded URL. - * Will only extract keyword if a known search engine has been detected. - * Returns the keyword: - * - in UTF8: automatically converted from other charsets when applicable - * - strtolowered: "QUErY test!" will return "query test!" - * - trimmed: extra spaces before and after are removed - * - * Lists of supported search engines can be found in /core/DataFiles/SearchEngines.php - * The function returns false when a keyword couldn't be found. - * eg. if the url is "http://www.google.com/partners.html" this will return false, - * as the google keyword parameter couldn't be found. - * - * @see unit tests in /tests/core/Common.test.php - * @param string $referrerUrl URL referer URL, eg. $_SERVER['HTTP_REFERER'] - * @return array|false false if a keyword couldn't be extracted, - * or array( - * 'name' => 'Google', - * 'keywords' => 'my searched keywords') - */ - static public function extractSearchEngineInformationFromUrl($referrerUrl) - { - $refererParsed = @parse_url($referrerUrl); - $refererHost = ''; - if(isset($refererParsed['host'])) - { - $refererHost = $refererParsed['host']; - } - if(empty($refererHost)) - { - 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'])) - { - $refererParsed['query'] = ''; - } - $query = $refererParsed['query']; - - // Google Referrers URLs sometimes have the fragment which contains the keyword - if(!empty($refererParsed['fragment'])) - { - $query .= '&' . $refererParsed['fragment']; - } - - $searchEngines = self::getSearchEngineUrls(); - - $hostPattern = self::getLossyUrl($refererHost); - if(array_key_exists($refererHost . $refererPath, $searchEngines)) - { - $refererHost = $refererHost . $refererPath; - } - elseif(array_key_exists($hostPattern . $refererPath, $searchEngines)) - { - $refererHost = $hostPattern . $refererPath; - } - elseif(array_key_exists($hostPattern, $searchEngines)) - { - $refererHost = $hostPattern; - } - elseif(!array_key_exists($refererHost, $searchEngines)) - { - if(!strncmp($query, 'cx=partner-pub-', 15)) - { - // Google custom search engine - $refererHost = 'google.com/cse'; - } - elseif(!strncmp($refererPath, '/pemonitorhosted/ws/results/', 28)) - { - // private-label search powered by InfoSpace Metasearch - $refererHost = 'wsdsold.infospace.com'; - } - elseif(strpos($refererHost, '.images.search.yahoo.com') != false) - { - // Yahoo! Images - $refererHost = 'images.search.yahoo.com'; - } - elseif(strpos($refererHost, '.search.yahoo.com') != false) - { - // Yahoo! - $refererHost = 'search.yahoo.com'; - } - else - { - return false; - } - } - $searchEngineName = $searchEngines[$refererHost][0]; - $variableNames = null; - if(isset($searchEngines[$refererHost][1])) - { - $variableNames = $searchEngines[$refererHost][1]; - } - if(!$variableNames) - { - $searchEngineNames = self::getSearchEngineNames(); - $url = $searchEngineNames[$searchEngineName]; - $variableNames = $searchEngines[$url][1]; - } - if(!is_array($variableNames)) - { - $variableNames = array($variableNames); - } - - $key = null; - if($searchEngineName === 'Google Images' - || ($searchEngineName === 'Google' && strpos($referrerUrl, '/imgres') !== false) ) - { - if (strpos($query, '&prev') !== false) - { - $query = urldecode(trim(self::getParameterFromQueryString($query, 'prev'))); - $query = str_replace('&', '&', strstr($query, '?')); - } - $searchEngineName = 'Google Images'; - } - else if($searchEngineName === 'Google' - && (strpos($query, '&as_') !== false || strpos($query, 'as_') === 0)) - { - $keys = array(); - $key = self::getParameterFromQueryString($query, 'as_q'); - if(!empty($key)) - { - array_push($keys, $key); - } - $key = self::getParameterFromQueryString($query, 'as_oq'); - if(!empty($key)) - { - array_push($keys, str_replace('+', ' OR ', $key)); - } - $key = self::getParameterFromQueryString($query, 'as_epq'); - if(!empty($key)) - { - array_push($keys, "\"$key\""); - } - $key = self::getParameterFromQueryString($query, 'as_eq'); - if(!empty($key)) - { - array_push($keys, "-$key"); - } - $key = trim(urldecode(implode(' ', $keys))); - } - - if ($searchEngineName === 'Google') - { - // top bar menu - $tbm = self::getParameterFromQueryString($query, 'tbm'); - switch ($tbm) - { - case 'isch': - $searchEngineName = 'Google Images'; break; - case 'vid': - $searchEngineName = 'Google Video'; break; - case 'shop': - $searchEngineName = 'Google Shopping'; break; - } - } - - if(empty($key)) - { - foreach($variableNames as $variableName) - { - if($variableName[0] == '/') - { - // regular expression match - if(preg_match($variableName, $referrerUrl, $matches)) - { - $key = trim(urldecode($matches[1])); - break; - } - } - else - { - // search for keywords now &vname=keyword - $key = self::getParameterFromQueryString($query, $variableName); - $key = trim(urldecode($key)); - - // Special case: Google & empty q parameter - if(empty($key) - && $variableName == 'q' - - && ( - // Google search with no keyword - ($searchEngineName == 'Google' - && ( // First, they started putting an empty q= parameter - strpos($query, '&q=') !== false - || strpos($query, '?q=') !== false - // then they started sending the full host only (no path/query string) - || (empty($query) && (empty($refererPath) || $refererPath == '/') && empty($refererParsed['fragment'])) - ) - ) - // search engines with no keyword - || $searchEngineName == 'Google Images' - || $searchEngineName == 'DuckDuckGo') - ) - { - $key = false; - } - if(!empty($key) - || $key === false) - { - break; - } - } - } - } - - // $key === false is the special case "No keyword provided" which is a Search engine match - if($key === null - || $key === '') - { - return false; - } - - if(!empty($key)) - { - if(function_exists('iconv') - && isset($searchEngines[$refererHost][3])) - { - // accepts string, array, or comma-separated list string in preferred order - $charsets = $searchEngines[$refererHost][3]; - if (!is_array($charsets)) - { - $charsets = explode(',', $charsets); - } - - if(!empty($charsets)) - { - $charset = $charsets[0]; - if (count($charsets) > 1 - && function_exists('mb_detect_encoding')) - { - $charset = mb_detect_encoding($key, $charsets); - if ($charset === false) - { - $charset = $charsets[0]; - } - } - - $newkey = @iconv($charset, 'UTF-8//IGNORE', $key); - if(!empty($newkey)) - { - $key = $newkey; - } - } - } - - $key = self::mb_strtolower($key); - } - - return array( - 'name' => $searchEngineName, - 'keywords' => $key, - ); - } - -/* - * System environment - */ - - /** - * Returns true if PHP was invoked from command-line interface (shell) - * - * @since added in 0.4.4 - * @return bool true if PHP invoked as a CGI or from CLI - */ - static public function isPhpCliMode() - { - $remoteAddr = @$_SERVER['REMOTE_ADDR']; - return PHP_SAPI == 'cli' || - (!strncmp(PHP_SAPI, 'cgi', 3) && empty($remoteAddr)); - } - - /** - * Is the current script execution triggered by misc/cron/archive.php ? - * - * Helpful for error handling: directly throw error without HTML (eg. when DB is down) - * @return bool - */ - static public function isArchivePhpTriggered() - { - return !empty($_GET['trigger']) - && $_GET['trigger'] == 'archivephp'; - } - - /** - * Assign CLI parameters as if they were REQUEST or GET parameters. - * You can trigger Piwik from the command line by - * # /usr/bin/php5 /path/to/piwik/index.php -- "module=API&method=Actions.getActions&idSite=1&period=day&date=previous8&format=php" - */ - static public function assignCliParametersToRequest() - { - if(isset($_SERVER['argc']) - && $_SERVER['argc'] > 0) - { - for ($i=1; $i < $_SERVER['argc']; $i++) - { - parse_str($_SERVER['argv'][$i],$tmp); - $_GET = array_merge($_GET, $tmp); - } - } - } - - /** - * Returns true if running on a Windows operating system - * - * @since added in 0.6.5 - * @return bool true if PHP detects it is running on Windows; else false - */ - static public function isWindows() - { - return DIRECTORY_SEPARATOR === '\\'; - } - - /** - * Returns true if running on MacOS - * - * @return bool true if PHP detects it is running on MacOS; else false - */ - static public function isMacOS() - { - return PHP_OS === 'Darwin'; - } - - /** - * Returns true if running on an Apache web server - * - * @return bool - */ - static public function isApache() - { - $apache = isset($_SERVER['SERVER_SOFTWARE']) && - !strncmp($_SERVER['SERVER_SOFTWARE'], 'Apache', 6); - - return $apache; - } - - /** - * Returns true if running on Microsoft IIS 7 (or above) - * - * @return bool - */ - static public function isIIS() - { - $iis = isset($_SERVER['SERVER_SOFTWARE']) && - preg_match('/^Microsoft-IIS\/(.+)/', $_SERVER['SERVER_SOFTWARE'], $matches) && - version_compare($matches[1], '7') >= 0; - - return $iis; - } - - /** - * Takes a list of fields defining numeric values and returns the corresponding - * unnamed parameters to be bound to the field names in the where clause of a SQL query - * - * @param array|string $fields array( fieldName1, fieldName2, fieldName3) Names of the mysql table fields to load - * @return string "?, ?, ?" - */ - public static function getSqlStringFieldsArray( $fields ) - { - if(is_string($fields)) - { - $fields = array($fields); - } - $count = count($fields); - if($count == 0) - { - return "''"; - } - return '?'.str_repeat(',?', $count-1); - } - - /** - * Sets outgoing header. - * - * @param string $header The header. - * @param bool $replace Whether to replace existing or not. - */ - public static function sendHeader( $header, $replace = true ) - { - if (isset($GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING']) && $GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING']) - { - @header($header, $replace); - } - else - { - header($header, $replace); - } - } - - /** - * Returns the ID of the current LocationProvider (see UserCountry plugin code) from - * the Tracker cache. - */ - public static function getCurrentLocationProviderId() - { - $cache = Piwik_Tracker_Cache::getCacheGeneral(); - return empty($cache['currentLocationProviderId']) - ? Piwik_UserCountry_LocationProvider_Default::ID - : $cache['currentLocationProviderId']; - } + /** + * Const used to map the referer type to an integer in the log_visit table + */ + const REFERER_TYPE_DIRECT_ENTRY = 1; + const REFERER_TYPE_SEARCH_ENGINE = 2; + const REFERER_TYPE_WEBSITE = 3; + const REFERER_TYPE_CAMPAIGN = 6; + + /** + * Flag used with htmlspecialchar + * See php.net/htmlspecialchars + */ + const HTML_ENCODING_QUOTE_STYLE = ENT_QUOTES; + + /* + * Database + */ + + /** + * Hashes a string into an integer which should be very low collision risks + * @param string $string String to hash + * @return int Resulting int hash + */ + static public function hashStringToInt($string) + { + $stringHash = substr(md5($string), 0, 8); + return base_convert($stringHash, 16, 10); + } + + static public $cachedTablePrefix = null; + + /** + * Returns the table name prefixed by the table prefix. + * Works in both Tracker and UI mode. + * + * @param string $table The table name to prefix, ie "log_visit" + * @return string The table name prefixed, ie "piwik-production_log_visit" + */ + static public function prefixTable($table) + { + if (is_null(self::$cachedTablePrefix)) { + self::$cachedTablePrefix = Piwik_Config::getInstance()->database['tables_prefix']; + } + return self::$cachedTablePrefix . $table; + } + + /** + * Returns an array containing the prefixed table names of every passed argument. + * + * @param string ... The table names to prefix, ie "log_visit" + * @return array The prefixed names in an array. + */ + static public function prefixTables() + { + $result = array(); + foreach (func_get_args() as $table) { + $result[] = self::prefixTable($table); + } + return $result; + } + + /** + * Returns the table name, after removing the table prefix + * + * @param string $table + * @return string + */ + static public function unprefixTable($table) + { + static $prefixTable = null; + if (is_null($prefixTable)) { + $prefixTable = Piwik_Config::getInstance()->database['tables_prefix']; + } + if (empty($prefixTable) + || strpos($table, $prefixTable) !== 0 + ) { + return $table; + } + $count = 1; + return str_replace($prefixTable, '', $table, $count); + } + + /* + * Tracker + */ + static public function isGoalPluginEnabled() + { + return Piwik_PluginsManager::getInstance()->isPluginActivated('Goals'); + } + + /* + * URLs + */ + + /** + * 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 + * + * @param string $url either http://piwik.org/test or / + * @return string + */ + static public function getPathAndQueryFromUrl($url) + { + $parsedUrl = parse_url($url); + $result = ''; + if (isset($parsedUrl['path'])) { + $result .= substr($parsedUrl['path'], 1); + } + if (isset($parsedUrl['query'])) { + $result .= '?' . $parsedUrl['query']; + } + return $result; + } + + /** + * Returns the value of a GET parameter $parameter in an URL query $urlQuery + * + * @param string $urlQuery result of parse_url()['query'] and htmlentitied (& is &) eg. module=test&action=toto or ?page=test + * @param string $parameter + * @return string|bool Parameter value if found (can be the empty string!), null if not found + */ + static public function getParameterFromQueryString($urlQuery, $parameter) + { + $nameToValue = self::getArrayFromQueryString($urlQuery); + if (isset($nameToValue[$parameter])) { + return $nameToValue[$parameter]; + } + return null; + } + + /** + * Returns an URL query string in an array format + * + * @param string $urlQuery + * @return array array( param1=> value1, param2=>value2) + */ + static public function getArrayFromQueryString($urlQuery) + { + if (strlen($urlQuery) == 0) { + return array(); + } + if ($urlQuery[0] == '?') { + $urlQuery = substr($urlQuery, 1); + } + $separator = '&'; + + $urlQuery = $separator . $urlQuery; + // $urlQuery = str_replace(array('%20'), ' ', $urlQuery); + $refererQuery = trim($urlQuery); + + $values = explode($separator, $refererQuery); + + $nameToValue = array(); + + foreach ($values as $value) { + $pos = strpos($value, '='); + if ($pos !== false) { + $name = substr($value, 0, $pos); + $value = substr($value, $pos + 1); + if ($value === false) { + $value = ''; + } + } else { + $name = $value; + $value = false; + } + if (!empty($name)) { + $name = Piwik_Common::sanitizeInputValue($name); + } + if (!empty($value)) { + $value = Piwik_Common::sanitizeInputValue($value); + } + + // if array without indexes + $count = 0; + $tmp = preg_replace('/(\[|%5b)(]|%5d)$/i', '', $name, -1, $count); + if (!empty($tmp) && $count) { + $name = $tmp; + if (isset($nameToValue[$name]) == false || is_array($nameToValue[$name]) == false) { + $nameToValue[$name] = array(); + } + array_push($nameToValue[$name], $value); + } else if (!empty($name)) { + $nameToValue[$name] = $value; + } + } + return $nameToValue; + } + + /** + * Builds a URL from the result of parse_url function + * Copied from the PHP comments at http://php.net/parse_url + * @param array $parsed + * @return bool|string + */ + static public function getParseUrlReverse($parsed) + { + if (!is_array($parsed)) { + return false; + } + + $uri = !empty($parsed['scheme']) ? $parsed['scheme'] . ':' . (!strcasecmp($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 .= (!strncmp($parsed['path'], '/', 1)) + ? $parsed['path'] + : ((!empty($uri) ? '/' : '') . $parsed['path']); + } + + $uri .= !empty($parsed['query']) ? '?' . $parsed['query'] : ''; + $uri .= !empty($parsed['fragment']) ? '#' . $parsed['fragment'] : ''; + return $uri; + } + + /** + * Returns true if the string passed may be a URL. + * We don't need a precise test here because the value comes from the website + * tracked source code and the URLs may look very strange. + * + * @param string $url + * @return bool + */ + static public function isLookLikeUrl($url) + { + return preg_match('~^(ftp|news|http|https)?://(.*)$~D', $url, $matches) !== 0 + && strlen($matches[2]) > 0; + } + + /* + * File operations + */ + + /** + * ending WITHOUT slash + * + * @return string + */ + static public function getPathToPiwikRoot() + { + return realpath(dirname(__FILE__) . "/.."); + } + + /** + * Create directory if permitted + * + * @param string $path + * @param bool $denyAccess + */ + static public function mkdir($path, $denyAccess = true) + { + if (!is_dir($path)) { + // the mode in mkdir is modified by the current umask + @mkdir($path, $mode = 0755, $recursive = true); + } + + // try to overcome restrictive umask (mis-)configuration + if (!is_writable($path)) { + @chmod($path, 0755); + if (!is_writable($path)) { + @chmod($path, 0775); + + // enough! we're not going to make the directory world-writeable + } + } + + if ($denyAccess) { + self::createHtAccess($path, $overwrite = false); + } + } + + /** + * Create .htaccess file in specified directory + * + * Apache-specific; for IIS @see web.config + * + * @param string $path without trailing slash + * @param bool $overwrite whether to overwrite an existing file or not + * @param string $content + */ + static public function createHtAccess($path, $overwrite = true, $content = "<Files \"*\">\n<IfModule mod_access.c>\nDeny from all\n</IfModule>\n<IfModule !mod_access_compat>\n<IfModule mod_authz_host.c>\nDeny from all\n</IfModule>\n</IfModule>\n<IfModule mod_access_compat>\nDeny from all\n</IfModule>\n</Files>\n") + { + if (self::isApache()) { + $file = $path . '/.htaccess'; + if ($overwrite || !file_exists($file)) { + @file_put_contents($file, $content); + } + } + } + + /** + * Get canonicalized absolute path + * See http://php.net/realpath + * + * @param string $path + * @return string canonicalized absolute path + */ + static public function realpath($path) + { + if (file_exists($path)) { + return realpath($path); + } + return $path; + } + + /** + * Returns true if the string is a valid filename + * File names that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), and dot(.) will be accepted. + * File names beginning with anything but a-Z or 0-9 will be rejected (including .htaccess for example). + * File names containing anything other than above mentioned will also be rejected (file names with spaces won't be accepted). + * + * @param string $filename + * @return bool + * + */ + static public function isValidFilename($filename) + { + return (0 !== preg_match('/(^[a-zA-Z0-9]+([a-zA-Z_0-9.-]*))$/D', $filename)); + } + + /* + * String operations + */ + + /** + * byte-oriented substr() - ASCII + * + * @param string $string + * @param int $start + * @param int ... optional length + * @return string + */ + static public function substr($string, $start) + { + // in case mbstring overloads substr function + $substr = function_exists('mb_orig_substr') ? 'mb_orig_substr' : 'substr'; + + $length = func_num_args() > 2 + ? func_get_arg(2) + : self::strlen($string); + + return $substr($string, $start, $length); + } + + /** + * byte-oriented strlen() - ASCII + * + * @param string $string + * @return int + */ + static public function strlen($string) + { + // in case mbstring overloads strlen function + $strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen'; + return $strlen($string); + } + + /** + * multi-byte substr() - UTF-8 + * + * @param string $string + * @param int $start + * @param int ... optional length + * @return string + */ + static public function mb_substr($string, $start) + { + $length = func_num_args() > 2 + ? func_get_arg(2) + : self::mb_strlen($string); + + if (function_exists('mb_substr')) { + return mb_substr($string, $start, $length, 'UTF-8'); + } + + return substr($string, $start, $length); + } + + /** + * multi-byte strlen() - UTF-8 + * + * @param string $string + * @return int + */ + static public function mb_strlen($string) + { + if (function_exists('mb_strlen')) { + return mb_strlen($string, 'UTF-8'); + } + + return strlen($string); + } + + /** + * multi-byte strtolower() - UTF-8 + * + * @param string $string + * @return string + */ + static public function mb_strtolower($string) + { + if (function_exists('mb_strtolower')) { + return mb_strtolower($string, 'UTF-8'); + } + + return strtolower($string); + } + + /* + * Escaping input + */ + + /** + * Returns the variable after cleaning operations. + * NB: The variable still has to be escaped before going into a SQL Query! + * + * If an array is passed the cleaning is done recursively on all the sub-arrays. + * The array's keys are filtered as well! + * + * How this method works: + * - The variable returned has been htmlspecialchars to avoid the XSS security problem. + * - The single quotes are not protected so "Piwik's amazing" will still be "Piwik's amazing". + * + * - Transformations are: + * - '&' (ampersand) becomes '&' + * - '"'(double quote) becomes '"' + * - '<' (less than) becomes '<' + * - '>' (greater than) becomes '>' + * - It handles the magic_quotes setting. + * - A non string value is returned without modification + * + * @param mixed $value The variable to be cleaned + * @param bool $alreadyStripslashed + * @throws Exception + * @return mixed The variable after cleaning + */ + static public function sanitizeInputValues($value, $alreadyStripslashed = false) + { + if (is_numeric($value)) { + return $value; + } elseif (is_string($value)) { + $value = self::sanitizeInputValue($value); + + if (!$alreadyStripslashed) // a JSON array was already stripslashed, don't do it again for each value + { + $value = self::undoMagicQuotes($value); + } + } elseif (is_array($value)) { + foreach (array_keys($value) as $key) { + $newKey = $key; + $newKey = self::sanitizeInputValues($newKey, $alreadyStripslashed); + if ($key != $newKey) { + $value[$newKey] = $value[$key]; + unset($value[$key]); + } + + $value[$newKey] = self::sanitizeInputValues($value[$newKey], $alreadyStripslashed); + } + } elseif (!is_null($value) + && !is_bool($value) + ) { + throw new Exception("The value to escape has not a supported type. Value = " . var_export($value, true)); + } + return $value; + } + + /** + * Sanitize a single input value + * + * @param string $value + * @return string sanitized input + */ + static public function sanitizeInputValue($value) + { + // $_GET and $_REQUEST already urldecode()'d + // decode + // note: before php 5.2.7, htmlspecialchars() double encodes &#x hex items + $value = html_entity_decode($value, Piwik_Common::HTML_ENCODING_QUOTE_STYLE, 'UTF-8'); + + // filter + $value = str_replace(array("\n", "\r", "\0"), '', $value); + + // escape + $tmp = @htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8'); + + // note: php 5.2.5 and above, htmlspecialchars is destructive if input is not UTF-8 + if ($value != '' && $tmp == '') { + // convert and escape + $value = utf8_encode($value); + $tmp = htmlspecialchars($value, self::HTML_ENCODING_QUOTE_STYLE, 'UTF-8'); + } + return $tmp; + } + + /** + * Unsanitize a single input value + * + * @param string $value + * @return string unsanitized input + */ + static public function unsanitizeInputValue($value) + { + return htmlspecialchars_decode($value, self::HTML_ENCODING_QUOTE_STYLE); + } + + /** + * Unsanitize one or more values. + * + * @param string|array $value + * @return string|array unsanitized input + */ + static public function unsanitizeInputValues($value) + { + if (is_array($value)) { + $result = array(); + foreach ($value as $key => $arrayValue) { + $result[$key] = self::unsanitizeInputValues($arrayValue); + } + return $result; + } else { + return self::unsanitizeInputValue($value); + } + } + + /** + * Undo the damage caused by magic_quotes; deprecated in php 5.3 but not removed until php 5.4 + * + * @param string + * @return string modified or not + */ + static public function undoMagicQuotes($value) + { + return version_compare(PHP_VERSION, '5.4', '<') + && get_magic_quotes_gpc() + ? stripslashes($value) + : $value; + } + + /** + * Returns a sanitized variable value from the $_GET and $_POST superglobal. + * If the variable doesn't have a value or an empty value, returns the defaultValue if specified. + * If the variable doesn't have neither a value nor a default value provided, an exception is raised. + * + * @see sanitizeInputValues() for the applied sanitization + * + * @param string $varName name of the variable + * @param string $varDefault default value. If '', and if the type doesn't match, exit() ! + * @param string $varType Expected type, the value must be one of the following: array, int, integer, string, json + * @param array $requestArrayToUse + * + * @throws Exception if the variable type is not known + * or if the variable we want to read doesn't have neither a value nor a default value specified + * + * @return mixed The variable after cleaning + */ + static public function getRequestVar($varName, $varDefault = null, $varType = null, $requestArrayToUse = null) + { + if (is_null($requestArrayToUse)) { + $requestArrayToUse = $_GET + $_POST; + } + $varDefault = self::sanitizeInputValues($varDefault); + if ($varType === 'int') { + // settype accepts only integer + // 'int' is simply a shortcut for 'integer' + $varType = 'integer'; + } + + // there is no value $varName in the REQUEST so we try to use the default value + if (empty($varName) + || !isset($requestArrayToUse[$varName]) + || (!is_array($requestArrayToUse[$varName]) + && strlen($requestArrayToUse[$varName]) === 0 + ) + ) { + if (is_null($varDefault)) { + throw new Exception("The parameter '$varName' isn't set in the Request, and a default value wasn't provided."); + } else { + if (!is_null($varType) + && in_array($varType, array('string', 'integer', 'array')) + ) { + settype($varDefault, $varType); + } + return $varDefault; + } + } + + // Normal case, there is a value available in REQUEST for the requested varName: + + // we deal w/ json differently + if ($varType == 'json') { + $value = self::undoMagicQuotes($requestArrayToUse[$varName]); + $value = Piwik_Common::json_decode($value, $assoc = true); + return self::sanitizeInputValues($value, $alreadyStripslashed = true); + } + + $value = self::sanitizeInputValues($requestArrayToUse[$varName]); + if (!is_null($varType)) { + $ok = false; + + if ($varType === 'string') { + if (is_string($value)) $ok = true; + } elseif ($varType === 'integer') { + if ($value == (string)(int)$value) $ok = true; + } elseif ($varType === 'float') { + if ($value == (string)(float)$value) $ok = true; + } elseif ($varType === 'array') { + if (is_array($value)) $ok = true; + } else { + throw new Exception("\$varType specified is not known. It should be one of the following: array, int, integer, float, string"); + } + + // The type is not correct + if ($ok === false) { + if ($varDefault === null) { + throw new Exception("The parameter '$varName' doesn't have a correct type, and a default value wasn't provided."); + } // we return the default value with the good type set + else { + settype($varDefault, $varType); + return $varDefault; + } + } + settype($value, $varType); + } + return $value; + } + + /* + * Generating unique strings + */ + + /** + * Returns a 32 characters long uniq ID + * + * @return string 32 chars + */ + static public function generateUniqId() + { + return md5(uniqid(rand(), true)); + } + + /** + * Get salt from [superuser] section + * + * @return string + */ + static public function getSalt() + { + static $salt = null; + if (is_null($salt)) { + $salt = @Piwik_Config::getInstance()->superuser['salt']; + } + return $salt; + } + + /** + * Configureable hash() algorithm (defaults to md5) + * + * @param string $str String to be hashed + * @param bool $raw_output + * @return string Hash string + */ + static function hash($str, $raw_output = false) + { + static $hashAlgorithm = null; + if (is_null($hashAlgorithm)) { + $hashAlgorithm = @Piwik_Config::getInstance()->General['hash_algorithm']; + } + + if ($hashAlgorithm) { + $hash = @hash($hashAlgorithm, $str, $raw_output); + if ($hash !== false) + return $hash; + } + + return md5($str, $raw_output); + } + + /** + * Generate random string + * + * @param int $length string length + * @param string $alphabet characters allowed in random string + * @return string random string with given length + */ + public static function getRandomString($length = 16, $alphabet = "abcdefghijklmnoprstuvwxyz0123456789") + { + $chars = $alphabet; + $str = ''; + + list($usec, $sec) = explode(" ", microtime()); + $seed = ((float)$sec + (float)$usec) * 100000; + mt_srand($seed); + + for ($i = 0; $i < $length; $i++) { + $rand_key = mt_rand(0, strlen($chars) - 1); + $str .= substr($chars, $rand_key, 1); + } + return str_shuffle($str); + } + + /* + * Conversions + */ + + /** + * Convert hexadecimal representation into binary data. + * !! Will emit warning if input string is not hex!! + * + * @see http://php.net/bin2hex + * + * @param string $str Hexadecimal representation + * @return string + */ + static public function hex2bin($str) + { + return pack("H*", $str); + } + + /** + * This function will convert the input string to the binary representation of the ID + * but it will throw an Exception if the specified input ID is not correct + * + * This is used when building segments containing visitorId which could be an invalid string + * therefore throwing Unexpected PHP error [pack(): Type H: illegal hex digit i] severity [E_WARNING] + * + * It would be simply to silent fail the pack() call above but in all other cases, we don't expect an error, + * so better be safe and get the php error when something unexpected is happening + * @param string $id + * @throws Exception + * @return string binary string + */ + static public function convertVisitorIdToBin($id) + { + if (strlen($id) !== Piwik_Tracker::LENGTH_HEX_ID_STRING + || @bin2hex(self::hex2bin($id)) != $id + ) { + throw new Exception("visitorId is expected to be a " . Piwik_Tracker::LENGTH_HEX_ID_STRING . " hex char string"); + } + return self::hex2bin($id); + } + + /** + * Convert IP address (in network address format) to presentation format. + * This is a backward compatibility function for code that only expects + * IPv4 addresses (i.e., doesn't support IPv6). + * + * @see Piwik_IP::N2P() + * + * This function does not support the long (or its string representation) + * returned by the built-in ip2long() function, from Piwik 1.3 and earlier. + * + * @deprecated 1.4 + * + * @param string $ip IP address in network address format + * @return string + */ + static public function long2ip($ip) + { + return Piwik_IP::long2ip($ip); + } + + /** + * Should we use the replacement json_encode/json_decode functions? + * + * @return bool True if broken; false otherwise + */ + static private function useJsonLibrary() + { + static $useLib; + + if (!isset($useLib)) { + /* + * 5.1.x - doesn't have json extension; we use lib/upgradephp instead + * 5.2 to 5.2.4 - broken in various ways, including: + * + * @see https://bugs.php.net/bug.php?id=38680 'json_decode cannot decode basic types' + * @see https://bugs.php.net/bug.php?id=41403 'json_decode cannot decode floats' + * @see https://bugs.php.net/bug.php?id=42785 'json_encode outputs numbers according to locale' + */ + $useLib = false; + if (version_compare(PHP_VERSION, '5.2.1') < 0) { + $useLib = true; + } else if (version_compare(PHP_VERSION, '5.2.5') < 0) { + $info = localeconv(); + $useLib = $info['decimal_point'] != '.'; + } + } + + return $useLib; + } + + /** + * JSON encode wrapper + * - missing or broken in some php 5.x versions + * + * @param mixed $value + * @return string + */ + static public function json_encode($value) + { + if (self::useJsonLibrary()) { + return _json_encode($value); + } + + return @json_encode($value); + } + + /** + * JSON decode wrapper + * - missing or broken in some php 5.x versions + * + * @param string $json + * @param bool $assoc + * @return mixed + */ + static public function json_decode($json, $assoc = false) + { + if (self::useJsonLibrary()) { + return _json_decode($json, $assoc); + } + + return json_decode($json, $assoc); + } + + /* + * DataFiles + */ + + /** + * Returns list of continent codes + * + * @see core/DataFiles/Countries.php + * + * @return array Array of 3 letter continent codes + */ + static public function getContinentsList() + { + require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php'; + + $continentsList = $GLOBALS['Piwik_ContinentList']; + return $continentsList; + } + + /** + * Returns list of valid country codes + * + * @see core/DataFiles/Countries.php + * + * @param bool $includeInternalCodes + * @return array Array of (2 letter ISO codes => 3 letter continent code) + */ + static public function getCountriesList($includeInternalCodes = false) + { + require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php'; + + $countriesList = $GLOBALS['Piwik_CountryList']; + $extras = $GLOBALS['Piwik_CountryList_Extras']; + + if ($includeInternalCodes) { + return array_merge($countriesList, $extras); + } + return $countriesList; + } + + /** + * Returns list of valid language codes + * + * @see core/DataFiles/Languages.php + * + * @return array Array of 2 letter ISO codes => Language name (in English) + */ + static public function getLanguagesList() + { + require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Languages.php'; + + $languagesList = $GLOBALS['Piwik_LanguageList']; + return $languagesList; + } + + /** + * Returns list of language to country mappings + * + * @see core/DataFiles/LanguageToCountry.php + * + * @return array Array of ( 2 letter ISO language codes => 2 letter ISO country codes ) + */ + static public function getLanguageToCountryList() + { + require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/LanguageToCountry.php'; + + $languagesList = $GLOBALS['Piwik_LanguageToCountry']; + return $languagesList; + } + + /** + * Returns list of search engines by URL + * + * @see core/DataFiles/SearchEngines.php + * + * @return array Array of ( URL => array( searchEngineName, keywordParameter, path, charset ) ) + */ + static public function getSearchEngineUrls() + { + require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php'; + + $searchEngines = $GLOBALS['Piwik_SearchEngines']; + return $searchEngines; + } + + /** + * Returns list of search engines by name + * + * @see core/DataFiles/SearchEngines.php + * + * @return array Array of ( searchEngineName => URL ) + */ + static public function getSearchEngineNames() + { + require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php'; + + $searchEngines = $GLOBALS['Piwik_SearchEngines_NameToUrl']; + return $searchEngines; + } + + /* + * Language, country, continent + */ + + /** + * Returns the browser language code, eg. "en-gb,en;q=0.5" + * + * @param string $browserLang Optional browser language, otherwise taken from the request header + * @return string + */ + static public function getBrowserLanguage($browserLang = NULL) + { + static $replacementPatterns = array( + // extraneous bits of RFC 3282 that we ignore + '/(\\\\.)/', // quoted-pairs + '/(\s+)/', // CFWcS white space + '/(\([^)]*\))/', // CFWS comments + '/(;q=[0-9.]+)/', // quality + + // found in the LANG environment variable + '/\.(.*)/', // charset (e.g., en_CA.UTF-8) + '/^C$/', // POSIX 'C' locale + ); + + if (is_null($browserLang)) { + $browserLang = self::sanitizeInputValues(@$_SERVER['HTTP_ACCEPT_LANGUAGE']); + if (empty($browserLang) && self::isPhpCliMode()) { + $browserLang = @getenv('LANG'); + } + } + + if (is_null($browserLang)) { + // a fallback might be to infer the language in HTTP_USER_AGENT (i.e., localized build) + $browserLang = ""; + } else { + // language tags are case-insensitive per HTTP/1.1 s3.10 but the region may be capitalized per ISO3166-1; + // underscores are not permitted per RFC 4646 or 4647 (which obsolete RFC 1766 and 3066), + // but we guard against a bad user agent which naively uses its locale + $browserLang = strtolower(str_replace('_', '-', $browserLang)); + + // filters + $browserLang = preg_replace($replacementPatterns, '', $browserLang); + + $browserLang = preg_replace('/((^|,)chrome:.*)/', '', $browserLang, 1); // Firefox bug + $browserLang = preg_replace('/(,)(?:en-securid,)|(?:(^|,)en-securid(,|$))/', '$1', $browserLang, 1); // unregistered language tag + + $browserLang = str_replace('sr-sp', 'sr-rs', $browserLang); // unofficial (proposed) code in the wild + } + + return $browserLang; + } + + /** + * 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 $enableLanguageToCountryGuess 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, $ip) + { + $country = null; + Piwik_PostEvent('Common.getCountry', $country, $ip); + if (!empty($country)) { + return strtolower($country); + } + + if (empty($lang) || strlen($lang) < 2 || $lang == 'xx') { + return 'xx'; + } + + $validCountries = self::getCountriesList(); + return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess); + } + + /** + * Returns list of valid country codes + * + * @param string $browserLanguage + * @param array $validCountries Array of valid countries + * @param bool $enableLanguageToCountryGuess (if true, will guess country based on language that lacks region information) + * @return array Array of 2 letter ISO codes + */ + static public function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess) + { + $langToCountry = self::getLanguageToCountryList(); + + if ($enableLanguageToCountryGuess) { + if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) { + // match language (without region) to infer the country of origin + if (array_key_exists($matches[1], $langToCountry)) { + return $langToCountry[$matches[1]]; + } + } + } + + if (!empty($validCountries) && preg_match_all('/[-]([a-z]{2})/', $browserLanguage, $matches, PREG_SET_ORDER)) { + foreach ($matches as $parts) { + // match location; we don't make any inferences from the language + if (array_key_exists($parts[1], $validCountries)) { + return $parts[1]; + } + } + } + return 'xx'; + } + + /** + * Returns the visitor language based only on the Browser 'accepted language' information + * + * @param $browserLanguage Browser's accepted langauge header + * @param $validLanguages array of valid language codes + * @return string 2 letter ISO 639 code + */ + static public function extractLanguageCodeFromBrowserLanguage($browserLanguage, $validLanguages) + { + // assumes language preference is sorted; + // does not handle language-script-region tags or language range (*) + if (!empty($validLanguages) && preg_match_all('/(?:^|,)([a-z]{2,3})([-][a-z]{2})?/', $browserLanguage, $matches, PREG_SET_ORDER)) { + foreach ($matches as $parts) { + if (count($parts) == 3) { + // match locale (language and location) + if (in_array($parts[1] . $parts[2], $validLanguages)) { + return $parts[1] . $parts[2]; + } + } + // match language only (where no region provided) + if (in_array($parts[1], $validLanguages)) { + return $parts[1]; + } + } + } + return 'xx'; + } + + /** + * Returns the continent of a given country + * + * @param string $country 2 letters isocode + * + * @return string Continent (3 letters code : afr, asi, eur, amn, ams, oce) + */ + static public function getContinent($country) + { + $countryList = self::getCountriesList(); + if (isset($countryList[$country])) { + return $countryList[$country]; + } + return 'unk'; + } + + /* + * Campaign + */ + + /** + * Returns the list of Campaign parameter names that will be read to classify + * a visit as coming from a Campaign + * + * @return array array( + * 0 => array( ... ) // campaign names parameters + * 1 => array( ... ) // campaign keyword parameters + * ); + */ + static public function getCampaignParameters() + { + $return = array( + Piwik_Config::getInstance()->Tracker['campaign_var_name'], + Piwik_Config::getInstance()->Tracker['campaign_keyword_var_name'], + ); + + foreach ($return as &$list) { + if (strpos($list, ',') !== false) { + $list = explode(',', $list); + } else { + $list = array($list); + } + } + + array_walk_recursive($return, 'trim'); + return $return; + } + + /* + * Referrer + */ + + /** + * Reduce URL to more minimal form. 2 letter country codes are + * replaced by '{}', while other parts are simply removed. + * + * Examples: + * www.example.com -> example.com + * search.example.com -> example.com + * m.example.com -> example.com + * de.example.com -> {}.example.com + * example.de -> example.{} + * example.co.uk -> example.{} + * + * @param string $url + * @return string + */ + static public function getLossyUrl($url) + { + static $countries; + if (!isset($countries)) { + $countries = implode('|', array_keys(self::getCountriesList(true))); + } + + return preg_replace( + array( + '/^(w+[0-9]*|search)\./', + '/(^|\.)m\./', + '/(\.(com|org|net|co|it|edu))?\.(' . $countries . ')(\/|$)/', + '/(^|\.)(' . $countries . ')\./', + ), + array( + '', + '$1', + '.{}$4', + '$1{}.', + ), + $url); + } + + /** + * Extracts a keyword from a raw not encoded URL. + * Will only extract keyword if a known search engine has been detected. + * Returns the keyword: + * - in UTF8: automatically converted from other charsets when applicable + * - strtolowered: "QUErY test!" will return "query test!" + * - trimmed: extra spaces before and after are removed + * + * Lists of supported search engines can be found in /core/DataFiles/SearchEngines.php + * The function returns false when a keyword couldn't be found. + * eg. if the url is "http://www.google.com/partners.html" this will return false, + * as the google keyword parameter couldn't be found. + * + * @see unit tests in /tests/core/Common.test.php + * @param string $referrerUrl URL referer URL, eg. $_SERVER['HTTP_REFERER'] + * @return array|false false if a keyword couldn't be extracted, + * or array( + * 'name' => 'Google', + * 'keywords' => 'my searched keywords') + */ + static public function extractSearchEngineInformationFromUrl($referrerUrl) + { + $refererParsed = @parse_url($referrerUrl); + $refererHost = ''; + if (isset($refererParsed['host'])) { + $refererHost = $refererParsed['host']; + } + if (empty($refererHost)) { + 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'])) { + $refererParsed['query'] = ''; + } + $query = $refererParsed['query']; + + // Google Referrers URLs sometimes have the fragment which contains the keyword + if (!empty($refererParsed['fragment'])) { + $query .= '&' . $refererParsed['fragment']; + } + + $searchEngines = self::getSearchEngineUrls(); + + $hostPattern = self::getLossyUrl($refererHost); + if (array_key_exists($refererHost . $refererPath, $searchEngines)) { + $refererHost = $refererHost . $refererPath; + } elseif (array_key_exists($hostPattern . $refererPath, $searchEngines)) { + $refererHost = $hostPattern . $refererPath; + } elseif (array_key_exists($hostPattern, $searchEngines)) { + $refererHost = $hostPattern; + } elseif (!array_key_exists($refererHost, $searchEngines)) { + if (!strncmp($query, 'cx=partner-pub-', 15)) { + // Google custom search engine + $refererHost = 'google.com/cse'; + } elseif (!strncmp($refererPath, '/pemonitorhosted/ws/results/', 28)) { + // private-label search powered by InfoSpace Metasearch + $refererHost = 'wsdsold.infospace.com'; + } elseif (strpos($refererHost, '.images.search.yahoo.com') != false) { + // Yahoo! Images + $refererHost = 'images.search.yahoo.com'; + } elseif (strpos($refererHost, '.search.yahoo.com') != false) { + // Yahoo! + $refererHost = 'search.yahoo.com'; + } else { + return false; + } + } + $searchEngineName = $searchEngines[$refererHost][0]; + $variableNames = null; + if (isset($searchEngines[$refererHost][1])) { + $variableNames = $searchEngines[$refererHost][1]; + } + if (!$variableNames) { + $searchEngineNames = self::getSearchEngineNames(); + $url = $searchEngineNames[$searchEngineName]; + $variableNames = $searchEngines[$url][1]; + } + if (!is_array($variableNames)) { + $variableNames = array($variableNames); + } + + $key = null; + if ($searchEngineName === 'Google Images' + || ($searchEngineName === 'Google' && strpos($referrerUrl, '/imgres') !== false) + ) { + if (strpos($query, '&prev') !== false) { + $query = urldecode(trim(self::getParameterFromQueryString($query, 'prev'))); + $query = str_replace('&', '&', strstr($query, '?')); + } + $searchEngineName = 'Google Images'; + } else if ($searchEngineName === 'Google' + && (strpos($query, '&as_') !== false || strpos($query, 'as_') === 0) + ) { + $keys = array(); + $key = self::getParameterFromQueryString($query, 'as_q'); + if (!empty($key)) { + array_push($keys, $key); + } + $key = self::getParameterFromQueryString($query, 'as_oq'); + if (!empty($key)) { + array_push($keys, str_replace('+', ' OR ', $key)); + } + $key = self::getParameterFromQueryString($query, 'as_epq'); + if (!empty($key)) { + array_push($keys, "\"$key\""); + } + $key = self::getParameterFromQueryString($query, 'as_eq'); + if (!empty($key)) { + array_push($keys, "-$key"); + } + $key = trim(urldecode(implode(' ', $keys))); + } + + if ($searchEngineName === 'Google') { + // top bar menu + $tbm = self::getParameterFromQueryString($query, 'tbm'); + switch ($tbm) { + case 'isch': + $searchEngineName = 'Google Images'; + break; + case 'vid': + $searchEngineName = 'Google Video'; + break; + case 'shop': + $searchEngineName = 'Google Shopping'; + break; + } + } + + if (empty($key)) { + foreach ($variableNames as $variableName) { + if ($variableName[0] == '/') { + // regular expression match + if (preg_match($variableName, $referrerUrl, $matches)) { + $key = trim(urldecode($matches[1])); + break; + } + } else { + // search for keywords now &vname=keyword + $key = self::getParameterFromQueryString($query, $variableName); + $key = trim(urldecode($key)); + + // Special case: Google & empty q parameter + if (empty($key) + && $variableName == 'q' + + && ( + // Google search with no keyword + ($searchEngineName == 'Google' + && ( // First, they started putting an empty q= parameter + strpos($query, '&q=') !== false + || strpos($query, '?q=') !== false + // then they started sending the full host only (no path/query string) + || (empty($query) && (empty($refererPath) || $refererPath == '/') && empty($refererParsed['fragment'])) + ) + ) + // search engines with no keyword + || $searchEngineName == 'Google Images' + || $searchEngineName == 'DuckDuckGo') + ) { + $key = false; + } + if (!empty($key) + || $key === false + ) { + break; + } + } + } + } + + // $key === false is the special case "No keyword provided" which is a Search engine match + if ($key === null + || $key === '' + ) { + return false; + } + + if (!empty($key)) { + if (function_exists('iconv') + && isset($searchEngines[$refererHost][3]) + ) { + // accepts string, array, or comma-separated list string in preferred order + $charsets = $searchEngines[$refererHost][3]; + if (!is_array($charsets)) { + $charsets = explode(',', $charsets); + } + + if (!empty($charsets)) { + $charset = $charsets[0]; + if (count($charsets) > 1 + && function_exists('mb_detect_encoding') + ) { + $charset = mb_detect_encoding($key, $charsets); + if ($charset === false) { + $charset = $charsets[0]; + } + } + + $newkey = @iconv($charset, 'UTF-8//IGNORE', $key); + if (!empty($newkey)) { + $key = $newkey; + } + } + } + + $key = self::mb_strtolower($key); + } + + return array( + 'name' => $searchEngineName, + 'keywords' => $key, + ); + } + + /* + * System environment + */ + + /** + * Returns true if PHP was invoked from command-line interface (shell) + * + * @since added in 0.4.4 + * @return bool true if PHP invoked as a CGI or from CLI + */ + static public function isPhpCliMode() + { + $remoteAddr = @$_SERVER['REMOTE_ADDR']; + return PHP_SAPI == 'cli' || + (!strncmp(PHP_SAPI, 'cgi', 3) && empty($remoteAddr)); + } + + /** + * Is the current script execution triggered by misc/cron/archive.php ? + * + * Helpful for error handling: directly throw error without HTML (eg. when DB is down) + * @return bool + */ + static public function isArchivePhpTriggered() + { + return !empty($_GET['trigger']) + && $_GET['trigger'] == 'archivephp'; + } + + /** + * Assign CLI parameters as if they were REQUEST or GET parameters. + * You can trigger Piwik from the command line by + * # /usr/bin/php5 /path/to/piwik/index.php -- "module=API&method=Actions.getActions&idSite=1&period=day&date=previous8&format=php" + */ + static public function assignCliParametersToRequest() + { + if (isset($_SERVER['argc']) + && $_SERVER['argc'] > 0 + ) { + for ($i = 1; $i < $_SERVER['argc']; $i++) { + parse_str($_SERVER['argv'][$i], $tmp); + $_GET = array_merge($_GET, $tmp); + } + } + } + + /** + * Returns true if running on a Windows operating system + * + * @since added in 0.6.5 + * @return bool true if PHP detects it is running on Windows; else false + */ + static public function isWindows() + { + return DIRECTORY_SEPARATOR === '\\'; + } + + /** + * Returns true if running on MacOS + * + * @return bool true if PHP detects it is running on MacOS; else false + */ + static public function isMacOS() + { + return PHP_OS === 'Darwin'; + } + + /** + * Returns true if running on an Apache web server + * + * @return bool + */ + static public function isApache() + { + $apache = isset($_SERVER['SERVER_SOFTWARE']) && + !strncmp($_SERVER['SERVER_SOFTWARE'], 'Apache', 6); + + return $apache; + } + + /** + * Returns true if running on Microsoft IIS 7 (or above) + * + * @return bool + */ + static public function isIIS() + { + $iis = isset($_SERVER['SERVER_SOFTWARE']) && + preg_match('/^Microsoft-IIS\/(.+)/', $_SERVER['SERVER_SOFTWARE'], $matches) && + version_compare($matches[1], '7') >= 0; + + return $iis; + } + + /** + * Takes a list of fields defining numeric values and returns the corresponding + * unnamed parameters to be bound to the field names in the where clause of a SQL query + * + * @param array|string $fields array( fieldName1, fieldName2, fieldName3) Names of the mysql table fields to load + * @return string "?, ?, ?" + */ + public static function getSqlStringFieldsArray($fields) + { + if (is_string($fields)) { + $fields = array($fields); + } + $count = count($fields); + if ($count == 0) { + return "''"; + } + return '?' . str_repeat(',?', $count - 1); + } + + /** + * Sets outgoing header. + * + * @param string $header The header. + * @param bool $replace Whether to replace existing or not. + */ + public static function sendHeader($header, $replace = true) + { + if (isset($GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING']) && $GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING']) { + @header($header, $replace); + } else { + header($header, $replace); + } + } + + /** + * Returns the ID of the current LocationProvider (see UserCountry plugin code) from + * the Tracker cache. + */ + public static function getCurrentLocationProviderId() + { + $cache = Piwik_Tracker_Cache::getCacheGeneral(); + return empty($cache['currentLocationProviderId']) + ? Piwik_UserCountry_LocationProvider_Default::ID + : $cache['currentLocationProviderId']; + } } /** @@ -1692,27 +1544,22 @@ class Piwik_Common */ function destroy(&$var) { - if (is_object($var)) $var->__destruct(); - unset($var); - $var = null; + if (is_object($var)) $var->__destruct(); + unset($var); + $var = null; } -if(!function_exists('printDebug')) -{ - function printDebug( $info = '' ) - { - if(isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) - { - if(is_array($info)) - { - print("<pre>"); - print(htmlspecialchars(var_export($info,true), ENT_QUOTES)); - print("</pre>"); - } - else - { - print(htmlspecialchars($info, ENT_QUOTES) . "<br />\n"); - } - } - } +if (!function_exists('printDebug')) { + function printDebug($info = '') + { + if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) { + if (is_array($info)) { + print("<pre>"); + print(htmlspecialchars(var_export($info, true), ENT_QUOTES)); + print("</pre>"); + } else { + print(htmlspecialchars($info, ENT_QUOTES) . "<br />\n"); + } + } + } } diff --git a/core/Config.php b/core/Config.php index 78d236d4ac..e906311306 100644 --- a/core/Config.php +++ b/core/Config.php @@ -30,7 +30,7 @@ * Piwik_Config::getInstance()->branding = $brandingConfig; * * Example setting an option within a section in the configuration: - * + * * $brandingConfig = Piwik_Config::getInstance()->branding; * $brandingConfig['use_custom_logo'] = 1; * Piwik_Config::getInstance()->branding = $brandingConfig; @@ -40,473 +40,431 @@ */ class Piwik_Config { - static private $instance = null; - - /** - * Returns the singleton Piwik_Config - * - * @return Piwik_Config - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Contains configuration files values - * - * @var array - */ - protected $initialized = false; - protected $configGlobal = array(); - protected $configLocal = array(); - protected $configCache = array(); - protected $pathGlobal = null; - protected $pathLocal = null; - - protected function __construct() - { - $this->pathGlobal = self::getGlobalConfigPath(); - $this->pathLocal = self::getLocalConfigPath(); - } - - /** - * @var boolean - */ - protected $isTest = false; - - /** - * Enable test environment - * - * @param string $pathLocal - * @param string $pathGlobal - */ - public function setTestEnvironment($pathLocal = null, $pathGlobal = null) - { - $this->isTest = true; - - $this->clear(); - - if ($pathLocal) - { - $this->pathLocal = $pathLocal; - } - - if ($pathGlobal) - { - $this->pathGlobal = $pathGlobal; - } - - $this->init(); - if(isset($this->configGlobal['database_tests']) - || isset($this->configLocal['database_tests'])) - { - $this->__get('database_tests'); - $this->configCache['database'] = $this->configCache['database_tests']; - } - - // Ensure local mods do not affect tests - if(is_null($pathGlobal)) - { - $this->configCache['Debug'] = $this->configGlobal['Debug']; - $this->configCache['branding'] = $this->configGlobal['branding']; - $this->configCache['mail'] = $this->configGlobal['mail']; - $this->configCache['General'] = $this->configGlobal['General']; - $this->configCache['Segments'] = $this->configGlobal['Segments']; - $this->configCache['Tracker'] = $this->configGlobal['Tracker']; - $this->configCache['Deletelogs'] = $this->configGlobal['Deletelogs']; - } - - // 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->configCache['PluginsInstalled'] = array('PluginsInstalled' => array()); - } - - /** - * Returns absolute path to the global configuration file - * - * @return string - */ - static public function getGlobalConfigPath() - { - return PIWIK_USER_PATH .'/config/global.ini.php'; - } - - /** - * Backward compatibility stub - * - * @todo remove in 2.0 - * @since 1.7 - * @deprecated 1.7 - * @return string - */ - static public function getDefaultDefaultConfigPath() - { - return self::getGlobalConfigPath(); - } - - /** - * Returns absolute path to the local configuration file - * - * @return string - */ - static public function getLocalConfigPath() - { - return PIWIK_USER_PATH .'/config/config.ini.php'; - } - - /** - * Is local configuration file writable? - * - * @return bool - */ - public function isFileWritable() - { - return is_writable($this->pathLocal); - } - - /** - * Clear in-memory configuration so it can be reloaded - */ - public function clear() - { - $this->configGlobal = array(); - $this->configLocal = array(); - $this->configCache = array(); - $this->initialized = false; - - $this->pathGlobal = self::getGlobalConfigPath(); - $this->pathLocal = self::getLocalConfigPath(); - } - - /** - * Read configuration from files into memory - * - * @throws Exception if local config file is not readable; exits for other errors - */ - public function init() - { - $this->initialized = true; - $reportError = empty($GLOBALS['PIWIK_TRACKER_MODE']); - - // read defaults from global.ini.php - if(!is_readable($this->pathGlobal) && $reportError) - { - Piwik_ExitWithMessage(Piwik_TranslateException('General_ExceptionConfigurationFileNotFound', array($this->pathGlobal))); - } - - $this->configGlobal = _parse_ini_file($this->pathGlobal, true); - if(empty($this->configGlobal) && $reportError) - { - Piwik_ExitWithMessage(Piwik_TranslateException('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()"))); - } - - // read the local settings from config.ini.php - if(!is_readable($this->pathLocal) && $reportError) - { - throw new Exception(Piwik_TranslateException('General_ExceptionConfigurationFileNotFound', array($this->pathLocal))); - } - - $this->configLocal = _parse_ini_file($this->pathLocal, true); - if(empty($this->configLocal) && $reportError) - { - Piwik_ExitWithMessage(Piwik_TranslateException('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()"))); - } - } - - /** - * Decode HTML entities - * - * @param mixed $values - * @return mixed - */ - protected function decodeValues($values) - { - if(is_array($values)) - { - foreach($values as &$value) - { - $value = $this->decodeValues($value); - } - } - else - { - $values = html_entity_decode($values, ENT_COMPAT, 'UTF-8'); - } - return $values; - } - - /** - * Encode HTML entities - * - * @param mixed $values - * @return mixed - */ - protected function encodeValues($values) - { - if(is_array($values)) - { - foreach($values as &$value) - { - $value = $this->encodeValues($value); - } - } - else - { - $values = htmlentities($values, ENT_COMPAT, 'UTF-8'); - } - return $values; - } - - /** - * Magic get methods catching calls to $config->var_name - * Returns the value if found in the configuration - * - * @param string $name - * @return string|array The value requested, returned by reference - * @throws Exception if the value requested not found in both files - */ - public function &__get( $name ) - { - if(!$this->initialized) - { - $this->init(); - } - - // check cache for merged section - if (isset($this->configCache[$name])) - { - $tmp =& $this->configCache[$name]; - return $tmp; - } - - $section = null; - - // merge corresponding sections from global and local settings - if(isset($this->configGlobal[$name])) - { - $section = $this->configGlobal[$name]; - } - - if(isset($this->configLocal[$name])) - { - // local settings override the global defaults - $section = $section - ? array_merge($section, $this->configLocal[$name]) - : $this->configLocal[$name]; - } - - if ($section === null) - { - throw new Exception("Error while trying to read a specific config file entry <b>'$name'</b> from your configuration files.</b>If you just completed a Piwik upgrade, please check that the file config/global.ini.php was overwritten by the latest Piwik version."); - } - - // cache merged section for later - $this->configCache[$name] = $this->decodeValues($section); - $tmp =& $this->configCache[$name]; - - return $tmp; - } - - /** - * Set value - * - * @param string $name This corresponds to the section name - * @param mixed $value - */ - public function __set($name, $value) - { - $this->configCache[$name] = $value; - } - - /** - * Comparison function - * - * @param mixed $elem1 - * @param mixed $elem2 - * @return int; - */ - static function compareElements($elem1, $elem2) - { - if (is_array($elem1)) - { - if (is_array($elem2)) - { - return strcmp(serialize($elem1), serialize($elem2)); - } - - return 1; - } - - if (is_array($elem2)) - { - return -1; - } - - if ((string)$elem1 === (string)$elem2) - { - return 0; - } - - return ((string)$elem1 > (string)$elem2) ? 1 : -1; - } - - /** - * Compare arrays and return difference, such that: - * - * $modified = array_merge($original, $difference); - * - * @param array $original original array - * @param array $modified modified array - * @return array differences between original and modified - */ - public function array_unmerge($original, $modified) - { - // return key/value pairs for keys in $modified but not in $original - // return key/value pairs for keys in both $modified and $original, but values differ - // ignore keys that are in $original but not in $modified - - return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements')); - } - - /** - * Dump config - * - * @param array $configLocal - * @param array $configGlobal - * @param array $configCache - * @return string - */ - public function dumpConfig($configLocal, $configGlobal, $configCache) - { - $dirty = false; - - $output = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n"; - $output .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n"; - - if ($configCache) - { - foreach($configLocal as $name => $section) - { - if (!isset($configCache[$name])) - { - $configCache[$name] = $this->decodeValues($section); - } - } - - $sectionNames = array_unique(array_merge(array_keys($configGlobal), array_keys($configCache))); - - foreach($sectionNames as $section) - { - if(!isset($configCache[$section])) - { - continue; - } - - // Only merge if the section exists in global.ini.php (in case a section only lives in config.ini.php) - - // get local and cached config - $local = isset($configLocal[$section]) ? $configLocal[$section] : array(); - $config = $configCache[$section]; - - // remove default values from both (they should not get written to local) - if (isset($configGlobal[$section])) - { - $config = $this->array_unmerge($configGlobal[$section], $configCache[$section]); - $local = $this->array_unmerge($configGlobal[$section], $local); - } - - // if either local/config have non-default values and the other doesn't, - // OR both have values, but different values, we must write to config.ini.php - if (empty($local) xor empty($config) - || (!empty($local) - && !empty($config) - && self::compareElements($config, $configLocal[$section]))) - { - $dirty = true; - } - - // no point in writing empty sections, so skip if the cached section is empty - if (empty($config)) - { - continue; - } - - $output .= "[$section]\n"; - - foreach($config as $name => $value) - { - $value = $this->encodeValues($value); - - if(is_numeric($name)) - { - $name = $section; - $value = array($value); - } - - if(is_array($value)) - { - foreach($value as $currentValue) - { - $output .= $name."[] = \"$currentValue\"\n"; - } - } - else - { - if(!is_numeric($value)) - { - $value = "\"$value\""; - } - $output .= $name.' = '.$value."\n"; - } - } - - $output .= "\n"; - } - - if ($dirty) - { - return $output; - } - } - - return false; - } - - - /** - * Write user configuration file - * - * @param array $configLocal - * @param array $configGlobal - * @param array $configCache - * @param string $pathLocal - */ - public function writeConfig($configLocal, $configGlobal, $configCache, $pathLocal) - { - if ($this->isTest) - { - return; - } - - $output = $this->dumpConfig($configLocal, $configGlobal, $configCache); - if ($output !== false) - { - @file_put_contents($pathLocal, $output); - } - - $this->clear(); - } - - /** - * Force save - */ - public function forceSave() - { - $this->writeConfig($this->configLocal, $this->configGlobal, $this->configCache, $this->pathLocal); - } + static private $instance = null; + + /** + * Returns the singleton Piwik_Config + * + * @return Piwik_Config + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Contains configuration files values + * + * @var array + */ + protected $initialized = false; + protected $configGlobal = array(); + protected $configLocal = array(); + protected $configCache = array(); + protected $pathGlobal = null; + protected $pathLocal = null; + + protected function __construct() + { + $this->pathGlobal = self::getGlobalConfigPath(); + $this->pathLocal = self::getLocalConfigPath(); + } + + /** + * @var boolean + */ + protected $isTest = false; + + /** + * Enable test environment + * + * @param string $pathLocal + * @param string $pathGlobal + */ + public function setTestEnvironment($pathLocal = null, $pathGlobal = null) + { + $this->isTest = true; + + $this->clear(); + + if ($pathLocal) { + $this->pathLocal = $pathLocal; + } + + if ($pathGlobal) { + $this->pathGlobal = $pathGlobal; + } + + $this->init(); + if (isset($this->configGlobal['database_tests']) + || isset($this->configLocal['database_tests']) + ) { + $this->__get('database_tests'); + $this->configCache['database'] = $this->configCache['database_tests']; + } + + // Ensure local mods do not affect tests + if (is_null($pathGlobal)) { + $this->configCache['Debug'] = $this->configGlobal['Debug']; + $this->configCache['branding'] = $this->configGlobal['branding']; + $this->configCache['mail'] = $this->configGlobal['mail']; + $this->configCache['General'] = $this->configGlobal['General']; + $this->configCache['Segments'] = $this->configGlobal['Segments']; + $this->configCache['Tracker'] = $this->configGlobal['Tracker']; + $this->configCache['Deletelogs'] = $this->configGlobal['Deletelogs']; + } + + // 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->configCache['PluginsInstalled'] = array('PluginsInstalled' => array()); + } + + /** + * Returns absolute path to the global configuration file + * + * @return string + */ + static public function getGlobalConfigPath() + { + return PIWIK_USER_PATH . '/config/global.ini.php'; + } + + /** + * Backward compatibility stub + * + * @todo remove in 2.0 + * @since 1.7 + * @deprecated 1.7 + * @return string + */ + static public function getDefaultDefaultConfigPath() + { + return self::getGlobalConfigPath(); + } + + /** + * Returns absolute path to the local configuration file + * + * @return string + */ + static public function getLocalConfigPath() + { + return PIWIK_USER_PATH . '/config/config.ini.php'; + } + + /** + * Is local configuration file writable? + * + * @return bool + */ + public function isFileWritable() + { + return is_writable($this->pathLocal); + } + + /** + * Clear in-memory configuration so it can be reloaded + */ + public function clear() + { + $this->configGlobal = array(); + $this->configLocal = array(); + $this->configCache = array(); + $this->initialized = false; + + $this->pathGlobal = self::getGlobalConfigPath(); + $this->pathLocal = self::getLocalConfigPath(); + } + + /** + * Read configuration from files into memory + * + * @throws Exception if local config file is not readable; exits for other errors + */ + public function init() + { + $this->initialized = true; + $reportError = empty($GLOBALS['PIWIK_TRACKER_MODE']); + + // read defaults from global.ini.php + if (!is_readable($this->pathGlobal) && $reportError) { + Piwik_ExitWithMessage(Piwik_TranslateException('General_ExceptionConfigurationFileNotFound', array($this->pathGlobal))); + } + + $this->configGlobal = _parse_ini_file($this->pathGlobal, true); + if (empty($this->configGlobal) && $reportError) { + Piwik_ExitWithMessage(Piwik_TranslateException('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()"))); + } + + // read the local settings from config.ini.php + if (!is_readable($this->pathLocal) && $reportError) { + throw new Exception(Piwik_TranslateException('General_ExceptionConfigurationFileNotFound', array($this->pathLocal))); + } + + $this->configLocal = _parse_ini_file($this->pathLocal, true); + if (empty($this->configLocal) && $reportError) { + Piwik_ExitWithMessage(Piwik_TranslateException('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()"))); + } + } + + /** + * Decode HTML entities + * + * @param mixed $values + * @return mixed + */ + protected function decodeValues($values) + { + if (is_array($values)) { + foreach ($values as &$value) { + $value = $this->decodeValues($value); + } + } else { + $values = html_entity_decode($values, ENT_COMPAT, 'UTF-8'); + } + return $values; + } + + /** + * Encode HTML entities + * + * @param mixed $values + * @return mixed + */ + protected function encodeValues($values) + { + if (is_array($values)) { + foreach ($values as &$value) { + $value = $this->encodeValues($value); + } + } else { + $values = htmlentities($values, ENT_COMPAT, 'UTF-8'); + } + return $values; + } + + /** + * Magic get methods catching calls to $config->var_name + * Returns the value if found in the configuration + * + * @param string $name + * @return string|array The value requested, returned by reference + * @throws Exception if the value requested not found in both files + */ + public function &__get($name) + { + if (!$this->initialized) { + $this->init(); + } + + // check cache for merged section + if (isset($this->configCache[$name])) { + $tmp =& $this->configCache[$name]; + return $tmp; + } + + $section = null; + + // merge corresponding sections from global and local settings + if (isset($this->configGlobal[$name])) { + $section = $this->configGlobal[$name]; + } + + if (isset($this->configLocal[$name])) { + // local settings override the global defaults + $section = $section + ? array_merge($section, $this->configLocal[$name]) + : $this->configLocal[$name]; + } + + if ($section === null) { + throw new Exception("Error while trying to read a specific config file entry <b>'$name'</b> from your configuration files.</b>If you just completed a Piwik upgrade, please check that the file config/global.ini.php was overwritten by the latest Piwik version."); + } + + // cache merged section for later + $this->configCache[$name] = $this->decodeValues($section); + $tmp =& $this->configCache[$name]; + + return $tmp; + } + + /** + * Set value + * + * @param string $name This corresponds to the section name + * @param mixed $value + */ + public function __set($name, $value) + { + $this->configCache[$name] = $value; + } + + /** + * Comparison function + * + * @param mixed $elem1 + * @param mixed $elem2 + * @return int; + */ + static function compareElements($elem1, $elem2) + { + if (is_array($elem1)) { + if (is_array($elem2)) { + return strcmp(serialize($elem1), serialize($elem2)); + } + + return 1; + } + + if (is_array($elem2)) { + return -1; + } + + if ((string)$elem1 === (string)$elem2) { + return 0; + } + + return ((string)$elem1 > (string)$elem2) ? 1 : -1; + } + + /** + * Compare arrays and return difference, such that: + * + * $modified = array_merge($original, $difference); + * + * @param array $original original array + * @param array $modified modified array + * @return array differences between original and modified + */ + public function array_unmerge($original, $modified) + { + // return key/value pairs for keys in $modified but not in $original + // return key/value pairs for keys in both $modified and $original, but values differ + // ignore keys that are in $original but not in $modified + + return array_udiff_assoc($modified, $original, array(__CLASS__, 'compareElements')); + } + + /** + * Dump config + * + * @param array $configLocal + * @param array $configGlobal + * @param array $configCache + * @return string + */ + public function dumpConfig($configLocal, $configGlobal, $configCache) + { + $dirty = false; + + $output = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n"; + $output .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n"; + + if ($configCache) { + foreach ($configLocal as $name => $section) { + if (!isset($configCache[$name])) { + $configCache[$name] = $this->decodeValues($section); + } + } + + $sectionNames = array_unique(array_merge(array_keys($configGlobal), array_keys($configCache))); + + foreach ($sectionNames as $section) { + if (!isset($configCache[$section])) { + continue; + } + + // Only merge if the section exists in global.ini.php (in case a section only lives in config.ini.php) + + // get local and cached config + $local = isset($configLocal[$section]) ? $configLocal[$section] : array(); + $config = $configCache[$section]; + + // remove default values from both (they should not get written to local) + if (isset($configGlobal[$section])) { + $config = $this->array_unmerge($configGlobal[$section], $configCache[$section]); + $local = $this->array_unmerge($configGlobal[$section], $local); + } + + // if either local/config have non-default values and the other doesn't, + // OR both have values, but different values, we must write to config.ini.php + if (empty($local) xor empty($config) + || (!empty($local) + && !empty($config) + && self::compareElements($config, $configLocal[$section])) + ) { + $dirty = true; + } + + // no point in writing empty sections, so skip if the cached section is empty + if (empty($config)) { + continue; + } + + $output .= "[$section]\n"; + + foreach ($config as $name => $value) { + $value = $this->encodeValues($value); + + if (is_numeric($name)) { + $name = $section; + $value = array($value); + } + + if (is_array($value)) { + foreach ($value as $currentValue) { + $output .= $name . "[] = \"$currentValue\"\n"; + } + } else { + if (!is_numeric($value)) { + $value = "\"$value\""; + } + $output .= $name . ' = ' . $value . "\n"; + } + } + + $output .= "\n"; + } + + if ($dirty) { + return $output; + } + } + + return false; + } + + + /** + * Write user configuration file + * + * @param array $configLocal + * @param array $configGlobal + * @param array $configCache + * @param string $pathLocal + */ + public function writeConfig($configLocal, $configGlobal, $configCache, $pathLocal) + { + if ($this->isTest) { + return; + } + + $output = $this->dumpConfig($configLocal, $configGlobal, $configCache); + if ($output !== false) { + @file_put_contents($pathLocal, $output); + } + + $this->clear(); + } + + /** + * Force save + */ + public function forceSave() + { + $this->writeConfig($this->configLocal, $this->configGlobal, $this->configCache, $this->pathLocal); + } } diff --git a/core/Config/Compat.php b/core/Config/Compat.php index 2169e3e284..549b2cb9d7 100644 --- a/core/Config/Compat.php +++ b/core/Config/Compat.php @@ -22,149 +22,145 @@ */ class Piwik_Config_Compat_Array { - private $data; - /** - * @var Piwik_Config_Compat - */ - private $parent; - - /** - * Constructor - * - * @param Piwik_Config_Compat $parent - * @param array $data configuration section - */ - public function __construct($parent, array $data) - { - $this->parent = $parent; - $this->data = $data; - } - - /** - * Get value by name - * - * @param string $name - * @return mixed - */ - public function __get($name) - { - $tmp = isset($this->data[$name]) ? $this->data[$name] : false; - return is_array($tmp) ? new Piwik_Config_Compat_Array($this, $tmp) : $tmp; - } - - /** - * Set name, value pair - * - * @param string $name - * @param mixed $value - */ - public function __set($name, $value) - { - if (is_object($value) && get_class($value) == 'Piwik_Config_Compat_Array') - { - $value = $value->toArray(); - } - - $this->data[$name] = $value; - $this->setDirtyBit(); - } - - /** - * Convert object to array - * - * @return array - */ - public function toArray() - { - return $this->data; - } - - /** - * Set dirty bit - */ - public function setDirtyBit() - { - $this->parent->setDirtyBit(); - } + private $data; + /** + * @var Piwik_Config_Compat + */ + private $parent; + + /** + * Constructor + * + * @param Piwik_Config_Compat $parent + * @param array $data configuration section + */ + public function __construct($parent, array $data) + { + $this->parent = $parent; + $this->data = $data; + } + + /** + * Get value by name + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + $tmp = isset($this->data[$name]) ? $this->data[$name] : false; + return is_array($tmp) ? new Piwik_Config_Compat_Array($this, $tmp) : $tmp; + } + + /** + * Set name, value pair + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + if (is_object($value) && get_class($value) == 'Piwik_Config_Compat_Array') { + $value = $value->toArray(); + } + + $this->data[$name] = $value; + $this->setDirtyBit(); + } + + /** + * Convert object to array + * + * @return array + */ + public function toArray() + { + return $this->data; + } + + /** + * Set dirty bit + */ + public function setDirtyBit() + { + $this->parent->setDirtyBit(); + } } class Piwik_Config_Compat { - private $config; - private $data; - private $enabled; - private $dirty; - - /** - * Constructor - */ - public function __construct() - { - $this->config = Piwik_Config::getInstance(); - $this->data = array(); - $this->enabled = true; - $this->dirty = false; - } - - /** - * Destructor - */ - public function __destruct() - { - if ($this->enabled && $this->dirty) - { - $this->config->forceSave(); - } - $this->config->clear(); - } - - /** - * Get value by name - * - * @param string $name - * @return mixed - */ - public function __get($name) - { - if (!isset($this->data[$name])) - { - $this->data[$name] = $this->config->__get($name); - } - - $tmp = $this->data[$name]; - return is_array($tmp) ? new Piwik_Config_Compat_Array($this, $tmp) : $tmp; - } - - /** - * Set name, value pair - * - * @param string $name - * @param mixed $value - */ - public function __set($name, $value) - { - if (is_object($value) && get_class($value) == 'Piwik_Config_Compat_Array') - { - $value = $value->toArray(); - } - - $this->config->__set($name, $value); - $this->dirty = true; - } - - /** - * Set dirty bit - */ - public function setDirtyBit() - { - $this->dirty = true; - } - - /** - * Disable saving of configuration changes - */ - public function disableSavingConfigurationFileUpdates() - { - $this->enabled = false; - } + private $config; + private $data; + private $enabled; + private $dirty; + + /** + * Constructor + */ + public function __construct() + { + $this->config = Piwik_Config::getInstance(); + $this->data = array(); + $this->enabled = true; + $this->dirty = false; + } + + /** + * Destructor + */ + public function __destruct() + { + if ($this->enabled && $this->dirty) { + $this->config->forceSave(); + } + $this->config->clear(); + } + + /** + * Get value by name + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if (!isset($this->data[$name])) { + $this->data[$name] = $this->config->__get($name); + } + + $tmp = $this->data[$name]; + return is_array($tmp) ? new Piwik_Config_Compat_Array($this, $tmp) : $tmp; + } + + /** + * Set name, value pair + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + if (is_object($value) && get_class($value) == 'Piwik_Config_Compat_Array') { + $value = $value->toArray(); + } + + $this->config->__set($name, $value); + $this->dirty = true; + } + + /** + * Set dirty bit + */ + public function setDirtyBit() + { + $this->dirty = true; + } + + /** + * Disable saving of configuration changes + */ + public function disableSavingConfigurationFileUpdates() + { + $this->enabled = false; + } } diff --git a/core/Controller.php b/core/Controller.php index 0c625bde6e..9d97b6ff90 100644 --- a/core/Controller.php +++ b/core/Controller.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,914 +12,850 @@ /** * Parent class of all plugins Controllers (located in /plugins/PluginName/Controller.php * It defines some helper functions controllers can use. - * + * * @package Piwik */ abstract class Piwik_Controller { - /** - * Plugin name, eg. Referers - * @var string - */ - protected $pluginName; - - /** - * Date string - * - * @var string - */ - protected $strDate; - - /** - * Piwik_Date object or null if the requested date is a range - * - * @var Piwik_Date|null - */ - protected $date; - - /** - * @var int - */ - protected $idSite; - - /** - * @var Piwik_Site - */ - protected $site = null; - - /** - * Builds the controller object, reads the date from the request, extracts plugin name from - */ - function __construct() - { - $this->init(); - } - - protected function init() - { - $aPluginName = explode('_', get_class($this)); - $this->pluginName = $aPluginName[1]; - $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; - } - } - - /** - * 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 string $date today, yesterday, YYYY-MM-DD - * @param string $defaultTimezone default timezone to use - * @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 Piwik_Date $date - * @return void - */ - protected function setDate(Piwik_Date $date) - { - $this->date = $date; - $strDate = $this->date->toString(); - $this->strDate = $strDate; - } - - /** - * Returns the name of the default method that will be called - * when visiting: index.php?module=PluginName without the action parameter - * - * @return string - */ - function getDefaultAction() - { - return 'index'; - } - - /** - * Given an Object implementing Piwik_View_Interface, we either: - * - echo the output of the rendering if fetch = false - * - returns the output of the rendering if fetch = true - * - * @param Piwik_ViewDataTable $view view object to use - * @param bool $fetch indicates whether to output or return the content - * @return string|void - */ - protected function renderView( Piwik_ViewDataTable $view, $fetch = false) - { - Piwik_PostEvent( 'Controller.renderView', - $this, - array( 'view' => $view, - 'controllerName' => $view->getCurrentControllerName(), - 'controllerAction' => $view->getCurrentControllerAction(), - 'apiMethodToRequestDataTable' => $view->getApiMethodToRequestDataTable(), - 'controllerActionCalledWhenRequestSubTable' => $view->getControllerActionCalledWhenRequestSubTable(), - ) - ); - - $view->main(); - - $rendered = $view->getView()->render(); - if($fetch) - { - return $rendered; - } - echo $rendered; - } - - /** - * Returns a ViewDataTable object of an Evolution graph - * for the last30 days/weeks/etc. of the current period, relative to the current date. - * - * @param string $currentModuleName - * @param string $currentControllerAction - * @param string $apiMethod - * @return Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution - */ - protected function getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod) - { - $view = Piwik_ViewDataTable::factory('graphEvolution'); - $view->init( $currentModuleName, $currentControllerAction, $apiMethod ); - return $view; - } - - /** - * This method is similar to self::getLastUnitGraph. It works with API.get to combine metrics - * of different *.get reports. The returned ViewDataTable is configured with column - * translations and selectable metrics. - * - * @param string $currentModuleName - * @param string $currentControllerAction - * @param array $columnsToDisplay - * @param array $selectableColumns - * @param bool|string $reportDocumentation - * @param string $apiMethod The method to request the report from - * (by default, this is API.get but it can be changed for custom stuff) - * @return Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution - */ - protected function getLastUnitGraphAcrossPlugins($currentModuleName, $currentControllerAction, - $columnsToDisplay, $selectableColumns=array(), $reportDocumentation=false, $apiMethod='API.get') - { - // back up and manipulate the columns parameter - $backupColumns = false; - if (isset($_GET['columns'])) - { - $backupColumns = $_GET['columns']; - } - - $_GET['columns'] = implode(',', $columnsToDisplay); - - // load translations from meta data - $idSite = Piwik_Common::getRequestVar('idSite'); - $period = Piwik_Common::getRequestVar('period'); - $date = Piwik_Common::getRequestVar('date'); - $meta = Piwik_API_API::getInstance()->getReportMetadata($idSite, $period, $date); - - $columns = array_merge($columnsToDisplay, $selectableColumns); - $translations = array(); - foreach ($meta as $reportMeta) - { - if ($reportMeta['action'] == 'get' && !isset($reportMeta['parameters'])) - { - foreach ($columns as $column) - { - if (isset($reportMeta['metrics'][$column])) - { - $translations[$column] = $reportMeta['metrics'][$column]; - } - } - } - } - - // initialize the graph and load the data - $view = $this->getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod); - $view->setColumnsToDisplay($columnsToDisplay); - $view->setSelectableColumns($selectableColumns); - $view->setColumnsTranslations($translations); - - if ($reportDocumentation) - { - $view->setReportDocumentation($reportDocumentation); - } - - $view->main(); - - // restore the columns parameter - if ($backupColumns !== false) - { - $_GET['columns'] = $backupColumns; - } - else - { - unset($_GET['columns']); - } - - return $view; - } - - /** - * Returns the array of new processed parameters once the parameters are applied. - * For example: if you set range=last30 and date=2008-03-10, - * the date element of the returned array will be "2008-02-10,2008-03-10" - * - * Parameters you can set: - * - range: last30, previous10, etc. - * - date: YYYY-MM-DD, today, yesterday - * - period: day, week, month, year - * - * @param array $paramsToSet array( 'date' => 'last50', 'viewDataTable' =>'sparkline' ) - * @throws Piwik_Access_NoAccessException - * @return array - */ - protected function getGraphParamsModified($paramsToSet = array()) - { - if(!isset($paramsToSet['period'])) - { - $period = Piwik_Common::getRequestVar('period'); - } - else - { - $period = $paramsToSet['period']; - } - if($period == 'range') - { - return $paramsToSet; - } - if(!isset($paramsToSet['range'])) - { - $range = 'last30'; - } - else - { - $range = $paramsToSet['range']; - } - - if(!isset($paramsToSet['date'])) - { - $endDate = $this->strDate; - } - else - { - $endDate = $paramsToSet['date']; - } - - if(is_null($this->site)) - { - throw new Piwik_Access_NoAccessException("Website not initialized, check that you are logged in and/or using the correct token_auth."); - } - $paramDate = self::getDateRangeRelativeToEndDate($period, $range, $endDate, $this->site); - - $params = array_merge($paramsToSet , array( 'date' => $paramDate ) ); - return $params; - } - - /** - * Given for example, $period = month, $lastN = 'last6', $endDate = '2011-07-01', - * It will return the $date = '2011-01-01,2011-07-01' which is useful to draw graphs for the last N periods - * - * @param string $period - * @param string $lastN - * @param string $endDate - * @param Piwik_Site $site - * @return string - */ - static public function getDateRangeRelativeToEndDate($period, $lastN, $endDate, $site ) - { - $last30Relative = new Piwik_Period_Range($period, $lastN, $site->getTimezone() ); - $last30Relative->setDefaultEndDate(Piwik_Date::factory($endDate)); - $date = $last30Relative->getDateStart()->toString() . "," . $last30Relative->getDateEnd()->toString(); - return $date; - } - - /** - * Returns a numeric value from the API. - * Works only for API methods that originally returns numeric values (there is no cast here) - * - * @param string $methodToCall Name of method to call, eg. Referers.getNumberOfDistinctSearchEngines - * @param string|false $date A custom date to use when getting the value. If false, the 'date' query - * parameter is used. - * @return int|float - */ - protected function getNumericValue( $methodToCall, $date = false ) - { - $params = $date === false ? array() : array('date' => $date); - - $return = Piwik_API_Request::processRequest($methodToCall, $params); - $columns = $return->getFirstRow()->getColumns(); - return reset($columns); - } - - /** - * Returns the current URL to use in a img src=X to display a sparkline. - * $action must be the name of a Controller method that requests data using the Piwik_ViewDataTable::factory - * It will automatically build a sparkline by setting the viewDataTable=sparkline parameter in the URL. - * It will also computes automatically the 'date' for the 'last30' days/weeks/etc. - * - * @param string $action Method name of the controller to call in the img src - * @param array $customParameters Array of name => value of parameters to set in the generated GET url - * @return string The generated URL - */ - protected function getUrlSparkline( $action, $customParameters = array() ) - { - $params = $this->getGraphParamsModified( - array( 'viewDataTable' => 'sparkline', - 'action' => $action, - 'module' => $this->pluginName) - + $customParameters - ); - // convert array values to comma separated - foreach($params as &$value) - { - if(is_array($value)) - { - $value = rawurlencode(implode(',', $value)); - } - } - $url = Piwik_Url::getCurrentQueryStringWithParametersModified($params); - return $url; - } - - /** - * Sets the first date available in the calendar - * - * @param Piwik_Date $minDate - * @param Piwik_View $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 Piwik_Date $maxDate - * @param Piwik_View $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. - * If any error happens, displays the login screen - * - * @param Piwik_View $view - * @throws Exception - * @return void - */ - protected function setGeneralVariablesView($view) - { - $view->date = $this->strDate; - - try { - $view->idSite = $this->idSite; - if(empty($this->site) || empty($this->idSite)) - { - throw new Exception("The requested website idSite is not found in the request, or is invalid. + /** + * Plugin name, eg. Referers + * @var string + */ + protected $pluginName; + + /** + * Date string + * + * @var string + */ + protected $strDate; + + /** + * Piwik_Date object or null if the requested date is a range + * + * @var Piwik_Date|null + */ + protected $date; + + /** + * @var int + */ + protected $idSite; + + /** + * @var Piwik_Site + */ + protected $site = null; + + /** + * Builds the controller object, reads the date from the request, extracts plugin name from + */ + function __construct() + { + $this->init(); + } + + protected function init() + { + $aPluginName = explode('_', get_class($this)); + $this->pluginName = $aPluginName[1]; + $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; + } + } + + /** + * 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 string $date today, yesterday, YYYY-MM-DD + * @param string $defaultTimezone default timezone to use + * @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 Piwik_Date $date + * @return void + */ + protected function setDate(Piwik_Date $date) + { + $this->date = $date; + $strDate = $this->date->toString(); + $this->strDate = $strDate; + } + + /** + * Returns the name of the default method that will be called + * when visiting: index.php?module=PluginName without the action parameter + * + * @return string + */ + function getDefaultAction() + { + return 'index'; + } + + /** + * Given an Object implementing Piwik_View_Interface, we either: + * - echo the output of the rendering if fetch = false + * - returns the output of the rendering if fetch = true + * + * @param Piwik_ViewDataTable $view view object to use + * @param bool $fetch indicates whether to output or return the content + * @return string|void + */ + protected function renderView(Piwik_ViewDataTable $view, $fetch = false) + { + Piwik_PostEvent('Controller.renderView', + $this, + array('view' => $view, + 'controllerName' => $view->getCurrentControllerName(), + 'controllerAction' => $view->getCurrentControllerAction(), + 'apiMethodToRequestDataTable' => $view->getApiMethodToRequestDataTable(), + 'controllerActionCalledWhenRequestSubTable' => $view->getControllerActionCalledWhenRequestSubTable(), + ) + ); + + $view->main(); + + $rendered = $view->getView()->render(); + if ($fetch) { + return $rendered; + } + echo $rendered; + } + + /** + * Returns a ViewDataTable object of an Evolution graph + * for the last30 days/weeks/etc. of the current period, relative to the current date. + * + * @param string $currentModuleName + * @param string $currentControllerAction + * @param string $apiMethod + * @return Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution + */ + protected function getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod) + { + $view = Piwik_ViewDataTable::factory('graphEvolution'); + $view->init($currentModuleName, $currentControllerAction, $apiMethod); + return $view; + } + + /** + * This method is similar to self::getLastUnitGraph. It works with API.get to combine metrics + * of different *.get reports. The returned ViewDataTable is configured with column + * translations and selectable metrics. + * + * @param string $currentModuleName + * @param string $currentControllerAction + * @param array $columnsToDisplay + * @param array $selectableColumns + * @param bool|string $reportDocumentation + * @param string $apiMethod The method to request the report from + * (by default, this is API.get but it can be changed for custom stuff) + * @return Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution + */ + protected function getLastUnitGraphAcrossPlugins($currentModuleName, $currentControllerAction, + $columnsToDisplay, $selectableColumns = array(), $reportDocumentation = false, $apiMethod = 'API.get') + { + // back up and manipulate the columns parameter + $backupColumns = false; + if (isset($_GET['columns'])) { + $backupColumns = $_GET['columns']; + } + + $_GET['columns'] = implode(',', $columnsToDisplay); + + // load translations from meta data + $idSite = Piwik_Common::getRequestVar('idSite'); + $period = Piwik_Common::getRequestVar('period'); + $date = Piwik_Common::getRequestVar('date'); + $meta = Piwik_API_API::getInstance()->getReportMetadata($idSite, $period, $date); + + $columns = array_merge($columnsToDisplay, $selectableColumns); + $translations = array(); + foreach ($meta as $reportMeta) { + if ($reportMeta['action'] == 'get' && !isset($reportMeta['parameters'])) { + foreach ($columns as $column) { + if (isset($reportMeta['metrics'][$column])) { + $translations[$column] = $reportMeta['metrics'][$column]; + } + } + } + } + + // initialize the graph and load the data + $view = $this->getLastUnitGraph($currentModuleName, $currentControllerAction, $apiMethod); + $view->setColumnsToDisplay($columnsToDisplay); + $view->setSelectableColumns($selectableColumns); + $view->setColumnsTranslations($translations); + + if ($reportDocumentation) { + $view->setReportDocumentation($reportDocumentation); + } + + $view->main(); + + // restore the columns parameter + if ($backupColumns !== false) { + $_GET['columns'] = $backupColumns; + } else { + unset($_GET['columns']); + } + + return $view; + } + + /** + * Returns the array of new processed parameters once the parameters are applied. + * For example: if you set range=last30 and date=2008-03-10, + * the date element of the returned array will be "2008-02-10,2008-03-10" + * + * Parameters you can set: + * - range: last30, previous10, etc. + * - date: YYYY-MM-DD, today, yesterday + * - period: day, week, month, year + * + * @param array $paramsToSet array( 'date' => 'last50', 'viewDataTable' =>'sparkline' ) + * @throws Piwik_Access_NoAccessException + * @return array + */ + protected function getGraphParamsModified($paramsToSet = array()) + { + if (!isset($paramsToSet['period'])) { + $period = Piwik_Common::getRequestVar('period'); + } else { + $period = $paramsToSet['period']; + } + if ($period == 'range') { + return $paramsToSet; + } + if (!isset($paramsToSet['range'])) { + $range = 'last30'; + } else { + $range = $paramsToSet['range']; + } + + if (!isset($paramsToSet['date'])) { + $endDate = $this->strDate; + } else { + $endDate = $paramsToSet['date']; + } + + if (is_null($this->site)) { + throw new Piwik_Access_NoAccessException("Website not initialized, check that you are logged in and/or using the correct token_auth."); + } + $paramDate = self::getDateRangeRelativeToEndDate($period, $range, $endDate, $this->site); + + $params = array_merge($paramsToSet, array('date' => $paramDate)); + return $params; + } + + /** + * Given for example, $period = month, $lastN = 'last6', $endDate = '2011-07-01', + * It will return the $date = '2011-01-01,2011-07-01' which is useful to draw graphs for the last N periods + * + * @param string $period + * @param string $lastN + * @param string $endDate + * @param Piwik_Site $site + * @return string + */ + static public function getDateRangeRelativeToEndDate($period, $lastN, $endDate, $site) + { + $last30Relative = new Piwik_Period_Range($period, $lastN, $site->getTimezone()); + $last30Relative->setDefaultEndDate(Piwik_Date::factory($endDate)); + $date = $last30Relative->getDateStart()->toString() . "," . $last30Relative->getDateEnd()->toString(); + return $date; + } + + /** + * Returns a numeric value from the API. + * Works only for API methods that originally returns numeric values (there is no cast here) + * + * @param string $methodToCall Name of method to call, eg. Referers.getNumberOfDistinctSearchEngines + * @param string|false $date A custom date to use when getting the value. If false, the 'date' query + * parameter is used. + * @return int|float + */ + protected function getNumericValue($methodToCall, $date = false) + { + $params = $date === false ? array() : array('date' => $date); + + $return = Piwik_API_Request::processRequest($methodToCall, $params); + $columns = $return->getFirstRow()->getColumns(); + return reset($columns); + } + + /** + * Returns the current URL to use in a img src=X to display a sparkline. + * $action must be the name of a Controller method that requests data using the Piwik_ViewDataTable::factory + * It will automatically build a sparkline by setting the viewDataTable=sparkline parameter in the URL. + * It will also computes automatically the 'date' for the 'last30' days/weeks/etc. + * + * @param string $action Method name of the controller to call in the img src + * @param array $customParameters Array of name => value of parameters to set in the generated GET url + * @return string The generated URL + */ + protected function getUrlSparkline($action, $customParameters = array()) + { + $params = $this->getGraphParamsModified( + array('viewDataTable' => 'sparkline', + 'action' => $action, + 'module' => $this->pluginName) + + $customParameters + ); + // convert array values to comma separated + foreach ($params as &$value) { + if (is_array($value)) { + $value = rawurlencode(implode(',', $value)); + } + } + $url = Piwik_Url::getCurrentQueryStringWithParametersModified($params); + return $url; + } + + /** + * Sets the first date available in the calendar + * + * @param Piwik_Date $minDate + * @param Piwik_View $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 Piwik_Date $maxDate + * @param Piwik_View $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. + * If any error happens, displays the login screen + * + * @param Piwik_View $view + * @throws Exception + * @return void + */ + protected function setGeneralVariablesView($view) + { + $view->date = $this->strDate; + + try { + $view->idSite = $this->idSite; + if (empty($this->site) || empty($this->idSite)) { + throw new Exception("The requested website idSite is not found in the request, or is invalid. Please check that you are logged in Piwik and have permission to access the specified website."); - } - $this->setPeriodVariablesView($view); - - $rawDate = Piwik_Common::getRequestVar('date'); - $periodStr = Piwik_Common::getRequestVar('period'); - if($periodStr != 'range') - { - $date = Piwik_Date::factory($this->strDate); - $period = Piwik_Period::factory($periodStr, $date); - } - else - { - $period = new Piwik_Period_Range($periodStr, $rawDate, $this->site->getTimezone()); - } - $view->rawDate = $rawDate; - $view->prettyDate = self::getCalendarPrettyDate($period); - - $view->siteName = $this->site->getName(); - $view->siteMainUrl = $this->site->getMainUrl(); - - $datetimeMinDate = $this->site->getCreationDate()->getDatetime(); - $minDate = Piwik_Date::factory($datetimeMinDate, $this->site->getTimezone()); - $this->setMinDateView($minDate, $view); - - $maxDate = Piwik_Date::factory('now', $this->site->getTimezone()); - $this->setMaxDateView($maxDate, $view); - - // Setting current period start & end dates, for pre-setting the calendar when "Date Range" is selected - $dateStart = $period->getDateStart(); - if($dateStart->isEarlier($minDate)) { $dateStart = $minDate; } - $dateEnd = $period->getDateEnd(); - if($dateEnd->isLater($maxDate)) { $dateEnd = $maxDate; } - - $view->startDate = $dateStart; - $view->endDate = $dateEnd; - - $language = Piwik_LanguagesManager::getLanguageForSession(); - $view->language = !empty($language) ? $language : Piwik_LanguagesManager::getLanguageCodeForCurrentUser(); - - $view->config_action_url_category_delimiter = Piwik_Config::getInstance()->General['action_url_category_delimiter']; - - $this->setBasicVariablesView($view); - - $view->topMenu = Piwik_GetTopMenu(); - } catch(Exception $e) { - Piwik_ExitWithMessage($e->getMessage(), '' /* $e->getTraceAsString() */ ); - } - } - - /** - * Set the minimal variables in the view object - * - * @param Piwik_View $view - */ - protected function setBasicVariablesView($view) - { - $view->debugTrackVisitsInsidePiwikUI = Piwik_Config::getInstance()->Debug['track_visits_inside_piwik_ui']; - $view->isSuperUser = Zend_Registry::get('access')->isSuperUser(); - $view->hasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess(); - $view->isCustomLogo = Piwik_Config::getInstance()->branding['use_custom_logo']; - $view->logoHeader = Piwik_API_API::getInstance()->getHeaderLogoUrl(); - $view->logoLarge = Piwik_API_API::getInstance()->getLogoUrl(); - $view->logoSVG = Piwik_API_API::getInstance()->getSVGLogoUrl(); - $view->hasSVGLogo = Piwik_API_API::getInstance()->hasSVGLogo(); - - $view->enableFrames = Piwik_Config::getInstance()->General['enable_framed_pages'] - || @Piwik_Config::getInstance()->General['enable_framed_logins']; - if(!$view->enableFrames) - { - $view->setXFrameOptions('sameorigin'); - } - - self::setHostValidationVariablesView($view); - } - - /** - * Checks if the current host is valid and sets variables on the given view, including: - * - * isValidHost - true if host is valid, false if otherwise - * invalidHostMessage - message to display if host is invalid (only set if host is invalid) - * invalidHost - the invalid hostname (only set if host is invalid) - * mailLinkStart - the open tag of a link to email the super user of this problem (only set - * if host is invalid) - */ - public static function setHostValidationVariablesView( $view ) - { - // check if host is valid - $view->isValidHost = Piwik_Url::isValidHost(); - if (!$view->isValidHost) - { - // invalid host, so display warning to user - $validHost = Piwik_Config::getInstance()->General['trusted_hosts'][0]; - $invalidHost = Piwik_Common::sanitizeInputValue($_SERVER['HTTP_HOST']); - - $emailSubject = rawurlencode(Piwik_Translate('CoreHome_InjectedHostEmailSubject', $invalidHost)); - $emailBody = rawurlencode(Piwik_Translate('CoreHome_InjectedHostEmailBody')); - $superUserEmail = Piwik::getSuperUserEmail(); - - $mailToUrl = "mailto:$superUserEmail?subject=$emailSubject&body=$emailBody"; - $mailLinkStart = "<a href=\"$mailToUrl\">"; - - $invalidUrl = Piwik_Url::getCurrentUrlWithoutQueryString($checkIfTrusted = false); - $validUrl = Piwik_Url::getCurrentScheme() . '://' . $validHost - . Piwik_Url::getCurrentScriptName(); + } + $this->setPeriodVariablesView($view); + + $rawDate = Piwik_Common::getRequestVar('date'); + $periodStr = Piwik_Common::getRequestVar('period'); + if ($periodStr != 'range') { + $date = Piwik_Date::factory($this->strDate); + $period = Piwik_Period::factory($periodStr, $date); + } else { + $period = new Piwik_Period_Range($periodStr, $rawDate, $this->site->getTimezone()); + } + $view->rawDate = $rawDate; + $view->prettyDate = self::getCalendarPrettyDate($period); + + $view->siteName = $this->site->getName(); + $view->siteMainUrl = $this->site->getMainUrl(); + + $datetimeMinDate = $this->site->getCreationDate()->getDatetime(); + $minDate = Piwik_Date::factory($datetimeMinDate, $this->site->getTimezone()); + $this->setMinDateView($minDate, $view); + + $maxDate = Piwik_Date::factory('now', $this->site->getTimezone()); + $this->setMaxDateView($maxDate, $view); + + // Setting current period start & end dates, for pre-setting the calendar when "Date Range" is selected + $dateStart = $period->getDateStart(); + if ($dateStart->isEarlier($minDate)) { + $dateStart = $minDate; + } + $dateEnd = $period->getDateEnd(); + if ($dateEnd->isLater($maxDate)) { + $dateEnd = $maxDate; + } + + $view->startDate = $dateStart; + $view->endDate = $dateEnd; + + $language = Piwik_LanguagesManager::getLanguageForSession(); + $view->language = !empty($language) ? $language : Piwik_LanguagesManager::getLanguageCodeForCurrentUser(); + + $view->config_action_url_category_delimiter = Piwik_Config::getInstance()->General['action_url_category_delimiter']; + + $this->setBasicVariablesView($view); + + $view->topMenu = Piwik_GetTopMenu(); + } catch (Exception $e) { + Piwik_ExitWithMessage($e->getMessage(), '' /* $e->getTraceAsString() */); + } + } + + /** + * Set the minimal variables in the view object + * + * @param Piwik_View $view + */ + protected function setBasicVariablesView($view) + { + $view->debugTrackVisitsInsidePiwikUI = Piwik_Config::getInstance()->Debug['track_visits_inside_piwik_ui']; + $view->isSuperUser = Zend_Registry::get('access')->isSuperUser(); + $view->hasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess(); + $view->isCustomLogo = Piwik_Config::getInstance()->branding['use_custom_logo']; + $view->logoHeader = Piwik_API_API::getInstance()->getHeaderLogoUrl(); + $view->logoLarge = Piwik_API_API::getInstance()->getLogoUrl(); + $view->logoSVG = Piwik_API_API::getInstance()->getSVGLogoUrl(); + $view->hasSVGLogo = Piwik_API_API::getInstance()->hasSVGLogo(); + + $view->enableFrames = Piwik_Config::getInstance()->General['enable_framed_pages'] + || @Piwik_Config::getInstance()->General['enable_framed_logins']; + if (!$view->enableFrames) { + $view->setXFrameOptions('sameorigin'); + } + + self::setHostValidationVariablesView($view); + } + + /** + * Checks if the current host is valid and sets variables on the given view, including: + * + * isValidHost - true if host is valid, false if otherwise + * invalidHostMessage - message to display if host is invalid (only set if host is invalid) + * invalidHost - the invalid hostname (only set if host is invalid) + * mailLinkStart - the open tag of a link to email the super user of this problem (only set + * if host is invalid) + */ + public static function setHostValidationVariablesView($view) + { + // check if host is valid + $view->isValidHost = Piwik_Url::isValidHost(); + if (!$view->isValidHost) { + // invalid host, so display warning to user + $validHost = Piwik_Config::getInstance()->General['trusted_hosts'][0]; + $invalidHost = Piwik_Common::sanitizeInputValue($_SERVER['HTTP_HOST']); + + $emailSubject = rawurlencode(Piwik_Translate('CoreHome_InjectedHostEmailSubject', $invalidHost)); + $emailBody = rawurlencode(Piwik_Translate('CoreHome_InjectedHostEmailBody')); + $superUserEmail = Piwik::getSuperUserEmail(); + + $mailToUrl = "mailto:$superUserEmail?subject=$emailSubject&body=$emailBody"; + $mailLinkStart = "<a href=\"$mailToUrl\">"; + + $invalidUrl = Piwik_Url::getCurrentUrlWithoutQueryString($checkIfTrusted = false); + $validUrl = Piwik_Url::getCurrentScheme() . '://' . $validHost + . Piwik_Url::getCurrentScriptName(); $invalidUrl = Piwik_Common::sanitizeInputValue($invalidUrl); $validUrl = Piwik_Common::sanitizeInputValue($validUrl); - $changeTrustedHostsUrl = "index.php" - . Piwik_Url::getCurrentQueryStringWithParametersModified(array( - 'module' => 'CoreAdminHome', - 'action' => 'generalSettings' - )) - . "#trustedHostsSection"; - - $warningStart = Piwik_Translate('CoreHome_InjectedHostWarningIntro', array( - '<strong>'.$invalidUrl.'</strong>', - '<strong>'.$validUrl.'</strong>' - )) . ' <br/>'; - - if (Piwik::isUserIsSuperUser()) - { - $view->invalidHostMessage = $warningStart . ' ' - . Piwik_Translate('CoreHome_InjectedHostSuperUserWarning', array( - "<a href=\"$changeTrustedHostsUrl\">", - $invalidHost, - '</a>', - "<br/><a href=\"$validUrl\">", - $validHost, - '</a>' - )); - } - else - { - $view->invalidHostMessage = $warningStart . ' ' - . Piwik_Translate('CoreHome_InjectedHostNonSuperUserWarning', array( - "<br/><a href=\"$validUrl\">", - '</a>', - $mailLinkStart, - '</a>' - )); - } - $view->invalidHostMessageHowToFix = '<b>How do I fix this problem and how do I login again?</b><br/> The Piwik Super User can manually edit the file piwik/config/config.ini.php - and add the following lines: <pre>[General]'."\n".'trusted_hosts[] = "'.$validHost.'"</pre><br/>After making the change, you will be able to login again.<br/><br/> + $changeTrustedHostsUrl = "index.php" + . Piwik_Url::getCurrentQueryStringWithParametersModified(array( + 'module' => 'CoreAdminHome', + 'action' => 'generalSettings' + )) + . "#trustedHostsSection"; + + $warningStart = Piwik_Translate('CoreHome_InjectedHostWarningIntro', array( + '<strong>' . $invalidUrl . '</strong>', + '<strong>' . $validUrl . '</strong>' + )) . ' <br/>'; + + if (Piwik::isUserIsSuperUser()) { + $view->invalidHostMessage = $warningStart . ' ' + . Piwik_Translate('CoreHome_InjectedHostSuperUserWarning', array( + "<a href=\"$changeTrustedHostsUrl\">", + $invalidHost, + '</a>', + "<br/><a href=\"$validUrl\">", + $validHost, + '</a>' + )); + } else { + $view->invalidHostMessage = $warningStart . ' ' + . Piwik_Translate('CoreHome_InjectedHostNonSuperUserWarning', array( + "<br/><a href=\"$validUrl\">", + '</a>', + $mailLinkStart, + '</a>' + )); + } + $view->invalidHostMessageHowToFix = '<b>How do I fix this problem and how do I login again?</b><br/> The Piwik Super User can manually edit the file piwik/config/config.ini.php + and add the following lines: <pre>[General]' . "\n" . 'trusted_hosts[] = "' . $validHost . '"</pre><br/>After making the change, you will be able to login again.<br/><br/> You may also <i>disable this security feature (not recommended)</i>. To do so edit config/config.ini.php and add: - <pre>[General]'."\n".'enable_trusted_host_check=0</pre>'; - - $view->invalidHost = $invalidHost; // for UserSettings warning - $view->invalidHostMailLinkStart = $mailLinkStart; - } - } - - /** - * Sets general period variables (available periods, current period, period labels) used by templates - * - * @param Piwik_View $view - * @throws Exception - * @return void - */ - public static function setPeriodVariablesView($view) - { - if(isset($view->period)) - { - return; - } - - $currentPeriod = Piwik_Common::getRequestVar('period'); - $view->displayUniqueVisitors = Piwik::isUniqueVisitorsEnabled($currentPeriod); - $availablePeriods = array('day', 'week', 'month', 'year', 'range'); - if(!in_array($currentPeriod,$availablePeriods)) - { - throw new Exception("Period must be one of: ".implode(",",$availablePeriods)); - } - $periodNames = array( - 'day' => array('singular' => Piwik_Translate('CoreHome_PeriodDay'), 'plural' => Piwik_Translate('CoreHome_PeriodDays')), - 'week' => array('singular' => Piwik_Translate('CoreHome_PeriodWeek'), 'plural' => Piwik_Translate('CoreHome_PeriodWeeks')), - 'month' => array('singular' => Piwik_Translate('CoreHome_PeriodMonth'), 'plural' => Piwik_Translate('CoreHome_PeriodMonths')), - 'year' => array('singular' => Piwik_Translate('CoreHome_PeriodYear'), 'plural' => Piwik_Translate('CoreHome_PeriodYears')), - // Note: plural is not used for date range - 'range' => array('singular' => Piwik_Translate('General_DateRangeInPeriodList'), 'plural' => Piwik_Translate('General_DateRangeInPeriodList') ), - ); - - $found = array_search($currentPeriod,$availablePeriods); - if($found !== false) - { - unset($availablePeriods[$found]); - } - $view->period = $currentPeriod; - $view->otherPeriods = $availablePeriods; - $view->periodsNames = $periodNames; - } - - /** - * Set metrics variables (displayed metrics, available metrics) used by template - * Handles the server-side of the metrics picker - * - * @param Piwik_View|Piwik_ViewDataTable $view - * @param string $defaultMetricDay name of the default metric for period=day - * @param string $defaultMetric name of the default metric for other periods - * @param array $metricsForDay metrics that are only available for period=day - * @param array $metricsForAllPeriods metrics that are available for all periods - * @param bool $labelDisplayed add 'label' to columns to display? - * @return void - */ - protected function setMetricsVariablesView(Piwik_ViewDataTable $view, $defaultMetricDay='nb_uniq_visitors', - $defaultMetric='nb_visits', $metricsForDay=array('nb_uniq_visitors'), - $metricsForAllPeriods=array('nb_visits', 'nb_actions'), $labelDisplayed=true) - { - // columns is set in the request if metrics picker has been used - $columns = Piwik_Common::getRequestVar('columns', false); - if ($columns !== false) - { - $columns = Piwik::getArrayFromApiParameter($columns); - $firstColumn = $columns[0]; - } - else - { - // default columns - $firstColumn = isset($view->period) && $view->period == 'day' ? $defaultMetricDay : $defaultMetric; - $columns = array($firstColumn); - } - // displayed columns - if ($labelDisplayed - && !($view instanceof Piwik_ViewDataTable_GenerateGraphData)) - { - array_unshift($columns, 'label'); - } - $view->setColumnsToDisplay($columns); - - - // Continue only for graphs - if(!($view instanceof Piwik_ViewDataTable_GenerateGraphData)) - { - return; - } - // do not sort if sorted column was initially "label" or eg. it would make "Visits by Server time" not pretty - if($view->getSortedColumn() != 'label') - { - $view->setSortedColumn($firstColumn); - } - // selectable columns - if (isset($view->period) && $view->period == 'day') - { - $selectableColumns = array_merge($metricsForDay, $metricsForAllPeriods); - } - else - { - $selectableColumns = $metricsForAllPeriods; - } - $view->setSelectableColumns($selectableColumns); - } - - /** - * 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 string $moduleToRedirect Module, eg. "MultiSites" - * @param string $actionToRedirect Action, eg. "index" - * @param string $websiteId Website ID, eg. 1 - * @param string $defaultPeriod Default period, eg. "day" - * @param string $defaultDate Default date, eg. "today" - * @param array $parameters Parameters to append to url - */ - function redirectToIndex($moduleToRedirect, $actionToRedirect, $websiteId = null, $defaultPeriod = null, $defaultDate = null, $parameters = array()) - { - if(is_null($websiteId)) - { - $websiteId = $this->getDefaultWebsiteId(); - } - if(is_null($defaultDate)) - { - $defaultDate = $this->getDefaultDate(); - } - if(is_null($defaultPeriod)) - { - $defaultPeriod = $this->getDefaultPeriod(); - } - $parametersString = ''; - if(!empty($parameters)) - { - $parametersString = '&' . Piwik_Url::getQueryStringFromParameters($parameters); - } - - if($websiteId) { - $url = "Location: index.php?module=".$moduleToRedirect - ."&action=".$actionToRedirect - ."&idSite=".$websiteId - ."&period=".$defaultPeriod - ."&date=".$defaultDate - .$parametersString; - header($url); - exit; - } - - if(Piwik::isUserIsSuperUser()) - { - Piwik_ExitWithMessage("Error: no website was found in this Piwik installation. - <br />Check the table '". Piwik_Common::prefixTable('site') ."' in your database, it should contain your Piwik websites.", false, true); - } - - $currentLogin = Piwik::getCurrentUserLogin(); - if(!empty($currentLogin) - && $currentLogin != 'anonymous') - { - $errorMessage = sprintf(Piwik_Translate('CoreHome_NoPrivilegesAskPiwikAdmin'), $currentLogin, "<br/><a href='mailto:".Piwik::getSuperUserEmail()."?subject=Access to Piwik for user $currentLogin'>", "</a>"); - $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::getInstance()->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() - { - // NOTE: a change in this function might mean a change in plugins/UsersManager/templates/userSettings.js as well - $userSettingsDate = Piwik_UsersManager_API::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), Piwik_UsersManager_API::PREFERENCE_DEFAULT_REPORT_DATE); - if($userSettingsDate === false) - { - return Piwik_Config::getInstance()->General['default_day']; - } - if($userSettingsDate == 'yesterday') - { - return $userSettingsDate; - } - // if last7, last30, etc. - if(strpos($userSettingsDate, 'last') === 0 - || strpos($userSettingsDate, 'previous') === 0) - { - 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 Piwik_Config::getInstance()->General['default_period']; - } - if(in_array($userSettingsDate, array('today','yesterday'))) - { - return 'day'; - } - if(strpos($userSettingsDate, 'last') === 0 - || strpos($userSettingsDate, 'previous') === 0) - { - return 'range'; - } - return $userSettingsDate; - } - - /** - * Checks that the specified token matches the current logged in user token. - * Note: this protection against CSRF should be limited to controller - * actions that are either invoked via AJAX or redirect to a page - * within the site. The token should never appear in the browser's - * address bar. - * - * @throws Piwik_Access_NoAccessException if token doesn't match - * @return void - */ - protected function checkTokenInUrl() - { - if(Piwik_Common::getRequestVar('token_auth', false) != Piwik::getCurrentUserTokenAuth()) { - throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionInvalidToken')); - } - } - - /** - * Returns pretty date for use in period selector widget. - * - * @param Piwik_Period $period - * @return string - */ - public static function getCalendarPrettyDate($period) - { - if ($period instanceof Piwik_Period_Month) // show month name when period is for a month - { - return $period->getLocalizedLongString(); - } - else - { - return $period->getPrettyString(); - } - } - - - - /** - * Returns the pretty date representation - * - * @param $date string - * @param $period string - * @return string Pretty date - */ - public static function getPrettyDate($date, $period) - { - return self::getCalendarPrettyDate( Piwik_Period::factory($period, Piwik_Date::factory($date)) ); - } - - - /** - * Calculates the evolution from one value to another and returns HTML displaying - * the evolution percent. The HTML includes an up/down arrow and is colored red, black or - * green depending on whether the evolution is negative, 0 or positive. - * - * No HTML is returned if the current value and evolution percent are both 0. - * - * @param string $date The date of the current value. - * @param int $currentValue The value to calculate evolution to. - * @param string $pastDate The date of past value. - * @param int $pastValue The value in the past to calculate evolution from. - * @return string|false The HTML or false if the evolution is 0 and the current value is 0. - */ - protected function getEvolutionHtml( $date, $currentValue, $pastDate, $pastValue) - { - $evolutionPercent = Piwik_DataTable_Filter_CalculateEvolutionFilter::calculate( - $currentValue, $pastValue, $precision = 1); - - // do not display evolution if evolution percent is 0 and current value is 0 - if ($evolutionPercent == 0 - && $currentValue == 0) - { - return false; - } - - $titleEvolutionPercent = $evolutionPercent; - if ($evolutionPercent < 0) - { - $color = "#e02a3b"; //red - $img = "arrow_down.png"; - } - else if ($evolutionPercent == 0) - { - $img = "stop.png"; - } - else - { - $color = "green"; - $img = "arrow_up.png"; - $titleEvolutionPercent = '+'.$titleEvolutionPercent; - } - - $title = Piwik_Translate('General_EvolutionSummaryGeneric', array( - Piwik_Translate('General_NVisits', $currentValue), - $date, - Piwik_Translate('General_NVisits', $pastValue), - $pastDate, - $titleEvolutionPercent - )); - - $result = '<span class="metricEvolution" title="'.$title - . '"><img style="padding-right:4px" src="plugins/MultiSites/images/'.$img.'"/><strong'; - - if (isset($color)) - { - $result .= ' style="color:'.$color.'"'; - } - $result .= '>'.$evolutionPercent.'</strong></span>'; - - return $result; - } + <pre>[General]' . "\n" . 'enable_trusted_host_check=0</pre>'; + + $view->invalidHost = $invalidHost; // for UserSettings warning + $view->invalidHostMailLinkStart = $mailLinkStart; + } + } + + /** + * Sets general period variables (available periods, current period, period labels) used by templates + * + * @param Piwik_View $view + * @throws Exception + * @return void + */ + public static function setPeriodVariablesView($view) + { + if (isset($view->period)) { + return; + } + + $currentPeriod = Piwik_Common::getRequestVar('period'); + $view->displayUniqueVisitors = Piwik::isUniqueVisitorsEnabled($currentPeriod); + $availablePeriods = array('day', 'week', 'month', 'year', 'range'); + if (!in_array($currentPeriod, $availablePeriods)) { + throw new Exception("Period must be one of: " . implode(",", $availablePeriods)); + } + $periodNames = array( + 'day' => array('singular' => Piwik_Translate('CoreHome_PeriodDay'), 'plural' => Piwik_Translate('CoreHome_PeriodDays')), + 'week' => array('singular' => Piwik_Translate('CoreHome_PeriodWeek'), 'plural' => Piwik_Translate('CoreHome_PeriodWeeks')), + 'month' => array('singular' => Piwik_Translate('CoreHome_PeriodMonth'), 'plural' => Piwik_Translate('CoreHome_PeriodMonths')), + 'year' => array('singular' => Piwik_Translate('CoreHome_PeriodYear'), 'plural' => Piwik_Translate('CoreHome_PeriodYears')), + // Note: plural is not used for date range + 'range' => array('singular' => Piwik_Translate('General_DateRangeInPeriodList'), 'plural' => Piwik_Translate('General_DateRangeInPeriodList')), + ); + + $found = array_search($currentPeriod, $availablePeriods); + if ($found !== false) { + unset($availablePeriods[$found]); + } + $view->period = $currentPeriod; + $view->otherPeriods = $availablePeriods; + $view->periodsNames = $periodNames; + } + + /** + * Set metrics variables (displayed metrics, available metrics) used by template + * Handles the server-side of the metrics picker + * + * @param Piwik_View|Piwik_ViewDataTable $view + * @param string $defaultMetricDay name of the default metric for period=day + * @param string $defaultMetric name of the default metric for other periods + * @param array $metricsForDay metrics that are only available for period=day + * @param array $metricsForAllPeriods metrics that are available for all periods + * @param bool $labelDisplayed add 'label' to columns to display? + * @return void + */ + protected function setMetricsVariablesView(Piwik_ViewDataTable $view, $defaultMetricDay = 'nb_uniq_visitors', + $defaultMetric = 'nb_visits', $metricsForDay = array('nb_uniq_visitors'), + $metricsForAllPeriods = array('nb_visits', 'nb_actions'), $labelDisplayed = true) + { + // columns is set in the request if metrics picker has been used + $columns = Piwik_Common::getRequestVar('columns', false); + if ($columns !== false) { + $columns = Piwik::getArrayFromApiParameter($columns); + $firstColumn = $columns[0]; + } else { + // default columns + $firstColumn = isset($view->period) && $view->period == 'day' ? $defaultMetricDay : $defaultMetric; + $columns = array($firstColumn); + } + // displayed columns + if ($labelDisplayed + && !($view instanceof Piwik_ViewDataTable_GenerateGraphData) + ) { + array_unshift($columns, 'label'); + } + $view->setColumnsToDisplay($columns); + + + // Continue only for graphs + if (!($view instanceof Piwik_ViewDataTable_GenerateGraphData)) { + return; + } + // do not sort if sorted column was initially "label" or eg. it would make "Visits by Server time" not pretty + if ($view->getSortedColumn() != 'label') { + $view->setSortedColumn($firstColumn); + } + // selectable columns + if (isset($view->period) && $view->period == 'day') { + $selectableColumns = array_merge($metricsForDay, $metricsForAllPeriods); + } else { + $selectableColumns = $metricsForAllPeriods; + } + $view->setSelectableColumns($selectableColumns); + } + + /** + * 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 string $moduleToRedirect Module, eg. "MultiSites" + * @param string $actionToRedirect Action, eg. "index" + * @param string $websiteId Website ID, eg. 1 + * @param string $defaultPeriod Default period, eg. "day" + * @param string $defaultDate Default date, eg. "today" + * @param array $parameters Parameters to append to url + */ + function redirectToIndex($moduleToRedirect, $actionToRedirect, $websiteId = null, $defaultPeriod = null, $defaultDate = null, $parameters = array()) + { + if (is_null($websiteId)) { + $websiteId = $this->getDefaultWebsiteId(); + } + if (is_null($defaultDate)) { + $defaultDate = $this->getDefaultDate(); + } + if (is_null($defaultPeriod)) { + $defaultPeriod = $this->getDefaultPeriod(); + } + $parametersString = ''; + if (!empty($parameters)) { + $parametersString = '&' . Piwik_Url::getQueryStringFromParameters($parameters); + } + + if ($websiteId) { + $url = "Location: index.php?module=" . $moduleToRedirect + . "&action=" . $actionToRedirect + . "&idSite=" . $websiteId + . "&period=" . $defaultPeriod + . "&date=" . $defaultDate + . $parametersString; + header($url); + exit; + } + + if (Piwik::isUserIsSuperUser()) { + Piwik_ExitWithMessage("Error: no website was found in this Piwik installation. + <br />Check the table '" . Piwik_Common::prefixTable('site') . "' in your database, it should contain your Piwik websites.", false, true); + } + + $currentLogin = Piwik::getCurrentUserLogin(); + if (!empty($currentLogin) + && $currentLogin != 'anonymous' + ) { + $errorMessage = sprintf(Piwik_Translate('CoreHome_NoPrivilegesAskPiwikAdmin'), $currentLogin, "<br/><a href='mailto:" . Piwik::getSuperUserEmail() . "?subject=Access to Piwik for user $currentLogin'>", "</a>"); + $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::getInstance()->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() + { + // NOTE: a change in this function might mean a change in plugins/UsersManager/templates/userSettings.js as well + $userSettingsDate = Piwik_UsersManager_API::getInstance()->getUserPreference(Piwik::getCurrentUserLogin(), Piwik_UsersManager_API::PREFERENCE_DEFAULT_REPORT_DATE); + if ($userSettingsDate === false) { + return Piwik_Config::getInstance()->General['default_day']; + } + if ($userSettingsDate == 'yesterday') { + return $userSettingsDate; + } + // if last7, last30, etc. + if (strpos($userSettingsDate, 'last') === 0 + || strpos($userSettingsDate, 'previous') === 0 + ) { + 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 Piwik_Config::getInstance()->General['default_period']; + } + if (in_array($userSettingsDate, array('today', 'yesterday'))) { + return 'day'; + } + if (strpos($userSettingsDate, 'last') === 0 + || strpos($userSettingsDate, 'previous') === 0 + ) { + return 'range'; + } + return $userSettingsDate; + } + + /** + * Checks that the specified token matches the current logged in user token. + * Note: this protection against CSRF should be limited to controller + * actions that are either invoked via AJAX or redirect to a page + * within the site. The token should never appear in the browser's + * address bar. + * + * @throws Piwik_Access_NoAccessException if token doesn't match + * @return void + */ + protected function checkTokenInUrl() + { + if (Piwik_Common::getRequestVar('token_auth', false) != Piwik::getCurrentUserTokenAuth()) { + throw new Piwik_Access_NoAccessException(Piwik_TranslateException('General_ExceptionInvalidToken')); + } + } + + /** + * Returns pretty date for use in period selector widget. + * + * @param Piwik_Period $period + * @return string + */ + public static function getCalendarPrettyDate($period) + { + if ($period instanceof Piwik_Period_Month) // show month name when period is for a month + { + return $period->getLocalizedLongString(); + } else { + return $period->getPrettyString(); + } + } + + + /** + * Returns the pretty date representation + * + * @param $date string + * @param $period string + * @return string Pretty date + */ + public static function getPrettyDate($date, $period) + { + return self::getCalendarPrettyDate(Piwik_Period::factory($period, Piwik_Date::factory($date))); + } + + + /** + * Calculates the evolution from one value to another and returns HTML displaying + * the evolution percent. The HTML includes an up/down arrow and is colored red, black or + * green depending on whether the evolution is negative, 0 or positive. + * + * No HTML is returned if the current value and evolution percent are both 0. + * + * @param string $date The date of the current value. + * @param int $currentValue The value to calculate evolution to. + * @param string $pastDate The date of past value. + * @param int $pastValue The value in the past to calculate evolution from. + * @return string|false The HTML or false if the evolution is 0 and the current value is 0. + */ + protected function getEvolutionHtml($date, $currentValue, $pastDate, $pastValue) + { + $evolutionPercent = Piwik_DataTable_Filter_CalculateEvolutionFilter::calculate( + $currentValue, $pastValue, $precision = 1); + + // do not display evolution if evolution percent is 0 and current value is 0 + if ($evolutionPercent == 0 + && $currentValue == 0 + ) { + return false; + } + + $titleEvolutionPercent = $evolutionPercent; + if ($evolutionPercent < 0) { + $color = "#e02a3b"; //red + $img = "arrow_down.png"; + } else if ($evolutionPercent == 0) { + $img = "stop.png"; + } else { + $color = "green"; + $img = "arrow_up.png"; + $titleEvolutionPercent = '+' . $titleEvolutionPercent; + } + + $title = Piwik_Translate('General_EvolutionSummaryGeneric', array( + Piwik_Translate('General_NVisits', $currentValue), + $date, + Piwik_Translate('General_NVisits', $pastValue), + $pastDate, + $titleEvolutionPercent + )); + + $result = '<span class="metricEvolution" title="' . $title + . '"><img style="padding-right:4px" src="plugins/MultiSites/images/' . $img . '"/><strong'; + + if (isset($color)) { + $result .= ' style="color:' . $color . '"'; + } + $result .= '>' . $evolutionPercent . '</strong></span>'; + + return $result; + } } diff --git a/core/Controller/Admin.php b/core/Controller/Admin.php index 1278b94d89..2378d005d0 100644 --- a/core/Controller/Admin.php +++ b/core/Controller/Admin.php @@ -1,66 +1,65 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Parent class of all plugins Controllers with admin functions - * + * * @package Piwik */ abstract class Piwik_Controller_Admin extends Piwik_Controller { - /** - * Set the minimal variables in the view object - * Extended by some admin view specific variables - * - * @param Piwik_View $view - */ - protected function setBasicVariablesView($view) - { - parent::setBasicVariablesView($view); + /** + * Set the minimal variables in the view object + * Extended by some admin view specific variables + * + * @param Piwik_View $view + */ + protected function setBasicVariablesView($view) + { + parent::setBasicVariablesView($view); - self::setBasicVariablesAdminView($view); - } + self::setBasicVariablesAdminView($view); + } - static public function setBasicVariablesAdminView($view) - { - $statsEnabled = Piwik_Config::getInstance()->Tracker['record_statistics']; - if($statsEnabled == "0"){ - $view->statisticsNotRecorded = true; - } + static public function setBasicVariablesAdminView($view) + { + $statsEnabled = Piwik_Config::getInstance()->Tracker['record_statistics']; + if ($statsEnabled == "0") { + $view->statisticsNotRecorded = true; + } - $view->topMenu = Piwik_GetTopMenu(); - $view->currentAdminMenuName = Piwik_GetCurrentAdminMenuName(); + $view->topMenu = Piwik_GetTopMenu(); + $view->currentAdminMenuName = Piwik_GetCurrentAdminMenuName(); - $view->enableFrames = Piwik_Config::getInstance()->General['enable_framed_settings']; - if (!$view->enableFrames) { - $view->setXFrameOptions('sameorigin'); - } - - $view->isSuperUser = Piwik::isUserIsSuperUser(); - - // for old geoip plugin warning - $view->usingOldGeoIPPlugin = Piwik_PluginsManager::getInstance()->isPluginActivated('GeoIP'); - - // for cannot find installed plugin warning - $missingPlugins = Piwik_PluginsManager::getInstance()->getMissingPlugins(); - if (!empty($missingPlugins)) - { - $pluginsLink = Piwik_Url::getCurrentQueryStringWithParametersModified(array( - 'module' => 'CorePluginsAdmin', 'action' => 'index' - )); - $view->missingPluginsWarning = Piwik_Translate('CoreAdminHome_MissingPluginsWarning', array( - '<strong>'.implode('</strong>, <strong>', $missingPlugins).'</strong>', - '<a href="'.$pluginsLink.'"/>', - '</a>' - )); - } - } + $view->enableFrames = Piwik_Config::getInstance()->General['enable_framed_settings']; + if (!$view->enableFrames) { + $view->setXFrameOptions('sameorigin'); + } + + $view->isSuperUser = Piwik::isUserIsSuperUser(); + + // for old geoip plugin warning + $view->usingOldGeoIPPlugin = Piwik_PluginsManager::getInstance()->isPluginActivated('GeoIP'); + + // for cannot find installed plugin warning + $missingPlugins = Piwik_PluginsManager::getInstance()->getMissingPlugins(); + if (!empty($missingPlugins)) { + $pluginsLink = Piwik_Url::getCurrentQueryStringWithParametersModified(array( + 'module' => 'CorePluginsAdmin', 'action' => 'index' + )); + $view->missingPluginsWarning = Piwik_Translate('CoreAdminHome_MissingPluginsWarning', array( + '<strong>' . implode('</strong>, <strong>', $missingPlugins) . '</strong>', + '<a href="' . $pluginsLink . '"/>', + '</a>' + )); + } + } } diff --git a/core/Cookie.php b/core/Cookie.php index 2651cf390e..46f9dc9e66 100644 --- a/core/Cookie.php +++ b/core/Cookie.php @@ -19,382 +19,367 @@ */ class Piwik_Cookie { - /** - * Don't create a cookie bigger than 1k - */ - const MAX_COOKIE_SIZE = 1024; - - /** - * The name of the cookie - * @var string - */ - protected $name = null; - - /** - * The expire time for the cookie (expressed in UNIX Timestamp) - * @var int - */ - protected $expire = null; - - /** - * Restrict cookie path - * @var string - */ - protected $path = ''; - - /** - * Restrict cookie to a domain (or subdomains) - * @var string - */ - protected $domain = ''; - - /** - * If true, cookie should only be transmitted over secure HTTPS - * @var bool - */ - protected $secure = false; - - /** - * If true, cookie will only be made available via the HTTP protocol. - * Note: not well supported by browsers. - * @var bool - */ - protected $httponly = false; - - /** - * The content of the cookie - * @var array - */ - protected $value = array(); - - /** - * The character used to separate the tuple name=value in the cookie - */ - const VALUE_SEPARATOR = ':'; - - /** - * Instantiate a new Cookie object and tries to load the cookie content if the cookie - * exists already. - * - * @param string $cookieName cookie Name - * @param int $expire The timestamp after which the cookie will expire, eg time() + 86400; - * use 0 (int zero) to expire cookie at end of browser session - * @param string $path The path on the server in which the cookie will be available on. - * @param bool|string $keyStore Will be used to store several bits of data (eg. one array per website) - */ - public function __construct( $cookieName, $expire = null, $path = null, $keyStore = false) - { - $this->name = $cookieName; - $this->path = $path; - $this->expire = $expire; - if(is_null($expire) - || !is_numeric($expire) - || $expire < 0) - { - $this->expire = $this->getDefaultExpire(); - } - - $this->keyStore = $keyStore; - if($this->isCookieFound()) - { - $this->loadContentFromCookie(); - } - } - - /** - * Returns true if the visitor already has the cookie. - * - * @return bool - */ - public function isCookieFound() - { - return isset($_COOKIE[$this->name]); - } - - /** - * Returns the default expiry time, 2 years - * - * @return int Timestamp in 2 years - */ - protected function getDefaultExpire() - { - return time() + 86400*365*2; - } - - /** - * setcookie() replacement -- we don't use the built-in function because - * it is buggy for some PHP versions. - * - * @link http://php.net/setcookie - * - * @param string $Name Name of cookie - * @param string $Value Value of cookie - * @param int $Expires Time the cookie expires - * @param string $Path - * @param string $Domain - * @param bool $Secure - * @param bool $HTTPOnly - */ - protected function setCookie($Name, $Value, $Expires, $Path = '', $Domain = '', $Secure = false, $HTTPOnly = false) - { - if (!empty($Domain)) - { - // Fix the domain to accept domains with and without 'www.'. - if (!strncasecmp($Domain, 'www.', 4)) - { - $Domain = substr($Domain, 4); - } - $Domain = '.' . $Domain; - - // Remove port information. - $Port = strpos($Domain, ':'); - if ($Port !== false) $Domain = substr($Domain, 0, $Port); - } - - $header = 'Set-Cookie: ' . rawurlencode($Name) . '=' . rawurlencode($Value) - . (empty($Expires) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', $Expires) . ' GMT') - . (empty($Path) ? '' : '; path=' . $Path) - . (empty($Domain) ? '' : '; domain=' . $Domain) - . (!$Secure ? '' : '; secure') - . (!$HTTPOnly ? '' : '; HttpOnly'); - - Piwik_Common::sendHeader($header, false); - } - - /** - * We set the privacy policy header - */ - protected function setP3PHeader() - { - Piwik_Common::sendHeader("P3P: CP='OTI DSP COR NID STP UNI OTPa OUR'"); - } - - /** - * Delete the cookie - */ - public function delete() - { - $this->setP3PHeader(); - $this->setCookie($this->name, 'deleted', time() - 31536001, $this->path, $this->domain); - } - - /** - * Saves the cookie (set the Cookie header). - * You have to call this method before sending any text to the browser or you would get the - * "Header already sent" error. - */ - public function save() - { - $cookieString = $this->generateContentString(); - if(strlen($cookieString) > self::MAX_COOKIE_SIZE) - { - // If the cookie was going to be too large, instead, delete existing cookie and start afresh - $this->delete(); - return; - } - - $this->setP3PHeader(); - $this->setCookie($this->name, $cookieString, $this->expire, $this->path, $this->domain, $this->secure, $this->httponly); - } - - /** - * Extract signed content from string: content VALUE_SEPARATOR '_=' signature - * - * @param string $content - * @return string|bool Content or false if unsigned - */ - private function extractSignedContent($content) - { - $signature = substr($content, -40); - if(substr($content, -43, 3) == self::VALUE_SEPARATOR . '_=' && - $signature == sha1(substr($content, 0, -40) . Piwik_Common::getSalt())) - { - // strip trailing: VALUE_SEPARATOR '_=' signature" - return substr($content, 0, -43); - } - return false; - } - - /** - * Load the cookie content into a php array. - * Parses the cookie string to extract the different variables. - * Unserialize the array when necessary. - * Decode the non numeric values that were base64 encoded. - */ - protected function loadContentFromCookie() - { - $cookieStr = $this->extractSignedContent($_COOKIE[$this->name]); - if($cookieStr === false) - { - return; - } - - $values = explode( self::VALUE_SEPARATOR, $cookieStr); - foreach($values as $nameValue) - { - $equalPos = strpos($nameValue, '='); - $varName = substr($nameValue,0,$equalPos); - $varValue = substr($nameValue,$equalPos+1); - - // no numeric value are base64 encoded so we need to decode them - if(!is_numeric($varValue)) - { - $tmpValue = base64_decode($varValue); - $varValue = safe_unserialize($tmpValue); - - // discard entire cookie - // note: this assumes we never serialize a boolean - if($varValue === false && $tmpValue !== 'b:0;') - { - $this->value = array(); - unset($_COOKIE[$this->name]); - break; - } - } - - $this->value[$varName] = $varValue; - } - } - - /** - * Returns the string to save in the cookie from the $this->value array of values. - * It goes through the array and generates the cookie content string. - * - * @return string Cookie content - */ - protected function generateContentString() - { - $cookieStr = ''; - foreach($this->value as $name=>$value) - { - if(!is_numeric($value)) - { - $value = base64_encode(safe_serialize($value)); - } - - $cookieStr .= "$name=$value" . self::VALUE_SEPARATOR; - } - - if(!empty($cookieStr)) - { - $cookieStr .= '_='; - - // sign cookie - $signature = sha1($cookieStr . Piwik_Common::getSalt()); - return $cookieStr . $signature; - } - - return ''; - } - - /** - * Set cookie domain - * - * @param string $domain - */ - public function setDomain($domain) - { - $this->domain = $domain; - } - - /** - * Set secure flag - * - * @param bool $secure - */ - public function setSecure($secure) - { - $this->secure = $secure; - } - - /** - * Set HTTP only - * - * @param bool $httponly - */ - public function setHttpOnly($httponly) - { - $this->httponly = $httponly; - } - - /** - * Registers a new name => value association in the cookie. - * - * Registering new values is optimal if the value is a numeric value. - * If the value is a string, it will be saved as a base64 encoded string. - * If the value is an array, it will be saved as a serialized and base64 encoded - * string which is not very good in terms of bytes usage. - * You should save arrays only when you are sure about their maximum data size. - * A cookie has to stay small and its size shouldn't increase over time! - * - * @param string $name Name of the value to save; the name will be used to retrieve this value - * @param string|array|number $value Value to save. If null, entry will be deleted from cookie. - */ - public function set( $name, $value ) - { - $name = self::escapeValue($name); - - // Delete value if $value === null - if(is_null($value)) - { - if($this->keyStore === false) - { - unset($this->value[$name]); - return; - } - unset($this->value[$this->keyStore][$name]); - return; - } - - if($this->keyStore === false) - { - $this->value[$name] = $value; - return; - } - $this->value[$this->keyStore][$name] = $value; - } - - /** - * Returns the value defined by $name from the cookie. - * - * @param string|integer Index name of the value to return - * @return mixed The value if found, false if the value is not found - */ - public function get( $name ) - { - $name = self::escapeValue($name); - if($this->keyStore === false) - { - return isset($this->value[$name]) - ? self::escapeValue($this->value[$name]) - : false; - } - return isset($this->value[$this->keyStore][$name]) - ? self::escapeValue($this->value[$this->keyStore][$name]) - : false; - } - - /** - * Returns an easy to read cookie dump - * - * @return string The cookie dump - */ - public function __toString() - { - $str = 'COOKIE '.$this->name.', rows count: '.count($this->value). ', cookie size = '.strlen($this->generateContentString())." bytes\n"; - $str .= var_export($this->value, $return = true); - return $str; - } - - /** - * Escape values from the cookie before sending them back to the client - * (when using the get() method). - * - * @param string $value Value to be escaped - * @return mixed The value once cleaned. - */ - static protected function escapeValue( $value ) - { - return Piwik_Common::sanitizeInputValues($value); - } + /** + * Don't create a cookie bigger than 1k + */ + const MAX_COOKIE_SIZE = 1024; + + /** + * The name of the cookie + * @var string + */ + protected $name = null; + + /** + * The expire time for the cookie (expressed in UNIX Timestamp) + * @var int + */ + protected $expire = null; + + /** + * Restrict cookie path + * @var string + */ + protected $path = ''; + + /** + * Restrict cookie to a domain (or subdomains) + * @var string + */ + protected $domain = ''; + + /** + * If true, cookie should only be transmitted over secure HTTPS + * @var bool + */ + protected $secure = false; + + /** + * If true, cookie will only be made available via the HTTP protocol. + * Note: not well supported by browsers. + * @var bool + */ + protected $httponly = false; + + /** + * The content of the cookie + * @var array + */ + protected $value = array(); + + /** + * The character used to separate the tuple name=value in the cookie + */ + const VALUE_SEPARATOR = ':'; + + /** + * Instantiate a new Cookie object and tries to load the cookie content if the cookie + * exists already. + * + * @param string $cookieName cookie Name + * @param int $expire The timestamp after which the cookie will expire, eg time() + 86400; + * use 0 (int zero) to expire cookie at end of browser session + * @param string $path The path on the server in which the cookie will be available on. + * @param bool|string $keyStore Will be used to store several bits of data (eg. one array per website) + */ + public function __construct($cookieName, $expire = null, $path = null, $keyStore = false) + { + $this->name = $cookieName; + $this->path = $path; + $this->expire = $expire; + if (is_null($expire) + || !is_numeric($expire) + || $expire < 0 + ) { + $this->expire = $this->getDefaultExpire(); + } + + $this->keyStore = $keyStore; + if ($this->isCookieFound()) { + $this->loadContentFromCookie(); + } + } + + /** + * Returns true if the visitor already has the cookie. + * + * @return bool + */ + public function isCookieFound() + { + return isset($_COOKIE[$this->name]); + } + + /** + * Returns the default expiry time, 2 years + * + * @return int Timestamp in 2 years + */ + protected function getDefaultExpire() + { + return time() + 86400 * 365 * 2; + } + + /** + * setcookie() replacement -- we don't use the built-in function because + * it is buggy for some PHP versions. + * + * @link http://php.net/setcookie + * + * @param string $Name Name of cookie + * @param string $Value Value of cookie + * @param int $Expires Time the cookie expires + * @param string $Path + * @param string $Domain + * @param bool $Secure + * @param bool $HTTPOnly + */ + protected function setCookie($Name, $Value, $Expires, $Path = '', $Domain = '', $Secure = false, $HTTPOnly = false) + { + if (!empty($Domain)) { + // Fix the domain to accept domains with and without 'www.'. + if (!strncasecmp($Domain, 'www.', 4)) { + $Domain = substr($Domain, 4); + } + $Domain = '.' . $Domain; + + // Remove port information. + $Port = strpos($Domain, ':'); + if ($Port !== false) $Domain = substr($Domain, 0, $Port); + } + + $header = 'Set-Cookie: ' . rawurlencode($Name) . '=' . rawurlencode($Value) + . (empty($Expires) ? '' : '; expires=' . gmdate('D, d-M-Y H:i:s', $Expires) . ' GMT') + . (empty($Path) ? '' : '; path=' . $Path) + . (empty($Domain) ? '' : '; domain=' . $Domain) + . (!$Secure ? '' : '; secure') + . (!$HTTPOnly ? '' : '; HttpOnly'); + + Piwik_Common::sendHeader($header, false); + } + + /** + * We set the privacy policy header + */ + protected function setP3PHeader() + { + Piwik_Common::sendHeader("P3P: CP='OTI DSP COR NID STP UNI OTPa OUR'"); + } + + /** + * Delete the cookie + */ + public function delete() + { + $this->setP3PHeader(); + $this->setCookie($this->name, 'deleted', time() - 31536001, $this->path, $this->domain); + } + + /** + * Saves the cookie (set the Cookie header). + * You have to call this method before sending any text to the browser or you would get the + * "Header already sent" error. + */ + public function save() + { + $cookieString = $this->generateContentString(); + if (strlen($cookieString) > self::MAX_COOKIE_SIZE) { + // If the cookie was going to be too large, instead, delete existing cookie and start afresh + $this->delete(); + return; + } + + $this->setP3PHeader(); + $this->setCookie($this->name, $cookieString, $this->expire, $this->path, $this->domain, $this->secure, $this->httponly); + } + + /** + * Extract signed content from string: content VALUE_SEPARATOR '_=' signature + * + * @param string $content + * @return string|bool Content or false if unsigned + */ + private function extractSignedContent($content) + { + $signature = substr($content, -40); + if (substr($content, -43, 3) == self::VALUE_SEPARATOR . '_=' && + $signature == sha1(substr($content, 0, -40) . Piwik_Common::getSalt()) + ) { + // strip trailing: VALUE_SEPARATOR '_=' signature" + return substr($content, 0, -43); + } + return false; + } + + /** + * Load the cookie content into a php array. + * Parses the cookie string to extract the different variables. + * Unserialize the array when necessary. + * Decode the non numeric values that were base64 encoded. + */ + protected function loadContentFromCookie() + { + $cookieStr = $this->extractSignedContent($_COOKIE[$this->name]); + if ($cookieStr === false) { + return; + } + + $values = explode(self::VALUE_SEPARATOR, $cookieStr); + foreach ($values as $nameValue) { + $equalPos = strpos($nameValue, '='); + $varName = substr($nameValue, 0, $equalPos); + $varValue = substr($nameValue, $equalPos + 1); + + // no numeric value are base64 encoded so we need to decode them + if (!is_numeric($varValue)) { + $tmpValue = base64_decode($varValue); + $varValue = safe_unserialize($tmpValue); + + // discard entire cookie + // note: this assumes we never serialize a boolean + if ($varValue === false && $tmpValue !== 'b:0;') { + $this->value = array(); + unset($_COOKIE[$this->name]); + break; + } + } + + $this->value[$varName] = $varValue; + } + } + + /** + * Returns the string to save in the cookie from the $this->value array of values. + * It goes through the array and generates the cookie content string. + * + * @return string Cookie content + */ + protected function generateContentString() + { + $cookieStr = ''; + foreach ($this->value as $name => $value) { + if (!is_numeric($value)) { + $value = base64_encode(safe_serialize($value)); + } + + $cookieStr .= "$name=$value" . self::VALUE_SEPARATOR; + } + + if (!empty($cookieStr)) { + $cookieStr .= '_='; + + // sign cookie + $signature = sha1($cookieStr . Piwik_Common::getSalt()); + return $cookieStr . $signature; + } + + return ''; + } + + /** + * Set cookie domain + * + * @param string $domain + */ + public function setDomain($domain) + { + $this->domain = $domain; + } + + /** + * Set secure flag + * + * @param bool $secure + */ + public function setSecure($secure) + { + $this->secure = $secure; + } + + /** + * Set HTTP only + * + * @param bool $httponly + */ + public function setHttpOnly($httponly) + { + $this->httponly = $httponly; + } + + /** + * Registers a new name => value association in the cookie. + * + * Registering new values is optimal if the value is a numeric value. + * If the value is a string, it will be saved as a base64 encoded string. + * If the value is an array, it will be saved as a serialized and base64 encoded + * string which is not very good in terms of bytes usage. + * You should save arrays only when you are sure about their maximum data size. + * A cookie has to stay small and its size shouldn't increase over time! + * + * @param string $name Name of the value to save; the name will be used to retrieve this value + * @param string|array|number $value Value to save. If null, entry will be deleted from cookie. + */ + public function set($name, $value) + { + $name = self::escapeValue($name); + + // Delete value if $value === null + if (is_null($value)) { + if ($this->keyStore === false) { + unset($this->value[$name]); + return; + } + unset($this->value[$this->keyStore][$name]); + return; + } + + if ($this->keyStore === false) { + $this->value[$name] = $value; + return; + } + $this->value[$this->keyStore][$name] = $value; + } + + /** + * Returns the value defined by $name from the cookie. + * + * @param string|integer Index name of the value to return + * @return mixed The value if found, false if the value is not found + */ + public function get($name) + { + $name = self::escapeValue($name); + if ($this->keyStore === false) { + return isset($this->value[$name]) + ? self::escapeValue($this->value[$name]) + : false; + } + return isset($this->value[$this->keyStore][$name]) + ? self::escapeValue($this->value[$this->keyStore][$name]) + : false; + } + + /** + * Returns an easy to read cookie dump + * + * @return string The cookie dump + */ + public function __toString() + { + $str = 'COOKIE ' . $this->name . ', rows count: ' . count($this->value) . ', cookie size = ' . strlen($this->generateContentString()) . " bytes\n"; + $str .= var_export($this->value, $return = true); + return $str; + } + + /** + * Escape values from the cookie before sending them back to the client + * (when using the get() method). + * + * @param string $value Value to be escaped + * @return mixed The value once cleaned. + */ + static protected function escapeValue($value) + { + return Piwik_Common::sanitizeInputValues($value); + } } diff --git a/core/DataFiles/Countries.php b/core/DataFiles/Countries.php index 6690d75523..2c061e68bf 100644 --- a/core/DataFiles/Countries.php +++ b/core/DataFiles/Countries.php @@ -19,312 +19,310 @@ * America lies on its own continental plate (i.e., the Caribbean Plate), we * currently use a separate continent code (amc). */ -if(!isset($GLOBALS['Piwik_CountryList'])) -{ - // Primary reference: ISO 3166-1 alpha-2 - $GLOBALS['Piwik_CountryList'] = array( - 'ad' => 'eur', - 'ae' => 'asi', - 'af' => 'asi', - 'ag' => 'amc', - 'ai' => 'amc', - 'al' => 'eur', - 'am' => 'asi', - 'ao' => 'afr', - 'aq' => 'ant', - 'ar' => 'ams', - 'as' => 'oce', - 'at' => 'eur', - 'au' => 'oce', - 'aw' => 'amc', - 'ax' => 'eur', - 'az' => 'asi', - 'ba' => 'eur', - 'bb' => 'amc', - 'bd' => 'asi', - 'be' => 'eur', - 'bf' => 'afr', - 'bg' => 'eur', - 'bh' => 'asi', - 'bi' => 'afr', - 'bj' => 'afr', - 'bl' => 'amc', - 'bm' => 'amc', - 'bn' => 'asi', - 'bo' => 'ams', - 'bq' => 'amc', - 'br' => 'ams', - 'bs' => 'amc', - 'bt' => 'asi', - 'bv' => 'ant', - 'bw' => 'afr', - 'by' => 'eur', - 'bz' => 'amc', - 'ca' => 'amn', - 'cc' => 'asi', - 'cd' => 'afr', - 'cf' => 'afr', - 'cg' => 'afr', - 'ch' => 'eur', - 'ci' => 'afr', - 'ck' => 'oce', - 'cl' => 'ams', - 'cm' => 'afr', - 'cn' => 'asi', - 'co' => 'ams', - 'cr' => 'amc', - 'cu' => 'amc', - 'cv' => 'afr', - 'cw' => 'amc', - 'cx' => 'asi', - 'cy' => 'eur', - 'cz' => 'eur', - 'de' => 'eur', - 'dj' => 'afr', - 'dk' => 'eur', - 'dm' => 'amc', - 'do' => 'amc', - 'dz' => 'afr', - 'ec' => 'ams', - 'ee' => 'eur', - 'eg' => 'afr', - 'eh' => 'afr', - 'er' => 'afr', - 'es' => 'eur', - 'et' => 'afr', - 'fi' => 'eur', - 'fj' => 'oce', - 'fk' => 'ams', - 'fm' => 'oce', - 'fo' => 'eur', - 'fr' => 'eur', - 'ga' => 'afr', - 'gb' => 'eur', - 'gd' => 'amc', - 'ge' => 'asi', - 'gf' => 'ams', - 'gg' => 'eur', - 'gh' => 'afr', - 'gi' => 'eur', - 'gl' => 'amn', - 'gm' => 'afr', - 'gn' => 'afr', - 'gp' => 'amc', - 'gq' => 'afr', - 'gr' => 'eur', - 'gs' => 'ant', - 'gt' => 'amc', - 'gu' => 'oce', - 'gw' => 'afr', - 'gy' => 'ams', - 'hk' => 'asi', - 'hm' => 'ant', - 'hn' => 'amc', - 'hr' => 'eur', - 'ht' => 'amc', - 'hu' => 'eur', - 'id' => 'asi', - 'ie' => 'eur', - 'il' => 'asi', - 'im' => 'eur', - 'in' => 'asi', - 'io' => 'asi', - 'iq' => 'asi', - 'ir' => 'asi', - 'is' => 'eur', - 'it' => 'eur', - 'je' => 'eur', - 'jm' => 'amc', - 'jo' => 'asi', - 'jp' => 'asi', - 'ke' => 'afr', - 'kg' => 'asi', - 'kh' => 'asi', - 'ki' => 'oce', - 'km' => 'afr', - 'kn' => 'amc', - 'kp' => 'asi', - 'kr' => 'asi', - 'kw' => 'asi', - 'ky' => 'amc', - 'kz' => 'asi', - 'la' => 'asi', - 'lb' => 'asi', - 'lc' => 'amc', - 'li' => 'eur', - 'lk' => 'asi', - 'lr' => 'afr', - 'ls' => 'afr', - 'lt' => 'eur', - 'lu' => 'eur', - 'lv' => 'eur', - 'ly' => 'afr', - 'ma' => 'afr', - 'mc' => 'eur', - 'md' => 'eur', - 'me' => 'eur', - 'mf' => 'amc', - 'mg' => 'afr', - 'mh' => 'oce', - 'mk' => 'eur', - 'ml' => 'afr', - 'mm' => 'asi', - 'mn' => 'asi', - 'mo' => 'asi', - 'mp' => 'oce', - 'mq' => 'amc', - 'mr' => 'afr', - 'ms' => 'amc', - 'mt' => 'eur', - 'mu' => 'afr', - 'mv' => 'asi', - 'mw' => 'afr', - 'mx' => 'amn', - 'my' => 'asi', - 'mz' => 'afr', - 'na' => 'afr', - 'nc' => 'oce', - 'ne' => 'afr', - 'nf' => 'oce', - 'ng' => 'afr', - 'ni' => 'amc', - 'nl' => 'eur', - 'no' => 'eur', - 'np' => 'asi', - 'nr' => 'oce', - 'nu' => 'oce', - 'nz' => 'oce', - 'om' => 'asi', - 'pa' => 'amc', - 'pe' => 'ams', - 'pf' => 'oce', - 'pg' => 'oce', - 'ph' => 'asi', - 'pk' => 'asi', - 'pl' => 'eur', - 'pm' => 'amn', - 'pn' => 'oce', - 'pr' => 'amc', - 'ps' => 'asi', - 'pt' => 'eur', - 'pw' => 'oce', - 'py' => 'ams', - 'qa' => 'asi', - 're' => 'afr', - 'ro' => 'eur', - 'rs' => 'eur', - 'ru' => 'eur', - 'rw' => 'afr', - 'sa' => 'asi', - 'sb' => 'oce', - 'sc' => 'afr', - 'sd' => 'afr', - 'se' => 'eur', - 'sg' => 'asi', - 'sh' => 'afr', - 'si' => 'eur', - 'sj' => 'eur', - 'sk' => 'eur', - 'sl' => 'afr', - 'sm' => 'eur', - 'sn' => 'afr', - 'so' => 'afr', - 'sr' => 'ams', - 'ss' => 'afr', - 'st' => 'afr', - 'sv' => 'amc', - 'sx' => 'amc', - 'sy' => 'asi', - 'sz' => 'afr', - 'tc' => 'amc', - 'td' => 'afr', - 'tf' => 'ant', - 'tg' => 'afr', - 'th' => 'asi', - 'ti' => 'asi', - 'tj' => 'asi', - 'tk' => 'oce', - 'tl' => 'asi', - 'tm' => 'asi', - 'tn' => 'afr', - 'to' => 'oce', - 'tr' => 'eur', - 'tt' => 'amc', - 'tv' => 'oce', - 'tw' => 'asi', - 'tz' => 'afr', - 'ua' => 'eur', - 'ug' => 'afr', - 'um' => 'oce', - 'us' => 'amn', - 'uy' => 'ams', - 'uz' => 'asi', - 'va' => 'eur', - 'vc' => 'amc', - 've' => 'ams', - 'vg' => 'amc', - 'vi' => 'amc', - 'vn' => 'asi', - 'vu' => 'oce', - 'wf' => 'oce', - 'ws' => 'oce', - 'ye' => 'asi', - 'yt' => 'afr', - 'za' => 'afr', - 'zm' => 'afr', - 'zw' => 'afr', - ); +if (!isset($GLOBALS['Piwik_CountryList'])) { + // Primary reference: ISO 3166-1 alpha-2 + $GLOBALS['Piwik_CountryList'] = array( + 'ad' => 'eur', + 'ae' => 'asi', + 'af' => 'asi', + 'ag' => 'amc', + 'ai' => 'amc', + 'al' => 'eur', + 'am' => 'asi', + 'ao' => 'afr', + 'aq' => 'ant', + 'ar' => 'ams', + 'as' => 'oce', + 'at' => 'eur', + 'au' => 'oce', + 'aw' => 'amc', + 'ax' => 'eur', + 'az' => 'asi', + 'ba' => 'eur', + 'bb' => 'amc', + 'bd' => 'asi', + 'be' => 'eur', + 'bf' => 'afr', + 'bg' => 'eur', + 'bh' => 'asi', + 'bi' => 'afr', + 'bj' => 'afr', + 'bl' => 'amc', + 'bm' => 'amc', + 'bn' => 'asi', + 'bo' => 'ams', + 'bq' => 'amc', + 'br' => 'ams', + 'bs' => 'amc', + 'bt' => 'asi', + 'bv' => 'ant', + 'bw' => 'afr', + 'by' => 'eur', + 'bz' => 'amc', + 'ca' => 'amn', + 'cc' => 'asi', + 'cd' => 'afr', + 'cf' => 'afr', + 'cg' => 'afr', + 'ch' => 'eur', + 'ci' => 'afr', + 'ck' => 'oce', + 'cl' => 'ams', + 'cm' => 'afr', + 'cn' => 'asi', + 'co' => 'ams', + 'cr' => 'amc', + 'cu' => 'amc', + 'cv' => 'afr', + 'cw' => 'amc', + 'cx' => 'asi', + 'cy' => 'eur', + 'cz' => 'eur', + 'de' => 'eur', + 'dj' => 'afr', + 'dk' => 'eur', + 'dm' => 'amc', + 'do' => 'amc', + 'dz' => 'afr', + 'ec' => 'ams', + 'ee' => 'eur', + 'eg' => 'afr', + 'eh' => 'afr', + 'er' => 'afr', + 'es' => 'eur', + 'et' => 'afr', + 'fi' => 'eur', + 'fj' => 'oce', + 'fk' => 'ams', + 'fm' => 'oce', + 'fo' => 'eur', + 'fr' => 'eur', + 'ga' => 'afr', + 'gb' => 'eur', + 'gd' => 'amc', + 'ge' => 'asi', + 'gf' => 'ams', + 'gg' => 'eur', + 'gh' => 'afr', + 'gi' => 'eur', + 'gl' => 'amn', + 'gm' => 'afr', + 'gn' => 'afr', + 'gp' => 'amc', + 'gq' => 'afr', + 'gr' => 'eur', + 'gs' => 'ant', + 'gt' => 'amc', + 'gu' => 'oce', + 'gw' => 'afr', + 'gy' => 'ams', + 'hk' => 'asi', + 'hm' => 'ant', + 'hn' => 'amc', + 'hr' => 'eur', + 'ht' => 'amc', + 'hu' => 'eur', + 'id' => 'asi', + 'ie' => 'eur', + 'il' => 'asi', + 'im' => 'eur', + 'in' => 'asi', + 'io' => 'asi', + 'iq' => 'asi', + 'ir' => 'asi', + 'is' => 'eur', + 'it' => 'eur', + 'je' => 'eur', + 'jm' => 'amc', + 'jo' => 'asi', + 'jp' => 'asi', + 'ke' => 'afr', + 'kg' => 'asi', + 'kh' => 'asi', + 'ki' => 'oce', + 'km' => 'afr', + 'kn' => 'amc', + 'kp' => 'asi', + 'kr' => 'asi', + 'kw' => 'asi', + 'ky' => 'amc', + 'kz' => 'asi', + 'la' => 'asi', + 'lb' => 'asi', + 'lc' => 'amc', + 'li' => 'eur', + 'lk' => 'asi', + 'lr' => 'afr', + 'ls' => 'afr', + 'lt' => 'eur', + 'lu' => 'eur', + 'lv' => 'eur', + 'ly' => 'afr', + 'ma' => 'afr', + 'mc' => 'eur', + 'md' => 'eur', + 'me' => 'eur', + 'mf' => 'amc', + 'mg' => 'afr', + 'mh' => 'oce', + 'mk' => 'eur', + 'ml' => 'afr', + 'mm' => 'asi', + 'mn' => 'asi', + 'mo' => 'asi', + 'mp' => 'oce', + 'mq' => 'amc', + 'mr' => 'afr', + 'ms' => 'amc', + 'mt' => 'eur', + 'mu' => 'afr', + 'mv' => 'asi', + 'mw' => 'afr', + 'mx' => 'amn', + 'my' => 'asi', + 'mz' => 'afr', + 'na' => 'afr', + 'nc' => 'oce', + 'ne' => 'afr', + 'nf' => 'oce', + 'ng' => 'afr', + 'ni' => 'amc', + 'nl' => 'eur', + 'no' => 'eur', + 'np' => 'asi', + 'nr' => 'oce', + 'nu' => 'oce', + 'nz' => 'oce', + 'om' => 'asi', + 'pa' => 'amc', + 'pe' => 'ams', + 'pf' => 'oce', + 'pg' => 'oce', + 'ph' => 'asi', + 'pk' => 'asi', + 'pl' => 'eur', + 'pm' => 'amn', + 'pn' => 'oce', + 'pr' => 'amc', + 'ps' => 'asi', + 'pt' => 'eur', + 'pw' => 'oce', + 'py' => 'ams', + 'qa' => 'asi', + 're' => 'afr', + 'ro' => 'eur', + 'rs' => 'eur', + 'ru' => 'eur', + 'rw' => 'afr', + 'sa' => 'asi', + 'sb' => 'oce', + 'sc' => 'afr', + 'sd' => 'afr', + 'se' => 'eur', + 'sg' => 'asi', + 'sh' => 'afr', + 'si' => 'eur', + 'sj' => 'eur', + 'sk' => 'eur', + 'sl' => 'afr', + 'sm' => 'eur', + 'sn' => 'afr', + 'so' => 'afr', + 'sr' => 'ams', + 'ss' => 'afr', + 'st' => 'afr', + 'sv' => 'amc', + 'sx' => 'amc', + 'sy' => 'asi', + 'sz' => 'afr', + 'tc' => 'amc', + 'td' => 'afr', + 'tf' => 'ant', + 'tg' => 'afr', + 'th' => 'asi', + 'ti' => 'asi', + 'tj' => 'asi', + 'tk' => 'oce', + 'tl' => 'asi', + 'tm' => 'asi', + 'tn' => 'afr', + 'to' => 'oce', + 'tr' => 'eur', + 'tt' => 'amc', + 'tv' => 'oce', + 'tw' => 'asi', + 'tz' => 'afr', + 'ua' => 'eur', + 'ug' => 'afr', + 'um' => 'oce', + 'us' => 'amn', + 'uy' => 'ams', + 'uz' => 'asi', + 'va' => 'eur', + 'vc' => 'amc', + 've' => 'ams', + 'vg' => 'amc', + 'vi' => 'amc', + 'vn' => 'asi', + 'vu' => 'oce', + 'wf' => 'oce', + 'ws' => 'oce', + 'ye' => 'asi', + 'yt' => 'afr', + 'za' => 'afr', + 'zm' => 'afr', + 'zw' => 'afr', + ); - // codes for internal use - $GLOBALS['Piwik_CountryList_Extras'] = array( - // unknown - 'xx' => 'unk', + // codes for internal use + $GLOBALS['Piwik_CountryList_Extras'] = array( + // unknown + 'xx' => 'unk', - // exceptionally reserved - 'ac' => 'afr', // .ac TLD - 'cp' => 'amc', - 'dg' => 'asi', - 'ea' => 'afr', - 'eu' => 'eur', // .eu TLD - 'fx' => 'eur', - 'ic' => 'afr', - 'su' => 'eur', // .su TLD - 'ta' => 'afr', - 'uk' => 'eur', // .uk TLD + // exceptionally reserved + 'ac' => 'afr', // .ac TLD + 'cp' => 'amc', + 'dg' => 'asi', + 'ea' => 'afr', + 'eu' => 'eur', // .eu TLD + 'fx' => 'eur', + 'ic' => 'afr', + 'su' => 'eur', // .su TLD + 'ta' => 'afr', + 'uk' => 'eur', // .uk TLD - // transitionally reserved - 'an' => 'amc', // former Netherlands Antilles - 'bu' => 'asi', - 'cs' => 'eur', // former Serbia and Montenegro - 'nt' => 'asi', - 'sf' => 'eur', - 'tp' => 'oce', // .tp TLD - 'yu' => 'eur', // .yu TLD - 'zr' => 'afr', + // transitionally reserved + 'an' => 'amc', // former Netherlands Antilles + 'bu' => 'asi', + 'cs' => 'eur', // former Serbia and Montenegro + 'nt' => 'asi', + 'sf' => 'eur', + 'tp' => 'oce', // .tp TLD + 'yu' => 'eur', // .yu TLD + 'zr' => 'afr', - // MaxMind GeoIP specific - 'a1' => 'unk', - 'a2' => 'unk', - 'ap' => 'asi', - 'o1' => 'unk', + // MaxMind GeoIP specific + 'a1' => 'unk', + 'a2' => 'unk', + 'ap' => 'asi', + 'o1' => 'unk', - // Catalonia (Spain) - 'cat' => 'eur', - ); + // Catalonia (Spain) + 'cat' => 'eur', + ); } -if(!isset($GLOBALS['Piwik_ContinentList'])) -{ - // Primary reference: ISO 3166-1 alpha-2 - $GLOBALS['Piwik_ContinentList'] = array( - 'unk', // unknown - 'amn', // North America - 'amc', // Central America - 'ams', // South America - 'eur', // Europe - 'afr', // Africa - 'asi', // Asia - 'oce', // Oceania - 'ant', // Antarctica - ); +if (!isset($GLOBALS['Piwik_ContinentList'])) { + // Primary reference: ISO 3166-1 alpha-2 + $GLOBALS['Piwik_ContinentList'] = array( + 'unk', // unknown + 'amn', // North America + 'amc', // Central America + 'ams', // South America + 'eur', // Europe + 'afr', // Africa + 'asi', // Asia + 'oce', // Oceania + 'ant', // Antarctica + ); } diff --git a/core/DataFiles/Currencies.php b/core/DataFiles/Currencies.php index 7cdbd0a65f..b381c6c188 100644 --- a/core/DataFiles/Currencies.php +++ b/core/DataFiles/Currencies.php @@ -15,177 +15,176 @@ * @see http://en.wikipedia.org/wiki/List_of_circulating_currencies * @see http://www.iso.org/iso/currency_codes_list-1.html */ -if(!isset($GLOBALS['Piwik_CurrencyList'])) -{ - $GLOBALS['Piwik_CurrencyList'] = array( - // 'ISO-4217 CODE' => array('currency symbol', 'description'), +if (!isset($GLOBALS['Piwik_CurrencyList'])) { + $GLOBALS['Piwik_CurrencyList'] = array( + // 'ISO-4217 CODE' => array('currency symbol', 'description'), - // Top 5 by global trading volume - 'USD' => array('$', 'US dollar'), - 'EUR' => array('€', 'Euro'), - 'JPY' => array('¥', 'Japanese yen'), - 'GBP' => array('£', 'British pound'), - 'CHF' => array('Fr', 'Swiss franc'), + // Top 5 by global trading volume + 'USD' => array('$', 'US dollar'), + 'EUR' => array('€', 'Euro'), + 'JPY' => array('¥', 'Japanese yen'), + 'GBP' => array('£', 'British pound'), + 'CHF' => array('Fr', 'Swiss franc'), - 'AFN' => array('؋', 'Afghan afghani'), - 'ALL' => array('L', 'Albanian lek'), - 'DZD' => array('د.ج', 'Algerian dinar'), - 'AOA' => array('Kz', 'Angolan kwanza'), - 'ARS' => array('$', 'Argentine peso'), - 'AMD' => array('դր.', 'Armenian dram'), - 'AWG' => array('ƒ', 'Aruban florin'), - 'AUD' => array('$', 'Australian dollar'), - 'AZN' => array('m', 'Azerbaijani manat'), - 'BSD' => array('$', 'Bahamian dollar'), - 'BHD' => array('.د.ب', 'Bahraini dinar'), - 'BDT' => array('৳', 'Bangladeshi taka'), - 'BBD' => array('$', 'Barbadian dollar'), - 'BYR' => array('Br', 'Belarusian ruble'), - 'BZD' => array('$', 'Belize dollar'), - 'BMD' => array('$', 'Bermudian dollar'), - 'BTN' => array('Nu.', 'Bhutanese ngultrum'), - 'BOB' => array('Bs.', 'Bolivian boliviano'), - 'BAM' => array('KM', 'Bosnia Herzegovina mark'), - 'BWP' => array('P', 'Botswana pula'), - 'BRL' => array('R$', 'Brazilian real'), + 'AFN' => array('؋', 'Afghan afghani'), + 'ALL' => array('L', 'Albanian lek'), + 'DZD' => array('د.ج', 'Algerian dinar'), + 'AOA' => array('Kz', 'Angolan kwanza'), + 'ARS' => array('$', 'Argentine peso'), + 'AMD' => array('դր.', 'Armenian dram'), + 'AWG' => array('ƒ', 'Aruban florin'), + 'AUD' => array('$', 'Australian dollar'), + 'AZN' => array('m', 'Azerbaijani manat'), + 'BSD' => array('$', 'Bahamian dollar'), + 'BHD' => array('.د.ب', 'Bahraini dinar'), + 'BDT' => array('৳', 'Bangladeshi taka'), + 'BBD' => array('$', 'Barbadian dollar'), + 'BYR' => array('Br', 'Belarusian ruble'), + 'BZD' => array('$', 'Belize dollar'), + 'BMD' => array('$', 'Bermudian dollar'), + 'BTN' => array('Nu.', 'Bhutanese ngultrum'), + 'BOB' => array('Bs.', 'Bolivian boliviano'), + 'BAM' => array('KM', 'Bosnia Herzegovina mark'), + 'BWP' => array('P', 'Botswana pula'), + 'BRL' => array('R$', 'Brazilian real'), // 'GBP' => array('£', 'British pound'), - 'BND' => array('$', 'Brunei dollar'), - 'BGN' => array('лв', 'Bulgarian lev'), - 'BIF' => array('Fr', 'Burundian franc'), - 'KHR' => array('៛', 'Cambodian riel'), - 'CAD' => array('$', 'Canadian dollar'), - 'CVE' => array('$', 'Cape Verdean escudo'), - 'KYD' => array('$', 'Cayman Islands dollar'), - 'XAF' => array('Fr', 'Central African CFA franc'), - 'CLP' => array('$', 'Chilean peso'), - 'CNY' => array('元', 'Chinese yuan'), - 'COP' => array('$', 'Colombian peso'), - 'KMF' => array('Fr', 'Comorian franc'), - 'CDF' => array('Fr', 'Congolese franc'), - 'CRC' => array('₡', 'Costa Rican colón'), - 'HRK' => array('kn', 'Croatian kuna'), - 'XPF' => array('F', 'CFP franc'), - 'CUC' => array('$', 'Cuban convertible peso'), - 'CUP' => array('$', 'Cuban peso'), - 'CMG' => array('ƒ', 'Curaçao and Sint Maarten guilder'), - 'CZK' => array('Kč', 'Czech koruna'), - 'DKK' => array('kr', 'Danish krone'), - 'DJF' => array('Fr', 'Djiboutian franc'), - 'DOP' => array('$', 'Dominican peso'), - 'XCD' => array('$', 'East Caribbean dollar'), - 'EGP' => array('ج.م', 'Egyptian pound'), - 'ERN' => array('Nfk', 'Eritrean nakfa'), - 'EEK' => array('kr', 'Estonian kroon'), - 'ETB' => array('Br', 'Ethiopian birr'), + 'BND' => array('$', 'Brunei dollar'), + 'BGN' => array('лв', 'Bulgarian lev'), + 'BIF' => array('Fr', 'Burundian franc'), + 'KHR' => array('៛', 'Cambodian riel'), + 'CAD' => array('$', 'Canadian dollar'), + 'CVE' => array('$', 'Cape Verdean escudo'), + 'KYD' => array('$', 'Cayman Islands dollar'), + 'XAF' => array('Fr', 'Central African CFA franc'), + 'CLP' => array('$', 'Chilean peso'), + 'CNY' => array('元', 'Chinese yuan'), + 'COP' => array('$', 'Colombian peso'), + 'KMF' => array('Fr', 'Comorian franc'), + 'CDF' => array('Fr', 'Congolese franc'), + 'CRC' => array('₡', 'Costa Rican colón'), + 'HRK' => array('kn', 'Croatian kuna'), + 'XPF' => array('F', 'CFP franc'), + 'CUC' => array('$', 'Cuban convertible peso'), + 'CUP' => array('$', 'Cuban peso'), + 'CMG' => array('ƒ', 'Curaçao and Sint Maarten guilder'), + 'CZK' => array('Kč', 'Czech koruna'), + 'DKK' => array('kr', 'Danish krone'), + 'DJF' => array('Fr', 'Djiboutian franc'), + 'DOP' => array('$', 'Dominican peso'), + 'XCD' => array('$', 'East Caribbean dollar'), + 'EGP' => array('ج.م', 'Egyptian pound'), + 'ERN' => array('Nfk', 'Eritrean nakfa'), + 'EEK' => array('kr', 'Estonian kroon'), + 'ETB' => array('Br', 'Ethiopian birr'), // 'EUR' => array('€', 'Euro'), - 'FKP' => array('£', 'Falkland Islands pound'), - 'FJD' => array('$', 'Fijian dollar'), - 'GMD' => array('D', 'Gambian dalasi'), - 'GEL' => array('ლ', 'Georgian lari'), - 'GHS' => array('₵', 'Ghanaian cedi'), - 'GIP' => array('£', 'Gibraltar pound'), - 'GTQ' => array('Q', 'Guatemalan quetzal'), - 'GNF' => array('Fr', 'Guinean franc'), - 'GYD' => array('$', 'Guyanese dollar'), - 'HTG' => array('G', 'Haitian gourde'), - 'HNL' => array('L', 'Honduran lempira'), - 'HKD' => array('$', 'Hong Kong dollar'), - 'HUF' => array('Ft', 'Hungarian forint'), - 'ISK' => array('kr', 'Icelandic króna'), - 'INR' => array('₹', 'Indian rupee'), - 'IDR' => array('Rp', 'Indonesian rupiah'), - 'IRR' => array('﷼', 'Iranian rial'), - 'IQD' => array('ع.د', 'Iraqi dinar'), - 'ILS' => array('₪', 'Israeli new shekel'), - 'JMD' => array('$', 'Jamaican dollar'), + 'FKP' => array('£', 'Falkland Islands pound'), + 'FJD' => array('$', 'Fijian dollar'), + 'GMD' => array('D', 'Gambian dalasi'), + 'GEL' => array('ლ', 'Georgian lari'), + 'GHS' => array('₵', 'Ghanaian cedi'), + 'GIP' => array('£', 'Gibraltar pound'), + 'GTQ' => array('Q', 'Guatemalan quetzal'), + 'GNF' => array('Fr', 'Guinean franc'), + 'GYD' => array('$', 'Guyanese dollar'), + 'HTG' => array('G', 'Haitian gourde'), + 'HNL' => array('L', 'Honduran lempira'), + 'HKD' => array('$', 'Hong Kong dollar'), + 'HUF' => array('Ft', 'Hungarian forint'), + 'ISK' => array('kr', 'Icelandic króna'), + 'INR' => array('₹', 'Indian rupee'), + 'IDR' => array('Rp', 'Indonesian rupiah'), + 'IRR' => array('﷼', 'Iranian rial'), + 'IQD' => array('ع.د', 'Iraqi dinar'), + 'ILS' => array('₪', 'Israeli new shekel'), + 'JMD' => array('$', 'Jamaican dollar'), // 'JPY' => array('¥', 'Japanese yen'), - 'JOD' => array('د.ا', 'Jordanian dinar'), - 'KZT' => array('₸', 'Kazakhstani tenge'), - 'KES' => array('Sh', 'Kenyan shilling'), - 'KWD' => array('د.ك', 'Kuwaiti dinar'), - 'KGS' => array('лв', 'Kyrgyzstani som'), - 'LAK' => array('₭', 'Lao kip'), - 'LVL' => array('Ls', 'Latvian lats'), - 'LBP' => array('ل.ل', 'Lebanese pound'), - 'LSL' => array('L', 'Lesotho loti'), - 'LRD' => array('$', 'Liberian dollar'), - 'LYD' => array('ل.د', 'Libyan dinar'), - 'LTL' => array('Lt', 'Lithuanian litas'), - 'MOP' => array('P', 'Macanese pataca'), - 'MKD' => array('ден', 'Macedonian denar'), - 'MGA' => array('Ar', 'Malagasy ariary'), - 'MWK' => array('MK', 'Malawian kwacha'), - 'MYR' => array('RM', 'Malaysian ringgit'), - 'MVR' => array('ރ.', 'Maldivian rufiyaa'), - 'MRO' => array('UM', 'Mauritanian ouguiya'), - 'MUR' => array('₨', 'Mauritian rupee'), - 'MXN' => array('$', 'Mexican peso'), - 'MDL' => array('L', 'Moldovan leu'), - 'MNT' => array('₮', 'Mongolian tögrög'), - 'MAD' => array('د.م.', 'Moroccan dirham'), - 'MZN' => array('MTn', 'Mozambican metical'), - 'MMK' => array('K', 'Myanma kyat'), - 'NAD' => array('$', 'Namibian dollar'), - 'NPR' => array('₨', 'Nepalese rupee'), - 'ANG' => array('ƒ', 'Netherlands Antillean guilder'), - 'TWD' => array('$', 'New Taiwan dollar'), - 'NZD' => array('$', 'New Zealand dollar'), - 'NIO' => array('C$', 'Nicaraguan córdoba'), - 'NGN' => array('₦', 'Nigerian naira'), - 'KPW' => array('₩', 'North Korean won'), - 'NOK' => array('kr', 'Norwegian krone'), - 'OMR' => array('ر.ع.', 'Omani rial'), - 'PKR' => array('₨', 'Pakistani rupee'), - 'PAB' => array('B/.', 'Panamanian balboa'), - 'PGK' => array('K', 'Papua New Guinean kina'), - 'PYG' => array('₲', 'Paraguayan guaraní'), - 'PEN' => array('S/.', 'Peruvian nuevo sol'), - 'PHP' => array('₱', 'Philippine peso'), - 'PLN' => array('zł', 'Polish złoty'), - 'QAR' => array('ر.ق', 'Qatari riyal'), - 'RON' => array('L', 'Romanian leu'), - 'RUB' => array('руб.', 'Russian ruble'), - 'RWF' => array('Fr', 'Rwandan franc'), - 'SHP' => array('£', 'Saint Helena pound'), - 'SVC' => array('₡', 'Salvadoran colón'), - 'WST' => array('T', 'Samoan tala'), - 'STD' => array('Db', 'São Tomé and Príncipe dobra'), - 'SAR' => array('ر.س', 'Saudi riyal'), - 'RSD' => array('дин. or din.', 'Serbian dinar'), - 'SCR' => array('₨', 'Seychellois rupee'), - 'SLL' => array('Le', 'Sierra Leonean leone'), - 'SGD' => array('$', 'Singapore dollar'), - 'SBD' => array('$', 'Solomon Islands dollar'), - 'SOS' => array('Sh', 'Somali shilling'), - 'ZAR' => array('R', 'South African rand'), - 'KRW' => array('₩', 'South Korean won'), - 'LKR' => array('Rs', 'Sri Lankan rupee'), - 'SDG' => array('جنيه سوداني', 'Sudanese pound'), - 'SRD' => array('$', 'Surinamese dollar'), - 'SZL' => array('L', 'Swazi lilangeni'), - 'SEK' => array('kr', 'Swedish krona'), + 'JOD' => array('د.ا', 'Jordanian dinar'), + 'KZT' => array('₸', 'Kazakhstani tenge'), + 'KES' => array('Sh', 'Kenyan shilling'), + 'KWD' => array('د.ك', 'Kuwaiti dinar'), + 'KGS' => array('лв', 'Kyrgyzstani som'), + 'LAK' => array('₭', 'Lao kip'), + 'LVL' => array('Ls', 'Latvian lats'), + 'LBP' => array('ل.ل', 'Lebanese pound'), + 'LSL' => array('L', 'Lesotho loti'), + 'LRD' => array('$', 'Liberian dollar'), + 'LYD' => array('ل.د', 'Libyan dinar'), + 'LTL' => array('Lt', 'Lithuanian litas'), + 'MOP' => array('P', 'Macanese pataca'), + 'MKD' => array('ден', 'Macedonian denar'), + 'MGA' => array('Ar', 'Malagasy ariary'), + 'MWK' => array('MK', 'Malawian kwacha'), + 'MYR' => array('RM', 'Malaysian ringgit'), + 'MVR' => array('ރ.', 'Maldivian rufiyaa'), + 'MRO' => array('UM', 'Mauritanian ouguiya'), + 'MUR' => array('₨', 'Mauritian rupee'), + 'MXN' => array('$', 'Mexican peso'), + 'MDL' => array('L', 'Moldovan leu'), + 'MNT' => array('₮', 'Mongolian tögrög'), + 'MAD' => array('د.م.', 'Moroccan dirham'), + 'MZN' => array('MTn', 'Mozambican metical'), + 'MMK' => array('K', 'Myanma kyat'), + 'NAD' => array('$', 'Namibian dollar'), + 'NPR' => array('₨', 'Nepalese rupee'), + 'ANG' => array('ƒ', 'Netherlands Antillean guilder'), + 'TWD' => array('$', 'New Taiwan dollar'), + 'NZD' => array('$', 'New Zealand dollar'), + 'NIO' => array('C$', 'Nicaraguan córdoba'), + 'NGN' => array('₦', 'Nigerian naira'), + 'KPW' => array('₩', 'North Korean won'), + 'NOK' => array('kr', 'Norwegian krone'), + 'OMR' => array('ر.ع.', 'Omani rial'), + 'PKR' => array('₨', 'Pakistani rupee'), + 'PAB' => array('B/.', 'Panamanian balboa'), + 'PGK' => array('K', 'Papua New Guinean kina'), + 'PYG' => array('₲', 'Paraguayan guaraní'), + 'PEN' => array('S/.', 'Peruvian nuevo sol'), + 'PHP' => array('₱', 'Philippine peso'), + 'PLN' => array('zł', 'Polish złoty'), + 'QAR' => array('ر.ق', 'Qatari riyal'), + 'RON' => array('L', 'Romanian leu'), + 'RUB' => array('руб.', 'Russian ruble'), + 'RWF' => array('Fr', 'Rwandan franc'), + 'SHP' => array('£', 'Saint Helena pound'), + 'SVC' => array('₡', 'Salvadoran colón'), + 'WST' => array('T', 'Samoan tala'), + 'STD' => array('Db', 'São Tomé and Príncipe dobra'), + 'SAR' => array('ر.س', 'Saudi riyal'), + 'RSD' => array('дин. or din.', 'Serbian dinar'), + 'SCR' => array('₨', 'Seychellois rupee'), + 'SLL' => array('Le', 'Sierra Leonean leone'), + 'SGD' => array('$', 'Singapore dollar'), + 'SBD' => array('$', 'Solomon Islands dollar'), + 'SOS' => array('Sh', 'Somali shilling'), + 'ZAR' => array('R', 'South African rand'), + 'KRW' => array('₩', 'South Korean won'), + 'LKR' => array('Rs', 'Sri Lankan rupee'), + 'SDG' => array('جنيه سوداني', 'Sudanese pound'), + 'SRD' => array('$', 'Surinamese dollar'), + 'SZL' => array('L', 'Swazi lilangeni'), + 'SEK' => array('kr', 'Swedish krona'), // 'CHF' => array('Fr', 'Swiss franc'), - 'SYP' => array('ل.س', 'Syrian pound'), - 'TJS' => array('ЅМ', 'Tajikistani somoni'), - 'TZS' => array('Sh', 'Tanzanian shilling'), - 'THB' => array('฿', 'Thai baht'), - 'TOP' => array('T$', 'Tongan paʻanga'), - 'TTD' => array('$', 'Trinidad and Tobago dollar'), - 'TND' => array('د.ت', 'Tunisian dinar'), - 'TRY' => array('TL', 'Turkish lira'), - 'TMM' => array('m', 'Turkmenistani manat'), - 'UGX' => array('Sh', 'Ugandan shilling'), - 'UAH' => array('₴', 'Ukrainian hryvnia'), - 'AED' => array('د.إ', 'United Arab Emirates dirham'), + 'SYP' => array('ل.س', 'Syrian pound'), + 'TJS' => array('ЅМ', 'Tajikistani somoni'), + 'TZS' => array('Sh', 'Tanzanian shilling'), + 'THB' => array('฿', 'Thai baht'), + 'TOP' => array('T$', 'Tongan paʻanga'), + 'TTD' => array('$', 'Trinidad and Tobago dollar'), + 'TND' => array('د.ت', 'Tunisian dinar'), + 'TRY' => array('TL', 'Turkish lira'), + 'TMM' => array('m', 'Turkmenistani manat'), + 'UGX' => array('Sh', 'Ugandan shilling'), + 'UAH' => array('₴', 'Ukrainian hryvnia'), + 'AED' => array('د.إ', 'United Arab Emirates dirham'), // 'USD' => array('$', 'United States dollar'), - 'UYU' => array('$', 'Uruguayan peso'), - 'UZS' => array('лв', 'Uzbekistani som'), - 'VUV' => array('Vt', 'Vanuatu vatu'), - 'VEF' => array('Bs F', 'Venezuelan bolívar'), - 'VND' => array('₫', 'Vietnamese đồng'), - 'XOF' => array('Fr', 'West African CFA franc'), - 'YER' => array('﷼', 'Yemeni rial'), - 'ZMK' => array('ZK', 'Zambian kwacha'), - 'ZWL' => array('$', 'Zimbabwean dollar'), - ); + 'UYU' => array('$', 'Uruguayan peso'), + 'UZS' => array('лв', 'Uzbekistani som'), + 'VUV' => array('Vt', 'Vanuatu vatu'), + 'VEF' => array('Bs F', 'Venezuelan bolívar'), + 'VND' => array('₫', 'Vietnamese đồng'), + 'XOF' => array('Fr', 'West African CFA franc'), + 'YER' => array('﷼', 'Yemeni rial'), + 'ZMK' => array('ZK', 'Zambian kwacha'), + 'ZWL' => array('$', 'Zimbabwean dollar'), + ); } diff --git a/core/DataFiles/LanguageToCountry.php b/core/DataFiles/LanguageToCountry.php index 64f63b1040..4ad1d0ef0a 100644 --- a/core/DataFiles/LanguageToCountry.php +++ b/core/DataFiles/LanguageToCountry.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package DataFiles */ @@ -19,48 +19,47 @@ * * If you want to add a new entry, please email us at hello at piwik.org */ -if(!isset($GLOBALS['Piwik_LanguageToCountry'])) -{ - $GLOBALS['Piwik_LanguageToCountry'] = array( - 'bg' => 'bg', // Bulgarian => Bulgaria - 'ca' => 'es', // Catalan => Spain - 'cs' => 'cz', // Czech => Czech Republic - 'da' => 'dk', // Danish => Denmark - 'de' => 'de', // German => Germany - 'el' => 'gr', // Greek => Greece - 'es' => 'es', // Spanish => Spain - 'et' => 'ee', // Estonian => Estonia - 'fa' => 'ir', // Farsi => Iran - 'fi' => 'fi', // Finnish => Finland - 'fr' => 'fr', // French => France - 'he' => 'il', // Hebrew => Israel - 'hr' => 'hr', // Croatian => Croatia - 'hu' => 'hu', // Hungarian => Hungary - 'id' => 'id', // Indonesian => Indonesia - 'is' => 'is', // Icelandic => Iceland - 'it' => 'it', // Italian => Italy - 'ja' => 'jp', // Japanese => Japan - 'ko' => 'kr', // Korean => South Korea - 'lt' => 'lt', // Lithuanian => Lithuania - 'lv' => 'lv', // Latvian => Latvia - 'mk' => 'mk', // Macedonian => Macedonia - 'ms' => 'my', // Malay => Malaysia - 'nb' => 'no', // Bokmål => Norway - 'nl' => 'nl', // Dutch => Netherlands - 'nn' => 'no', // Nynorsk => Norway - 'no' => 'no', // Norwegian => Norway - 'pl' => 'pl', // Polish => Poland - 'pt' => 'pt', // Portugese => Portugal - 'ro' => 'ro', // Romanian => Romania - 'ru' => 'ru', // Russian => Russia - 'sk' => 'sk', // Slovak => Slovakia - 'sl' => 'si', // Slovene => Slovenia - 'sq' => 'al', // Albanian => Albania - 'sr' => 'rs', // Serbian => Serbia - 'sv' => 'se', // Swedish => Sweden - 'th' => 'th', // Thai => Thailand - 'bo' => 'ti', // Tibetan => Tibet - 'tr' => 'tr', // Turkish => Turkey - 'uk' => 'ua', // Ukrainian => Ukraine - ); +if (!isset($GLOBALS['Piwik_LanguageToCountry'])) { + $GLOBALS['Piwik_LanguageToCountry'] = array( + 'bg' => 'bg', // Bulgarian => Bulgaria + 'ca' => 'es', // Catalan => Spain + 'cs' => 'cz', // Czech => Czech Republic + 'da' => 'dk', // Danish => Denmark + 'de' => 'de', // German => Germany + 'el' => 'gr', // Greek => Greece + 'es' => 'es', // Spanish => Spain + 'et' => 'ee', // Estonian => Estonia + 'fa' => 'ir', // Farsi => Iran + 'fi' => 'fi', // Finnish => Finland + 'fr' => 'fr', // French => France + 'he' => 'il', // Hebrew => Israel + 'hr' => 'hr', // Croatian => Croatia + 'hu' => 'hu', // Hungarian => Hungary + 'id' => 'id', // Indonesian => Indonesia + 'is' => 'is', // Icelandic => Iceland + 'it' => 'it', // Italian => Italy + 'ja' => 'jp', // Japanese => Japan + 'ko' => 'kr', // Korean => South Korea + 'lt' => 'lt', // Lithuanian => Lithuania + 'lv' => 'lv', // Latvian => Latvia + 'mk' => 'mk', // Macedonian => Macedonia + 'ms' => 'my', // Malay => Malaysia + 'nb' => 'no', // Bokmål => Norway + 'nl' => 'nl', // Dutch => Netherlands + 'nn' => 'no', // Nynorsk => Norway + 'no' => 'no', // Norwegian => Norway + 'pl' => 'pl', // Polish => Poland + 'pt' => 'pt', // Portugese => Portugal + 'ro' => 'ro', // Romanian => Romania + 'ru' => 'ru', // Russian => Russia + 'sk' => 'sk', // Slovak => Slovakia + 'sl' => 'si', // Slovene => Slovenia + 'sq' => 'al', // Albanian => Albania + 'sr' => 'rs', // Serbian => Serbia + 'sv' => 'se', // Swedish => Sweden + 'th' => 'th', // Thai => Thailand + 'bo' => 'ti', // Tibetan => Tibet + 'tr' => 'tr', // Turkish => Turkey + 'uk' => 'ua', // Ukrainian => Ukraine + ); } diff --git a/core/DataFiles/Languages.php b/core/DataFiles/Languages.php index fb3c8c0ef8..0adf560854 100644 --- a/core/DataFiles/Languages.php +++ b/core/DataFiles/Languages.php @@ -12,195 +12,194 @@ /* * Language database */ -if(!isset($GLOBALS['Piwik_LanguageList'])) -{ - // Reference: ISO 639-1 alpha-2 - $GLOBALS['Piwik_LanguageList'] = array( - 'aa' => array('Afar'), - 'ab' => array('Abkhazian'), - 'ae' => array('Avestan'), - 'af' => array('Afrikaans'), - 'ak' => array('Akan'), - 'am' => array('Amharic'), - 'an' => array('Aragonese'), - 'ar' => array('Arabic'), - 'as' => array('Assamese'), - 'av' => array('Avaric'), - 'ay' => array('Aymara'), - 'az' => array('Azerbaijani'), - 'ba' => array('Bashkir'), - 'be' => array('Belarusian'), - 'bg' => array('Bulgarian'), - 'bh' => array('Bihari'), // 'Bihari languages' - 'bi' => array('Bislama'), - 'bm' => array('Bambara'), - 'bn' => array('Bengali'), - 'bo' => array('Tibetan'), - 'br' => array('Breton'), - 'bs' => array('Bosnian'), - 'ca' => array('Catalan', 'Valencian'), - 'ce' => array('Chechen'), - 'ch' => array('Chamorro'), - 'co' => array('Corsican'), - 'cr' => array('Cree'), - 'cs' => array('Czech'), - 'cu' => array('Church Slavic', 'Old Slavonic', 'Church Slavonic', 'Old Bulgarian', 'Old Church Slavonic'), - 'cv' => array('Chuvash'), - 'cy' => array('Welsh'), - 'da' => array('Danish'), - 'de' => array('German'), - 'dv' => array('Divehi', 'Dhivehi', 'Maldivian'), - 'dz' => array('Dzongkha'), - 'ee' => array('Ewe'), - 'el' => array('Greek', 'Modern Greek', 'Hellenic'), // Greek, Modern (1453-) - 'en' => array('English'), - 'eo' => array('Esperanto'), - 'es' => array('Spanish', 'Castilian'), - 'et' => array('Estonian'), - 'eu' => array('Basque'), - 'fa' => array('Persian'), - 'ff' => array('Fulah'), - 'fi' => array('Finnish'), - 'fj' => array('Fijian'), - 'fo' => array('Faroese'), - 'fr' => array('French'), - 'fy' => array('Western Frisian'), - 'ga' => array('Irish'), - 'gd' => array('Gaelic', 'Scottish Gaelic'), - 'gl' => array('Galician'), - 'gn' => array('Guarani'), - 'gu' => array('Gujarati'), - 'gv' => array('Manx'), - 'ha' => array('Hausa'), - 'he' => array('Hebrew'), - 'hi' => array('Hindi'), - 'ho' => array('Hiri Motu'), - 'hr' => array('Croatian'), - 'ht' => array('Haitian', 'Haitian Creole'), - 'hu' => array('Hungarian'), - 'hy' => array('Armenian'), - 'hz' => array('Herero'), - 'ia' => array('Interlingua'), // 'Interlingua (International Auxiliary Language Association)' - 'id' => array('Indonesian'), - 'ie' => array('Interlingue', 'Occidental'), - 'ig' => array('Igbo'), - 'ii' => array('Sichuan Yi', 'Nuosu'), - 'ik' => array('Inupiaq'), - 'io' => array('Ido'), - 'is' => array('Icelandic'), - 'it' => array('Italian'), - 'iu' => array('Inuktitut'), - 'ja' => array('Japanese'), - 'jv' => array('Javanese'), - 'ka' => array('Georgian'), - 'kg' => array('Kongo'), - 'ki' => array('Kikuyu', 'Gikuyu'), - 'kj' => array('Kuanyama', 'Kwanyama'), - 'kk' => array('Kazakh'), - 'kl' => array('Kalaallisut', 'Greenlandic'), - 'km' => array('Central Khmer'), - 'kn' => array('Kannada'), - 'ko' => array('Korean'), - 'kr' => array('Kanuri'), - 'ks' => array('Kashmiri'), - 'ku' => array('Kurdish'), - 'kv' => array('Komi'), - 'kw' => array('Cornish'), - 'ky' => array('Kirghiz', 'Kyrgyz'), - 'la' => array('Latin'), - 'lb' => array('Luxembourgish', 'Letzeburgesch'), - 'lg' => array('Ganda'), - 'li' => array('Limburgan', 'Limburger', 'Limburgish'), - 'ln' => array('Lingala'), - 'lo' => array('Lao'), - 'lt' => array('Lithuanian'), - 'lu' => array('Luba-Katanga'), - 'lv' => array('Latvian'), - 'mg' => array('Malagasy'), - 'mh' => array('Marshallese'), - 'mi' => array('Maori'), - 'mk' => array('Macedonian'), - 'ml' => array('Malayalam'), - 'mn' => array('Mongolian'), +if (!isset($GLOBALS['Piwik_LanguageList'])) { + // Reference: ISO 639-1 alpha-2 + $GLOBALS['Piwik_LanguageList'] = array( + 'aa' => array('Afar'), + 'ab' => array('Abkhazian'), + 'ae' => array('Avestan'), + 'af' => array('Afrikaans'), + 'ak' => array('Akan'), + 'am' => array('Amharic'), + 'an' => array('Aragonese'), + 'ar' => array('Arabic'), + 'as' => array('Assamese'), + 'av' => array('Avaric'), + 'ay' => array('Aymara'), + 'az' => array('Azerbaijani'), + 'ba' => array('Bashkir'), + 'be' => array('Belarusian'), + 'bg' => array('Bulgarian'), + 'bh' => array('Bihari'), // 'Bihari languages' + 'bi' => array('Bislama'), + 'bm' => array('Bambara'), + 'bn' => array('Bengali'), + 'bo' => array('Tibetan'), + 'br' => array('Breton'), + 'bs' => array('Bosnian'), + 'ca' => array('Catalan', 'Valencian'), + 'ce' => array('Chechen'), + 'ch' => array('Chamorro'), + 'co' => array('Corsican'), + 'cr' => array('Cree'), + 'cs' => array('Czech'), + 'cu' => array('Church Slavic', 'Old Slavonic', 'Church Slavonic', 'Old Bulgarian', 'Old Church Slavonic'), + 'cv' => array('Chuvash'), + 'cy' => array('Welsh'), + 'da' => array('Danish'), + 'de' => array('German'), + 'dv' => array('Divehi', 'Dhivehi', 'Maldivian'), + 'dz' => array('Dzongkha'), + 'ee' => array('Ewe'), + 'el' => array('Greek', 'Modern Greek', 'Hellenic'), // Greek, Modern (1453-) + 'en' => array('English'), + 'eo' => array('Esperanto'), + 'es' => array('Spanish', 'Castilian'), + 'et' => array('Estonian'), + 'eu' => array('Basque'), + 'fa' => array('Persian'), + 'ff' => array('Fulah'), + 'fi' => array('Finnish'), + 'fj' => array('Fijian'), + 'fo' => array('Faroese'), + 'fr' => array('French'), + 'fy' => array('Western Frisian'), + 'ga' => array('Irish'), + 'gd' => array('Gaelic', 'Scottish Gaelic'), + 'gl' => array('Galician'), + 'gn' => array('Guarani'), + 'gu' => array('Gujarati'), + 'gv' => array('Manx'), + 'ha' => array('Hausa'), + 'he' => array('Hebrew'), + 'hi' => array('Hindi'), + 'ho' => array('Hiri Motu'), + 'hr' => array('Croatian'), + 'ht' => array('Haitian', 'Haitian Creole'), + 'hu' => array('Hungarian'), + 'hy' => array('Armenian'), + 'hz' => array('Herero'), + 'ia' => array('Interlingua'), // 'Interlingua (International Auxiliary Language Association)' + 'id' => array('Indonesian'), + 'ie' => array('Interlingue', 'Occidental'), + 'ig' => array('Igbo'), + 'ii' => array('Sichuan Yi', 'Nuosu'), + 'ik' => array('Inupiaq'), + 'io' => array('Ido'), + 'is' => array('Icelandic'), + 'it' => array('Italian'), + 'iu' => array('Inuktitut'), + 'ja' => array('Japanese'), + 'jv' => array('Javanese'), + 'ka' => array('Georgian'), + 'kg' => array('Kongo'), + 'ki' => array('Kikuyu', 'Gikuyu'), + 'kj' => array('Kuanyama', 'Kwanyama'), + 'kk' => array('Kazakh'), + 'kl' => array('Kalaallisut', 'Greenlandic'), + 'km' => array('Central Khmer'), + 'kn' => array('Kannada'), + 'ko' => array('Korean'), + 'kr' => array('Kanuri'), + 'ks' => array('Kashmiri'), + 'ku' => array('Kurdish'), + 'kv' => array('Komi'), + 'kw' => array('Cornish'), + 'ky' => array('Kirghiz', 'Kyrgyz'), + 'la' => array('Latin'), + 'lb' => array('Luxembourgish', 'Letzeburgesch'), + 'lg' => array('Ganda'), + 'li' => array('Limburgan', 'Limburger', 'Limburgish'), + 'ln' => array('Lingala'), + 'lo' => array('Lao'), + 'lt' => array('Lithuanian'), + 'lu' => array('Luba-Katanga'), + 'lv' => array('Latvian'), + 'mg' => array('Malagasy'), + 'mh' => array('Marshallese'), + 'mi' => array('Maori'), + 'mk' => array('Macedonian'), + 'ml' => array('Malayalam'), + 'mn' => array('Mongolian'), // 'mo' => array('Moldavian'), // deprecated - 'mr' => array('Marathi'), - 'ms' => array('Malay'), - 'mt' => array('Maltese'), - 'my' => array('Burmese'), - 'na' => array('Nauru'), - 'nb' => array('Norwegian Bokmål'), - 'nd' => array('North Ndebele'), - 'ne' => array('Nepali'), - 'ng' => array('Ndonga'), - 'nl' => array('Dutch', 'Flemish'), - 'nn' => array('Norwegian Nynorsk'), - 'no' => array('Norwegian'), - 'nr' => array('South Ndebele'), - 'nv' => array('Navajo', 'Navaho'), - 'ny' => array('Chichewa', 'Chewa', 'Nyanja'), - 'oc' => array('Occitan', 'Provençal'), // Occitan (post 1500) - 'oj' => array('Ojibwa'), - 'om' => array('Oromo'), - 'or' => array('Oriya'), - 'os' => array('Ossetian', 'Ossetic'), - 'pa' => array('Panjabi', 'Punjabi'), - 'pi' => array('Pali'), - 'pl' => array('Polish'), - 'ps' => array('Pushto', 'Pashto'), - 'pt' => array('Portuguese'), - 'qu' => array('Quechua'), - 'rm' => array('Romansh'), - 'rn' => array('Rundi'), - 'ro' => array('Romanian', 'Moldavian', 'Moldovan'), - 'ru' => array('Russian'), - 'rw' => array('Kinyarwanda'), - 'sa' => array('Sanskrit'), - 'sc' => array('Sardinian'), - 'sd' => array('Sindhi'), - 'se' => array('Northern Sami'), - 'sg' => array('Sango'), + 'mr' => array('Marathi'), + 'ms' => array('Malay'), + 'mt' => array('Maltese'), + 'my' => array('Burmese'), + 'na' => array('Nauru'), + 'nb' => array('Norwegian Bokmål'), + 'nd' => array('North Ndebele'), + 'ne' => array('Nepali'), + 'ng' => array('Ndonga'), + 'nl' => array('Dutch', 'Flemish'), + 'nn' => array('Norwegian Nynorsk'), + 'no' => array('Norwegian'), + 'nr' => array('South Ndebele'), + 'nv' => array('Navajo', 'Navaho'), + 'ny' => array('Chichewa', 'Chewa', 'Nyanja'), + 'oc' => array('Occitan', 'Provençal'), // Occitan (post 1500) + 'oj' => array('Ojibwa'), + 'om' => array('Oromo'), + 'or' => array('Oriya'), + 'os' => array('Ossetian', 'Ossetic'), + 'pa' => array('Panjabi', 'Punjabi'), + 'pi' => array('Pali'), + 'pl' => array('Polish'), + 'ps' => array('Pushto', 'Pashto'), + 'pt' => array('Portuguese'), + 'qu' => array('Quechua'), + 'rm' => array('Romansh'), + 'rn' => array('Rundi'), + 'ro' => array('Romanian', 'Moldavian', 'Moldovan'), + 'ru' => array('Russian'), + 'rw' => array('Kinyarwanda'), + 'sa' => array('Sanskrit'), + 'sc' => array('Sardinian'), + 'sd' => array('Sindhi'), + 'se' => array('Northern Sami'), + 'sg' => array('Sango'), // 'sh' => array('Serbo-Croatian'), // deprecated - 'si' => array('Sinhala', 'Sinhalese'), - 'sk' => array('Slovak'), - 'sl' => array('Slovenian'), - 'sm' => array('Samoan'), - 'sn' => array('Shona'), - 'so' => array('Somali'), - 'sq' => array('Albanian'), - 'sr' => array('Serbian'), - 'ss' => array('Swati'), - 'st' => array('Southern Soth'), - 'su' => array('Sundanese'), - 'sv' => array('Swedish'), - 'sw' => array('Swahili'), - 'ta' => array('Tamil'), - 'te' => array('Telugu'), - 'tg' => array('Tajik'), - 'th' => array('Thai'), - 'ti' => array('Tigrinya'), - 'tk' => array('Turkmen'), - 'tl' => array('Tagalog'), - 'tn' => array('Tswana'), - 'to' => array('Tonga'), // Tonga (Tonga Islands) - 'tr' => array('Turkish'), - 'ts' => array('Tsonga'), - 'tt' => array('Tatar'), - 'tw' => array('Twi'), - 'ty' => array('Tahitian'), - 'ug' => array('Uighur', 'Uyghur'), - 'uk' => array('Ukrainian'), - 'ur' => array('Urdu'), - 'uz' => array('Uzbek'), - 've' => array('Venda'), - 'vi' => array('Vietnamese'), - 'vo' => array('Volapük'), - 'wa' => array('Walloon'), - 'wo' => array('Wolof'), - 'xh' => array('Xhosa'), - 'yi' => array('Yiddish'), - 'yo' => array('Yoruba'), - 'za' => array('Zhuang', 'Chuang'), - 'zh' => array('Chinese'), - 'zu' => array('Zulu'), - ); + 'si' => array('Sinhala', 'Sinhalese'), + 'sk' => array('Slovak'), + 'sl' => array('Slovenian'), + 'sm' => array('Samoan'), + 'sn' => array('Shona'), + 'so' => array('Somali'), + 'sq' => array('Albanian'), + 'sr' => array('Serbian'), + 'ss' => array('Swati'), + 'st' => array('Southern Soth'), + 'su' => array('Sundanese'), + 'sv' => array('Swedish'), + 'sw' => array('Swahili'), + 'ta' => array('Tamil'), + 'te' => array('Telugu'), + 'tg' => array('Tajik'), + 'th' => array('Thai'), + 'ti' => array('Tigrinya'), + 'tk' => array('Turkmen'), + 'tl' => array('Tagalog'), + 'tn' => array('Tswana'), + 'to' => array('Tonga'), // Tonga (Tonga Islands) + 'tr' => array('Turkish'), + 'ts' => array('Tsonga'), + 'tt' => array('Tatar'), + 'tw' => array('Twi'), + 'ty' => array('Tahitian'), + 'ug' => array('Uighur', 'Uyghur'), + 'uk' => array('Ukrainian'), + 'ur' => array('Urdu'), + 'uz' => array('Uzbek'), + 've' => array('Venda'), + 'vi' => array('Vietnamese'), + 'vo' => array('Volapük'), + 'wa' => array('Walloon'), + 'wo' => array('Wolof'), + 'xh' => array('Xhosa'), + 'yi' => array('Yiddish'), + 'yo' => array('Yoruba'), + 'za' => array('Zhuang', 'Chuang'), + 'zh' => array('Chinese'), + 'zu' => array('Zulu'), + ); } diff --git a/core/DataFiles/SearchEngines.php b/core/DataFiles/SearchEngines.php index 300a87017d..67b3bb98b4 100644 --- a/core/DataFiles/SearchEngines.php +++ b/core/DataFiles/SearchEngines.php @@ -26,7 +26,7 @@ * The main search engine URL has to be at the top of the list for the given * search Engine. This serves as the master record so additional URLs * don't have to duplicate all the information, but can override when needed. - * + * * The URL, "example.com", will match "example.com", "m.example.com", * "www.example.com", and "search.example.com". * @@ -48,949 +48,946 @@ * automatically be replaced by the keyword. * * A simple example is: - * 'www.google.com' => array('Google', 'q', 'search?q={k}'), + * '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'), + * 'www.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), 's?wd={k}', 'gb2312'), * * Another example using a regular expression to parse the path for keywords: * 'infospace.com' => array('InfoSpace', array('/dir1\/(pattern)\/dir2/'), '/dir1/{k}/dir2/stuff/'), */ -if(!isset($GLOBALS['Piwik_SearchEngines'] )) -{ - $GLOBALS['Piwik_SearchEngines'] = array( - // 1 - '1.cz' => array('1.cz', 'q', 'index.php?q={k}', 'iso-8859-2'), - - // 123people - 'www.123people.com' => array('123people', array('/s\/([^\/]+)/', 'search_term'), 's/{k}'), - '123people.{}' => array('123people'), - - // 360search - 'so.360.cn' => array('360search', 'q', 's?q={k}', array('UTF-8', 'gb2312')), - 'www.so.com' => array('360search', 'q', 's?q={k}', array('UTF-8', 'gb2312')), - - // Abacho - 'www.abacho.de' => array('Abacho', 'q', 'suche?q={k}'), - 'www.abacho.com' => array('Abacho'), - 'www.abacho.co.uk' => array('Abacho'), - 'www.se.abacho.com' => array('Abacho'), - 'www.tr.abacho.com' => array('Abacho'), - 'www.abacho.at' => array('Abacho'), - 'www.abacho.fr' => array('Abacho'), - 'www.abacho.es' => array('Abacho'), - 'www.abacho.ch' => array('Abacho'), - 'www.abacho.it' => array('Abacho'), - - // ABCsøk - 'abcsok.no' => array('ABCsøk', 'q', '?q={k}'), - 'verden.abcsok.no' => array('ABCsøk'), - - // Acoon - 'www.acoon.de' => array('Acoon', 'begriff', 'cgi-bin/search.exe?begriff={k}'), - - // Alexa - 'alexa.com' => array('Alexa', 'q', 'search?q={k}'), - 'search.toolbars.alexa.com' => array('Alexa'), - - // Alice Adsl - 'rechercher.aliceadsl.fr' => array('Alice Adsl', 'qs', 'google.pl?qs={k}'), - - // Allesklar - 'www.allesklar.de' => array('Allesklar', 'words', '?words={k}'), - 'www.allesklar.at' => array('Allesklar'), - 'www.allesklar.ch' => array('Allesklar'), - - // AllTheWeb - 'www.alltheweb.com' => array('AllTheWeb', 'q', 'search?q={k}'), - - // all.by - 'all.by' => array('All.by', 'query', 'cgi-bin/search.cgi?mode=by&query={k}'), - - // Altavista - 'www.altavista.com' => array('AltaVista', 'q', 'web/results?q={k}'), - 'search.altavista.com' => array('AltaVista'), - 'listings.altavista.com' => array('AltaVista'), - 'altavista.de' => array('AltaVista'), - 'altavista.fr' => array('AltaVista'), - '{}.altavista.com' => array('AltaVista'), - 'be-nl.altavista.com' => array('AltaVista'), - 'be-fr.altavista.com' => array('AltaVista'), - - // Apollo Latvia - 'apollo.lv/portal/search/' => array('Apollo lv', 'q', '?cof=FORID%3A11&q={k}&search_where=www'), - - // APOLLO7 - 'apollo7.de' => array('Apollo7', 'query', 'a7db/index.php?query={k}&de_sharelook=true&de_bing=true&de_witch=true&de_google=true&de_yahoo=true&de_lycos=true'), - - // AOL - 'search.aol.com' => array('AOL', array('query', 'q'), 'aol/search?q={k}'), - 'search.aol.it' => array('AOL'), - 'aolsearch.aol.com' => array('AOL'), - 'www.aolrecherche.aol.fr' => array('AOL'), - 'www.aolrecherches.aol.fr' => array('AOL'), - 'www.aolimages.aol.fr' => array('AOL'), - 'aim.search.aol.com' => array('AOL'), - 'www.recherche.aol.fr' => array('AOL'), - 'recherche.aol.fr' => array('AOL'), - 'find.web.aol.com' => array('AOL'), - 'recherche.aol.ca' => array('AOL'), - 'aolsearch.aol.co.uk' => array('AOL'), - 'search.aol.co.uk' => array('AOL'), - 'aolrecherche.aol.fr' => array('AOL'), - 'sucheaol.aol.de' => array('AOL'), - 'suche.aol.de' => array('AOL'), - 'o2suche.aol.de' => array('AOL'), - 'suche.aolsvc.de' => array('AOL'), - 'aolbusqueda.aol.com.mx' => array('AOL'), - 'alicesuche.aol.de' => array('AOL'), - 'alicesuchet.aol.de' => array('AOL'), - 'suchet2.aol.de' => array('AOL'), - 'search.hp.my.aol.com.au' => array('AOL'), - 'search.hp.my.aol.de' => array('AOL'), - 'search.hp.my.aol.it' => array('AOL'), - 'search-intl.netscape.com' => array('AOL'), - 'de.aolsearch.com' => array('AOL', 'q', 'search?q={k}'), - - // Aport - 'sm.aport.ru' => array('Aport', 'r', 'search?r={k}'), - - // arama - 'arama.com' => array('Arama', 'q', 'search.php3?q={k}'), - - // Arcor - 'www.arcor.de' => array('Arcor', 'Keywords', 'content/searchresult.jsp?Keywords={k}'), - - // Arianna (Libero.it) - 'arianna.libero.it' => array('Arianna', 'query', 'search/abin/integrata.cgi?query={k}'), - 'www.arianna.com' => array('Arianna'), - - // Ask (IAC Search & Media) - 'ask.com' => array('Ask', array('ask', 'q', 'searchfor'), 'web?q={k}'), - 'web.ask.com' => array('Ask'), - 'int.ask.com' => array('Ask'), - 'mws.ask.com' => array('Ask'), - 'images.ask.com' => array('Ask'), - 'images.{}.ask.com' => array('Ask'), - 'ask.reference.com' => array('Ask'), - 'www.askkids.com' => array('Ask'), - 'iwon.ask.com' => array('Ask'), - 'www.ask.co.uk' => array('Ask'), - '{}.ask.com' => array('Ask'), - 'www.qbyrd.com' => array('Ask'), - '{}.qbyrd.com' => array('Ask'), - 'www.search-results.com' => array('Ask'), - 'int.search-results.com' => array('Ask'), - '{}.search-results.com' => array('Ask'), - '{}.search.ask.com' => array('Ask'), - 'avira-int.ask.com' => array('Ask'), - - // Atlas - 'searchatlas.centrum.cz' => array('Atlas', 'q', '?q={k}'), - - // Austronaut - 'www2.austronaut.at' => array('Austronaut', 'q'), - 'www1.austronaut.at' => array('Austronaut'), - - // Babylon (Enhanced by Google) - 'search.babylon.com' => array('Babylon', array('q', '/\/web\/(.*)/'), '?q={k}'), - 'searchassist.babylon.com' => array('Babylon'), - - // Baidu - 'www.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), 's?wd={k}', array('UTF-8', 'gb2312')), - 'www1.baidu.com' => array('Baidu'), - 'zhidao.baidu.com' => array('Baidu'), - 'tieba.baidu.com' => array('Baidu'), - 'news.baidu.com' => array('Baidu'), - 'web.gougou.com' => array('Baidu', 'search', 'search?search={k}'), // uses baidu search - - // Biglobe - 'cgi.search.biglobe.ne.jp' => array('Biglobe', 'q', 'cgi-bin/search-st?q={k}'), - - // Bing - 'bing.com' => array('Bing', array('q', 'Q'), 'search?q={k}'), - '{}.bing.com' => array('Bing'), - 'msnbc.msn.com' => array('Bing'), - 'dizionario.it.msn.com' => array('Bing'), - - // Bing Cache - 'cc.bingj.com' => array('Bing'), - - // Bing Images - 'bing.com/images/search' => array('Bing Images', array('q', 'Q'), '?q={k}'), - '{}.bing.com/images/search' => array('Bing Images'), - - // blekko - 'blekko.com' => array('blekko', array('q', '/\/ws\/(.*)/'), 'ws/{k}'), - - // Blogdigger - 'www.blogdigger.com' => array('Blogdigger', 'q'), - - // Blogpulse - 'www.blogpulse.com' => array('Blogpulse', 'query', 'search?query={k}'), - - // Bluewin - 'search.bluewin.ch' => array('Bluewin', 'searchTerm', '?searchTerm={k}'), - - // canoe.ca - 'web.canoe.ca' => array('Canoe.ca', 'q', 'search?q={k}'), - - // Centrum - 'search.centrum.cz' => array('Centrum', 'q', '?q={k}'), - 'morfeo.centrum.cz' => array('Centrum'), - - // Charter - 'www.charter.net' => array('Charter', 'q', 'search/index.php?q={k}'), - - // Clix (Enhanced by Google) - 'pesquisa.clix.pt' => array('Clix', 'question', 'resultado.html?in=Mundial&question={k}'), +if (!isset($GLOBALS['Piwik_SearchEngines'])) { + $GLOBALS['Piwik_SearchEngines'] = array( + // 1 + '1.cz' => array('1.cz', 'q', 'index.php?q={k}', 'iso-8859-2'), + + // 123people + 'www.123people.com' => array('123people', array('/s\/([^\/]+)/', 'search_term'), 's/{k}'), + '123people.{}' => array('123people'), + + // 360search + 'so.360.cn' => array('360search', 'q', 's?q={k}', array('UTF-8', 'gb2312')), + 'www.so.com' => array('360search', 'q', 's?q={k}', array('UTF-8', 'gb2312')), + + // Abacho + 'www.abacho.de' => array('Abacho', 'q', 'suche?q={k}'), + 'www.abacho.com' => array('Abacho'), + 'www.abacho.co.uk' => array('Abacho'), + 'www.se.abacho.com' => array('Abacho'), + 'www.tr.abacho.com' => array('Abacho'), + 'www.abacho.at' => array('Abacho'), + 'www.abacho.fr' => array('Abacho'), + 'www.abacho.es' => array('Abacho'), + 'www.abacho.ch' => array('Abacho'), + 'www.abacho.it' => array('Abacho'), + + // ABCsøk + 'abcsok.no' => array('ABCsøk', 'q', '?q={k}'), + 'verden.abcsok.no' => array('ABCsøk'), + + // Acoon + 'www.acoon.de' => array('Acoon', 'begriff', 'cgi-bin/search.exe?begriff={k}'), + + // Alexa + 'alexa.com' => array('Alexa', 'q', 'search?q={k}'), + 'search.toolbars.alexa.com' => array('Alexa'), + + // Alice Adsl + 'rechercher.aliceadsl.fr' => array('Alice Adsl', 'qs', 'google.pl?qs={k}'), + + // Allesklar + 'www.allesklar.de' => array('Allesklar', 'words', '?words={k}'), + 'www.allesklar.at' => array('Allesklar'), + 'www.allesklar.ch' => array('Allesklar'), + + // AllTheWeb + 'www.alltheweb.com' => array('AllTheWeb', 'q', 'search?q={k}'), + + // all.by + 'all.by' => array('All.by', 'query', 'cgi-bin/search.cgi?mode=by&query={k}'), + + // Altavista + 'www.altavista.com' => array('AltaVista', 'q', 'web/results?q={k}'), + 'search.altavista.com' => array('AltaVista'), + 'listings.altavista.com' => array('AltaVista'), + 'altavista.de' => array('AltaVista'), + 'altavista.fr' => array('AltaVista'), + '{}.altavista.com' => array('AltaVista'), + 'be-nl.altavista.com' => array('AltaVista'), + 'be-fr.altavista.com' => array('AltaVista'), + + // Apollo Latvia + 'apollo.lv/portal/search/' => array('Apollo lv', 'q', '?cof=FORID%3A11&q={k}&search_where=www'), + + // APOLLO7 + 'apollo7.de' => array('Apollo7', 'query', 'a7db/index.php?query={k}&de_sharelook=true&de_bing=true&de_witch=true&de_google=true&de_yahoo=true&de_lycos=true'), + + // AOL + 'search.aol.com' => array('AOL', array('query', 'q'), 'aol/search?q={k}'), + 'search.aol.it' => array('AOL'), + 'aolsearch.aol.com' => array('AOL'), + 'www.aolrecherche.aol.fr' => array('AOL'), + 'www.aolrecherches.aol.fr' => array('AOL'), + 'www.aolimages.aol.fr' => array('AOL'), + 'aim.search.aol.com' => array('AOL'), + 'www.recherche.aol.fr' => array('AOL'), + 'recherche.aol.fr' => array('AOL'), + 'find.web.aol.com' => array('AOL'), + 'recherche.aol.ca' => array('AOL'), + 'aolsearch.aol.co.uk' => array('AOL'), + 'search.aol.co.uk' => array('AOL'), + 'aolrecherche.aol.fr' => array('AOL'), + 'sucheaol.aol.de' => array('AOL'), + 'suche.aol.de' => array('AOL'), + 'o2suche.aol.de' => array('AOL'), + 'suche.aolsvc.de' => array('AOL'), + 'aolbusqueda.aol.com.mx' => array('AOL'), + 'alicesuche.aol.de' => array('AOL'), + 'alicesuchet.aol.de' => array('AOL'), + 'suchet2.aol.de' => array('AOL'), + 'search.hp.my.aol.com.au' => array('AOL'), + 'search.hp.my.aol.de' => array('AOL'), + 'search.hp.my.aol.it' => array('AOL'), + 'search-intl.netscape.com' => array('AOL'), + 'de.aolsearch.com' => array('AOL', 'q', 'search?q={k}'), + + // Aport + 'sm.aport.ru' => array('Aport', 'r', 'search?r={k}'), + + // arama + 'arama.com' => array('Arama', 'q', 'search.php3?q={k}'), + + // Arcor + 'www.arcor.de' => array('Arcor', 'Keywords', 'content/searchresult.jsp?Keywords={k}'), + + // Arianna (Libero.it) + 'arianna.libero.it' => array('Arianna', 'query', 'search/abin/integrata.cgi?query={k}'), + 'www.arianna.com' => array('Arianna'), + + // Ask (IAC Search & Media) + 'ask.com' => array('Ask', array('ask', 'q', 'searchfor'), 'web?q={k}'), + 'web.ask.com' => array('Ask'), + 'int.ask.com' => array('Ask'), + 'mws.ask.com' => array('Ask'), + 'images.ask.com' => array('Ask'), + 'images.{}.ask.com' => array('Ask'), + 'ask.reference.com' => array('Ask'), + 'www.askkids.com' => array('Ask'), + 'iwon.ask.com' => array('Ask'), + 'www.ask.co.uk' => array('Ask'), + '{}.ask.com' => array('Ask'), + 'www.qbyrd.com' => array('Ask'), + '{}.qbyrd.com' => array('Ask'), + 'www.search-results.com' => array('Ask'), + 'int.search-results.com' => array('Ask'), + '{}.search-results.com' => array('Ask'), + '{}.search.ask.com' => array('Ask'), + 'avira-int.ask.com' => array('Ask'), + + // Atlas + 'searchatlas.centrum.cz' => array('Atlas', 'q', '?q={k}'), + + // Austronaut + 'www2.austronaut.at' => array('Austronaut', 'q'), + 'www1.austronaut.at' => array('Austronaut'), + + // Babylon (Enhanced by Google) + 'search.babylon.com' => array('Babylon', array('q', '/\/web\/(.*)/'), '?q={k}'), + 'searchassist.babylon.com' => array('Babylon'), + + // Baidu + 'www.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), 's?wd={k}', array('UTF-8', 'gb2312')), + 'www1.baidu.com' => array('Baidu'), + 'zhidao.baidu.com' => array('Baidu'), + 'tieba.baidu.com' => array('Baidu'), + 'news.baidu.com' => array('Baidu'), + 'web.gougou.com' => array('Baidu', 'search', 'search?search={k}'), // uses baidu search + + // Biglobe + 'cgi.search.biglobe.ne.jp' => array('Biglobe', 'q', 'cgi-bin/search-st?q={k}'), + + // Bing + 'bing.com' => array('Bing', array('q', 'Q'), 'search?q={k}'), + '{}.bing.com' => array('Bing'), + 'msnbc.msn.com' => array('Bing'), + 'dizionario.it.msn.com' => array('Bing'), + + // Bing Cache + 'cc.bingj.com' => array('Bing'), + + // Bing Images + 'bing.com/images/search' => array('Bing Images', array('q', 'Q'), '?q={k}'), + '{}.bing.com/images/search' => array('Bing Images'), + + // blekko + 'blekko.com' => array('blekko', array('q', '/\/ws\/(.*)/'), 'ws/{k}'), + + // Blogdigger + 'www.blogdigger.com' => array('Blogdigger', 'q'), + + // Blogpulse + 'www.blogpulse.com' => array('Blogpulse', 'query', 'search?query={k}'), + + // Bluewin + 'search.bluewin.ch' => array('Bluewin', 'searchTerm', '?searchTerm={k}'), + + // canoe.ca + 'web.canoe.ca' => array('Canoe.ca', 'q', 'search?q={k}'), + + // Centrum + 'search.centrum.cz' => array('Centrum', 'q', '?q={k}'), + 'morfeo.centrum.cz' => array('Centrum'), + + // Charter + 'www.charter.net' => array('Charter', 'q', 'search/index.php?q={k}'), + + // Clix (Enhanced by Google) + 'pesquisa.clix.pt' => array('Clix', 'question', 'resultado.html?in=Mundial&question={k}'), - // Conduit - 'search.conduit.com' => array('Conduit.com', 'q', 'Results.aspx?q={k}'), + // Conduit + 'search.conduit.com' => array('Conduit.com', 'q', 'Results.aspx?q={k}'), - // Comcast - 'search.comcast.net' => array('Comcast', 'q', '?q={k}'), + // Comcast + 'search.comcast.net' => array('Comcast', 'q', '?q={k}'), - // Crawler - 'www.crawler.com' => array('Crawler', 'q', 'search/results1.aspx?q={k}'), + // Crawler + 'www.crawler.com' => array('Crawler', 'q', 'search/results1.aspx?q={k}'), - // Compuserve - 'websearch.cs.com' => array('Compuserve.com (Enhanced by Google)', 'query', 'cs/search?query={k}'), + // Compuserve + 'websearch.cs.com' => array('Compuserve.com (Enhanced by Google)', 'query', 'cs/search?query={k}'), - // Cuil - 'www.cuil.com' => array('Cuil', 'q', 'search?q={k}'), + // Cuil + 'www.cuil.com' => array('Cuil', 'q', 'search?q={k}'), - // Daemon search - 'daemon-search.com' => array('Daemon search', 'q', 'explore/web?q={k}'), - 'my.daemon-search.com' => array('Daemon search'), + // Daemon search + 'daemon-search.com' => array('Daemon search', 'q', 'explore/web?q={k}'), + 'my.daemon-search.com' => array('Daemon search'), - // DasOertliche - 'www.dasoertliche.de' => array('DasOertliche', 'kw'), + // DasOertliche + 'www.dasoertliche.de' => array('DasOertliche', 'kw'), - // DasTelefonbuch - 'www1.dastelefonbuch.de' => array('DasTelefonbuch', 'kw'), + // DasTelefonbuch + 'www1.dastelefonbuch.de' => array('DasTelefonbuch', 'kw'), - // Daum - 'search.daum.net' => array('Daum', 'q', 'search?q={k}', 'EUC-KR'), + // Daum + 'search.daum.net' => array('Daum', 'q', 'search?q={k}', 'EUC-KR'), - // Delfi Latvia - 'smart.delfi.lv' => array('Delfi lv', 'q', 'find?q={k}'), + // Delfi Latvia + 'smart.delfi.lv' => array('Delfi lv', 'q', 'find?q={k}'), - // Delfi - 'otsing.delfi.ee' => array('Delfi EE', 'q', 'find?q={k}'), + // Delfi + 'otsing.delfi.ee' => array('Delfi EE', 'q', 'find?q={k}'), - // Digg - 'digg.com' => array('Digg', 's', 'search?s={k}'), + // Digg + 'digg.com' => array('Digg', 's', 'search?s={k}'), - // dir.com - 'fr.dir.com' => array('dir.com', 'req'), + // dir.com + 'fr.dir.com' => array('dir.com', 'req'), - // dmoz - 'dmoz.org' => array('dmoz', 'search'), - 'editors.dmoz.org' => array('dmoz'), + // dmoz + 'dmoz.org' => array('dmoz', 'search'), + 'editors.dmoz.org' => array('dmoz'), - // DuckDuckGo - 'duckduckgo.com' => array('DuckDuckGo', 'q', '?q={k}'), + // DuckDuckGo + 'duckduckgo.com' => array('DuckDuckGo', 'q', '?q={k}'), - // earthlink - 'search.earthlink.net' => array('Earthlink', 'q', 'search?q={k}'), + // earthlink + 'search.earthlink.net' => array('Earthlink', 'q', 'search?q={k}'), - // Ecosia (powered by Bing) - 'ecosia.org' => array('Ecosia', 'q', 'search.php?q={k}'), + // Ecosia (powered by Bing) + 'ecosia.org' => array('Ecosia', 'q', 'search.php?q={k}'), - // Eniro - 'www.eniro.se' => array('Eniro', array('q', 'search_word'), 'query?q={k}'), + // Eniro + 'www.eniro.se' => array('Eniro', array('q', 'search_word'), 'query?q={k}'), - // Eurip - 'www.eurip.com' => array('Eurip', 'q', 'search/?q={k}'), + // Eurip + 'www.eurip.com' => array('Eurip', 'q', 'search/?q={k}'), - // Euroseek - 'www.euroseek.com' => array('Euroseek', 'string', 'system/search.cgi?string={k}'), + // Euroseek + 'www.euroseek.com' => array('Euroseek', 'string', 'system/search.cgi?string={k}'), - // Everyclick - 'www.everyclick.com' => array('Everyclick', 'keyword'), + // Everyclick + 'www.everyclick.com' => array('Everyclick', 'keyword'), - // Excite - 'search.excite.it' => array('Excite', 'q', 'web/?q={k}'), - 'search.excite.fr' => array('Excite'), - 'search.excite.de' => array('Excite'), - 'search.excite.co.uk' => array('Excite'), - 'search.excite.es' => array('Excite'), - 'search.excite.nl' => array('Excite'), - 'msxml.excite.com' => array('Excite', '/\/[^\/]+\/ws\/results\/[^\/]+\/([^\/]+)/'), - 'www.excite.co.jp' => array('Excite', 'search', 'search.gw?search={k}', 'SHIFT_JIS'), + // Excite + 'search.excite.it' => array('Excite', 'q', 'web/?q={k}'), + 'search.excite.fr' => array('Excite'), + 'search.excite.de' => array('Excite'), + 'search.excite.co.uk' => array('Excite'), + 'search.excite.es' => array('Excite'), + 'search.excite.nl' => array('Excite'), + 'msxml.excite.com' => array('Excite', '/\/[^\/]+\/ws\/results\/[^\/]+\/([^\/]+)/'), + 'www.excite.co.jp' => array('Excite', 'search', 'search.gw?search={k}', 'SHIFT_JIS'), - // Exalead - 'www.exalead.fr' => array('Exalead', 'q', 'search/results?q={k}'), - 'www.exalead.com' => array('Exalead'), + // Exalead + 'www.exalead.fr' => array('Exalead', 'q', 'search/results?q={k}'), + 'www.exalead.com' => array('Exalead'), - // eo - 'eo.st' => array('eo', 'x_query', 'cgi-bin/eolost.cgi?x_query={k}'), + // eo + 'eo.st' => array('eo', 'x_query', 'cgi-bin/eolost.cgi?x_query={k}'), - // Facebook - 'www.facebook.com' => array('Facebook', 'q', 'search/?q={k}'), - - // Fast Browser Search - 'www.fastbrowsersearch.com' => array('Fast Browser Search', 'q', 'results/results.aspx?q={k}'), - - // Francite - 'recherche.francite.com' => array('Francite', 'name'), - - // Fireball - 'www.fireball.de' => array('Fireball', 'q', 'ajax.asp?q={k}'), - - // Firstfind - 'www.firstsfind.com' => array('Firstsfind', 'qry'), - - // Fixsuche - 'www.fixsuche.de' => array('Fixsuche', 'q'), - - // Flix - 'www.flix.de' => array('Flix.de', 'keyword'), - - // Forestle - 'forestle.org' => array('Forestle', 'q', 'search.php?q={k}'), - '{}.forestle.org' => array('Forestle'), - 'forestle.mobi' => array('Forestle'), - - // Free - 'search.free.fr' => array('Free', 'q'), - 'search1-2.free.fr' => array('Free'), - 'search1-1.free.fr' => array('Free'), - - // Freecause - 'search.freecause.com' => array('FreeCause', 'p', '?p={k}'), - - // Freenet - 'suche.freenet.de' => array('Freenet', array('query', 'Keywords'), 'suche/?query={k}'), - - // FriendFeed - 'friendfeed.com' => array('FriendFeed', 'q', 'search?q={k}'), - - // GAIS - 'gais.cs.ccu.edu.tw' => array('GAIS', 'q', 'search.php?q={k}'), - - // Geona - 'geona.net' => array('Geona', 'q', 'search?q={k}'), - - // Gigablast - 'www.gigablast.com' => array('Gigablast', 'q', 'search?q={k}'), - 'dir.gigablast.com' => array('Gigablast (Directory)', 'q'), - - // Gnadenmeer - 'www.gnadenmeer.de' => array('Gnadenmeer', 'keyword'), - - // Gomeo - 'www.gomeo.com' => array('Gomeo', array('Keywords', '/\/search\/([^\/]+)/'), '/search/{k}'), - - // goo - 'search.goo.ne.jp' => array('goo', 'MT', 'web.jsp?MT={k}'), - 'ocnsearch.goo.ne.jp' => array('goo'), - - // Google - 'google.com' => array('Google', 'q', 'search?q={k}'), - 'google.{}' => array('Google'), - 'www2.google.com' => array('Google'), - 'ipv6.google.com' => array('Google'), - 'go.google.com' => array('Google'), - - // Google vs typo squatters - 'wwwgoogle.com' => array('Google'), - 'wwwgoogle.{}' => array('Google'), - 'gogole.com' => array('Google'), - 'gogole.{}' => array('Google'), - 'gppgle.com' => array('Google'), - 'gppgle.{}' => array('Google'), - 'googel.com' => array('Google'), - 'googel.{}' => array('Google'), - - // Powered by Google - 'search.avg.com' => array('Google'), - 'isearch.avg.com' => array('Google'), - 'www.cnn.com' => array('Google', 'query'), - 'darkoogle.com' => array('Google'), - 'search.darkoogle.com' => array('Google'), - 'search.foxtab.com' => array('Google'), - 'www.gooofullsearch.com' => array('Google', 'Keywords'), - 'search.hiyo.com' => array('Google'), - 'search.incredimail.com' => array('Google'), - 'search1.incredimail.com' => array('Google'), - 'search2.incredimail.com' => array('Google'), - 'search3.incredimail.com' => array('Google'), - 'search4.incredimail.com' => array('Google'), - 'search.sweetim.com' => array('Google'), - 'www.fastweb.it' => array('Google'), - 'search.juno.com' => array('Google', 'query'), - 'find.tdc.dk' => array('Google'), - 'it.luna.tv' => array('Google'), - 'searchresults.verizon.com' => array('Google'), - 'search.walla.co.il' => array('Google'), - 'search.alot.com' => array('Google'), - 'suche.gmx.net' => array('Google', 'q', 'web?q={k}'), - 'search.incredibar.com' => array('Google', 'q', 'search.php?q={k}'), - 'www.delta-search.com' => array('Google', 'q'), - 'search.1und1.de' => array('Google', 'q', 'web?q={k}'), - 'search.zonealarm.com' => array('Google'), - 'start.lenovo.com' => array('Google', 'q', 'search/index.php?q={k}'), - - // Google Earth - // - 2010-09-13: are these redirects now? - 'www.googleearth.de' => array('Google'), - 'www.googleearth.fr' => array('Google'), - - // Google Cache - 'webcache.googleusercontent.com'=> array('Google', '/\/search\?q=cache:[A-Za-z0-9]+:[^+]+([^&]+)/', 'search?q={k}'), - - // Google SSL - 'encrypted.google.com' => array('Google SSL', 'q', 'search?q={k}'), - - // Google Blogsearch - 'blogsearch.google.com' => array('Google Blogsearch', 'q', 'blogsearch?q={k}'), - 'blogsearch.google.{}' => array('Google Blogsearch'), - - // Google Custom Search - 'google.com/cse' => array('Google Custom Search', array('q', 'query')), - 'google.{}/cse' => array('Google Custom Search'), - 'google.com/custom' => array('Google Custom Search'), - 'google.{}/custom' => array('Google Custom Search'), - - // Google Translation - 'translate.google.com' => array('Google Translations', 'q'), - - // Google Images - 'images.google.com' => array('Google Images', 'q', 'images?q={k}'), - 'images.google.{}' => array('Google Images'), - - // Google Maps - 'maps.google.com' => array('Google Maps', 'q', 'maps?q={k}'), - 'maps.google.{}' => array('Google Maps'), - - // Google News - 'news.google.com' => array('Google News', 'q'), - 'news.google.{}' => array('Google News'), - - // Google Shopping - 'google.com/products' => array('Google Shopping', 'q', '?q={k}&tbm=shop'), - 'google.{}/products' => array('Google Shopping'), - - // Google syndicated search - 'googlesyndicatedsearch.com'=> array('Google syndicated search', 'q'), - - // Google Video - 'video.google.com' => array('Google Video', 'q', 'search?q={k}&tbm=vid'), - - // Google Wireless Transcoder - // - does not appear to execute JavaScript + // Facebook + 'www.facebook.com' => array('Facebook', 'q', 'search/?q={k}'), + + // Fast Browser Search + 'www.fastbrowsersearch.com' => array('Fast Browser Search', 'q', 'results/results.aspx?q={k}'), + + // Francite + 'recherche.francite.com' => array('Francite', 'name'), + + // Fireball + 'www.fireball.de' => array('Fireball', 'q', 'ajax.asp?q={k}'), + + // Firstfind + 'www.firstsfind.com' => array('Firstsfind', 'qry'), + + // Fixsuche + 'www.fixsuche.de' => array('Fixsuche', 'q'), + + // Flix + 'www.flix.de' => array('Flix.de', 'keyword'), + + // Forestle + 'forestle.org' => array('Forestle', 'q', 'search.php?q={k}'), + '{}.forestle.org' => array('Forestle'), + 'forestle.mobi' => array('Forestle'), + + // Free + 'search.free.fr' => array('Free', 'q'), + 'search1-2.free.fr' => array('Free'), + 'search1-1.free.fr' => array('Free'), + + // Freecause + 'search.freecause.com' => array('FreeCause', 'p', '?p={k}'), + + // Freenet + 'suche.freenet.de' => array('Freenet', array('query', 'Keywords'), 'suche/?query={k}'), + + // FriendFeed + 'friendfeed.com' => array('FriendFeed', 'q', 'search?q={k}'), + + // GAIS + 'gais.cs.ccu.edu.tw' => array('GAIS', 'q', 'search.php?q={k}'), + + // Geona + 'geona.net' => array('Geona', 'q', 'search?q={k}'), + + // Gigablast + 'www.gigablast.com' => array('Gigablast', 'q', 'search?q={k}'), + 'dir.gigablast.com' => array('Gigablast (Directory)', 'q'), + + // Gnadenmeer + 'www.gnadenmeer.de' => array('Gnadenmeer', 'keyword'), + + // Gomeo + 'www.gomeo.com' => array('Gomeo', array('Keywords', '/\/search\/([^\/]+)/'), '/search/{k}'), + + // goo + 'search.goo.ne.jp' => array('goo', 'MT', 'web.jsp?MT={k}'), + 'ocnsearch.goo.ne.jp' => array('goo'), + + // Google + 'google.com' => array('Google', 'q', 'search?q={k}'), + 'google.{}' => array('Google'), + 'www2.google.com' => array('Google'), + 'ipv6.google.com' => array('Google'), + 'go.google.com' => array('Google'), + + // Google vs typo squatters + 'wwwgoogle.com' => array('Google'), + 'wwwgoogle.{}' => array('Google'), + 'gogole.com' => array('Google'), + 'gogole.{}' => array('Google'), + 'gppgle.com' => array('Google'), + 'gppgle.{}' => array('Google'), + 'googel.com' => array('Google'), + 'googel.{}' => array('Google'), + + // Powered by Google + 'search.avg.com' => array('Google'), + 'isearch.avg.com' => array('Google'), + 'www.cnn.com' => array('Google', 'query'), + 'darkoogle.com' => array('Google'), + 'search.darkoogle.com' => array('Google'), + 'search.foxtab.com' => array('Google'), + 'www.gooofullsearch.com' => array('Google', 'Keywords'), + 'search.hiyo.com' => array('Google'), + 'search.incredimail.com' => array('Google'), + 'search1.incredimail.com' => array('Google'), + 'search2.incredimail.com' => array('Google'), + 'search3.incredimail.com' => array('Google'), + 'search4.incredimail.com' => array('Google'), + 'search.sweetim.com' => array('Google'), + 'www.fastweb.it' => array('Google'), + 'search.juno.com' => array('Google', 'query'), + 'find.tdc.dk' => array('Google'), + 'it.luna.tv' => array('Google'), + 'searchresults.verizon.com' => array('Google'), + 'search.walla.co.il' => array('Google'), + 'search.alot.com' => array('Google'), + 'suche.gmx.net' => array('Google', 'q', 'web?q={k}'), + 'search.incredibar.com' => array('Google', 'q', 'search.php?q={k}'), + 'www.delta-search.com' => array('Google', 'q'), + 'search.1und1.de' => array('Google', 'q', 'web?q={k}'), + 'search.zonealarm.com' => array('Google'), + 'start.lenovo.com' => array('Google', 'q', 'search/index.php?q={k}'), + + // Google Earth + // - 2010-09-13: are these redirects now? + 'www.googleearth.de' => array('Google'), + 'www.googleearth.fr' => array('Google'), + + // Google Cache + 'webcache.googleusercontent.com' => array('Google', '/\/search\?q=cache:[A-Za-z0-9]+:[^+]+([^&]+)/', 'search?q={k}'), + + // Google SSL + 'encrypted.google.com' => array('Google SSL', 'q', 'search?q={k}'), + + // Google Blogsearch + 'blogsearch.google.com' => array('Google Blogsearch', 'q', 'blogsearch?q={k}'), + 'blogsearch.google.{}' => array('Google Blogsearch'), + + // Google Custom Search + 'google.com/cse' => array('Google Custom Search', array('q', 'query')), + 'google.{}/cse' => array('Google Custom Search'), + 'google.com/custom' => array('Google Custom Search'), + 'google.{}/custom' => array('Google Custom Search'), + + // Google Translation + 'translate.google.com' => array('Google Translations', 'q'), + + // Google Images + 'images.google.com' => array('Google Images', 'q', 'images?q={k}'), + 'images.google.{}' => array('Google Images'), + + // Google Maps + 'maps.google.com' => array('Google Maps', 'q', 'maps?q={k}'), + 'maps.google.{}' => array('Google Maps'), + + // Google News + 'news.google.com' => array('Google News', 'q'), + 'news.google.{}' => array('Google News'), + + // Google Shopping + 'google.com/products' => array('Google Shopping', 'q', '?q={k}&tbm=shop'), + 'google.{}/products' => array('Google Shopping'), + + // Google syndicated search + 'googlesyndicatedsearch.com' => array('Google syndicated search', 'q'), + + // Google Video + 'video.google.com' => array('Google Video', 'q', 'search?q={k}&tbm=vid'), + + // Google Wireless Transcoder + // - does not appear to execute JavaScript // 'google.com/gwt/n' => array('Google Wireless Transcoder'), - // Goyellow.de - 'www.goyellow.de' => array('GoYellow.de', 'MDN'), - - // Gule Sider - 'www.gulesider.no' => array('Gule Sider', 'q'), - - // HighBeam - 'www.highbeam.com' => array('HighBeam', 'q', 'Search.aspx?q={k}'), - - // Hit-Parade - 'req.hit-parade.com' => array('Hit-Parade', 'p7', 'general/recherche.asp?p7={k}'), - 'class.hit-parade.com' => array('Hit-Parade'), - 'www.hit-parade.com' => array('Hit-Parade'), - - // Holmes.ge - 'holmes.ge' => array('Holmes', 'q', 'search.htm?q={k}'), - - // Hooseek.com - 'www.hooseek.com' => array('Hooseek', 'recherche', 'web?recherche={k}'), - - // Hotbot - 'www.hotbot.com' => array('Hotbot', 'query'), - - // Icerocket - 'blogs.icerocket.com' => array('Icerocket', 'q', 'search?q={k}'), - - // ICQ - 'www.icq.com' => array('ICQ', 'q', 'search/results.php?q={k}'), - 'search.icq.com' => array('ICQ'), - - // Ilse - 'www.ilse.nl' => array('Ilse NL', 'search_for', '?search_for={k}'), - - // Inbox.com - 'www2.inbox.com' => array('Inbox', 'q', 'search/results1.aspx?q={k}'), - - // InfoSpace (and related web properties) - 'infospace.com' => array('InfoSpace', 'q', '/search/web?q={k}'), - 'dogpile.com' => array('InfoSpace'), - 'tattoodle.com' => array('InfoSpace'), - 'metacrawler.com' => array('InfoSpace'), - 'webfetch.com' => array('InfoSpace'), - 'webcrawler.com' => array('InfoSpace'), - 'search.kiwee.com' => array('InfoSpace'), - - // old infospace system - 'wsdsold.infospace.com' => array('InfoSpace', '/\/[^\/]+\/ws\/results\/[^\/]+\/([^\/]+)/', 'pemonitorhosted/ws/results/Web/{k}/1/417/TopNavigation/Source/'), - - // Powered by InfoSpace - 'isearch.babylon.com' => array('InfoSpace', 'q'), - 'start.facemoods.com' => array('InfoSpace', 's'), - 'search.magentic.com' => array('InfoSpace', 'q'), - 'search.searchcompletion.com'=> array('InfoSpace', 'q'), - - /* - * Other InfoSpace powered metasearches are handled in Piwik_Common::extractSearchEngineInformationFromUrl() - * - * This includes sites such as: - * - search.nation.com - * - ws.copernic.com - * - result.iminent.com - */ - - // Interia - 'www.google.interia.pl' => array('Interia', 'q', 'szukaj?q={k}'), - - // I-play - 'start.iplay.com' => array('I-play', 'q', 'searchresults.aspx?q={k}'), - - // Ixquick - 'ixquick.com' => array('Ixquick', 'query'), - 'www.eu.ixquick.com' => array('Ixquick'), - 'ixquick.de' => array('Ixquick'), - 'www.ixquick.de' => array('Ixquick'), - 'us.ixquick.com' => array('Ixquick'), - 's1.us.ixquick.com' => array('Ixquick'), - 's2.us.ixquick.com' => array('Ixquick'), - 's3.us.ixquick.com' => array('Ixquick'), - 's4.us.ixquick.com' => array('Ixquick'), - 's5.us.ixquick.com' => array('Ixquick'), - 'eu.ixquick.com' => array('Ixquick'), - 's8-eu.ixquick.com' => array('Ixquick'), - 's1-eu.ixquick.de' => array('Ixquick'), - - // Jyxo - 'jyxo.1188.cz' => array('Jyxo', 'q', 's?q={k}'), - - // Jungle Spider - 'www.jungle-spider.de' => array('Jungle Spider', 'q'), - - // Jungle key - 'junglekey.com' => array('Jungle Key', 'query', 'search.php?query={k}&type=web&lang=en'), - 'junglekey.fr' => array('Jungle Key'), + // Goyellow.de + 'www.goyellow.de' => array('GoYellow.de', 'MDN'), + + // Gule Sider + 'www.gulesider.no' => array('Gule Sider', 'q'), + + // HighBeam + 'www.highbeam.com' => array('HighBeam', 'q', 'Search.aspx?q={k}'), + + // Hit-Parade + 'req.hit-parade.com' => array('Hit-Parade', 'p7', 'general/recherche.asp?p7={k}'), + 'class.hit-parade.com' => array('Hit-Parade'), + 'www.hit-parade.com' => array('Hit-Parade'), + + // Holmes.ge + 'holmes.ge' => array('Holmes', 'q', 'search.htm?q={k}'), + + // Hooseek.com + 'www.hooseek.com' => array('Hooseek', 'recherche', 'web?recherche={k}'), + + // Hotbot + 'www.hotbot.com' => array('Hotbot', 'query'), + + // Icerocket + 'blogs.icerocket.com' => array('Icerocket', 'q', 'search?q={k}'), + + // ICQ + 'www.icq.com' => array('ICQ', 'q', 'search/results.php?q={k}'), + 'search.icq.com' => array('ICQ'), + + // Ilse + 'www.ilse.nl' => array('Ilse NL', 'search_for', '?search_for={k}'), + + // Inbox.com + 'www2.inbox.com' => array('Inbox', 'q', 'search/results1.aspx?q={k}'), + + // InfoSpace (and related web properties) + 'infospace.com' => array('InfoSpace', 'q', '/search/web?q={k}'), + 'dogpile.com' => array('InfoSpace'), + 'tattoodle.com' => array('InfoSpace'), + 'metacrawler.com' => array('InfoSpace'), + 'webfetch.com' => array('InfoSpace'), + 'webcrawler.com' => array('InfoSpace'), + 'search.kiwee.com' => array('InfoSpace'), + + // old infospace system + 'wsdsold.infospace.com' => array('InfoSpace', '/\/[^\/]+\/ws\/results\/[^\/]+\/([^\/]+)/', 'pemonitorhosted/ws/results/Web/{k}/1/417/TopNavigation/Source/'), + + // Powered by InfoSpace + 'isearch.babylon.com' => array('InfoSpace', 'q'), + 'start.facemoods.com' => array('InfoSpace', 's'), + 'search.magentic.com' => array('InfoSpace', 'q'), + 'search.searchcompletion.com' => array('InfoSpace', 'q'), + + /* + * Other InfoSpace powered metasearches are handled in Piwik_Common::extractSearchEngineInformationFromUrl() + * + * This includes sites such as: + * - search.nation.com + * - ws.copernic.com + * - result.iminent.com + */ + + // Interia + 'www.google.interia.pl' => array('Interia', 'q', 'szukaj?q={k}'), + + // I-play + 'start.iplay.com' => array('I-play', 'q', 'searchresults.aspx?q={k}'), + + // Ixquick + 'ixquick.com' => array('Ixquick', 'query'), + 'www.eu.ixquick.com' => array('Ixquick'), + 'ixquick.de' => array('Ixquick'), + 'www.ixquick.de' => array('Ixquick'), + 'us.ixquick.com' => array('Ixquick'), + 's1.us.ixquick.com' => array('Ixquick'), + 's2.us.ixquick.com' => array('Ixquick'), + 's3.us.ixquick.com' => array('Ixquick'), + 's4.us.ixquick.com' => array('Ixquick'), + 's5.us.ixquick.com' => array('Ixquick'), + 'eu.ixquick.com' => array('Ixquick'), + 's8-eu.ixquick.com' => array('Ixquick'), + 's1-eu.ixquick.de' => array('Ixquick'), + + // Jyxo + 'jyxo.1188.cz' => array('Jyxo', 'q', 's?q={k}'), + + // Jungle Spider + 'www.jungle-spider.de' => array('Jungle Spider', 'q'), + + // Jungle key + 'junglekey.com' => array('Jungle Key', 'query', 'search.php?query={k}&type=web&lang=en'), + 'junglekey.fr' => array('Jungle Key'), + + // Kataweb + 'www.kataweb.it' => array('Kataweb', 'q'), + + // Kvasir + 'www.kvasir.no' => array('Kvasir', 'q', 'alle?q={k}'), + + // Latne + 'www.latne.lv' => array('Latne', 'q', 'siets.php?q={k}'), + + // La Toile Du Québec via Google + 'www.toile.com' => array('La Toile Du Québec (Google)', 'q', 'search?q={k}'), + 'web.toile.com' => array('La Toile Du Québec (Google)'), + + // Looksmart + 'www.looksmart.com' => array('Looksmart', 'key'), + + // Lo.st (Enhanced by Google) + 'lo.st' => array('Lo.st', 'x_query', 'cgi-bin/eolost.cgi?x_query={k}'), + + // Lycos + 'search.lycos.com' => array('Lycos', 'query', '?query={k}'), + 'lycos.{}' => array('Lycos'), + + // maailm.com + 'www.maailm.com' => array('maailm.com', 'tekst'), + + // Mail.ru + 'go.mail.ru' => array('Mailru', 'q', 'search?rch=e&q={k}', array('UTF-8', 'windows-1251')), + + // Mamma + 'www.mamma.com' => array('Mamma', 'query', 'result.php?q={k}'), + 'mamma75.mamma.com' => array('Mamma'), + + // Meta + 'meta.ua' => array('Meta.ua', 'q', 'search.asp?q={k}'), + + // MetaCrawler.de + 's1.metacrawler.de' => array('MetaCrawler DE', 'qry', '?qry={k}'), + 's2.metacrawler.de' => array('MetaCrawler DE'), + 's3.metacrawler.de' => array('MetaCrawler DE'), + + // Metager + 'meta.rrzn.uni-hannover.de' => array('Metager', 'eingabe', 'meta/cgi-bin/meta.ger1?eingabe={k}'), + 'www.metager.de' => array('Metager'), + + // Metager2 + 'metager2.de' => array('Metager2', 'q', 'search/index.php?q={k}'), + + // Meinestadt + 'www.meinestadt.de' => array('Meinestadt.de', 'words'), - // Kataweb - 'www.kataweb.it' => array('Kataweb', 'q'), + // Mister Wong + 'www.mister-wong.com' => array('Mister Wong', 'keywords', 'search/?keywords={k}'), + 'www.mister-wong.de' => array('Mister Wong'), - // Kvasir - 'www.kvasir.no' => array('Kvasir', 'q', 'alle?q={k}'), + // Monstercrawler + 'www.monstercrawler.com' => array('Monstercrawler', 'qry'), - // Latne - 'www.latne.lv' => array('Latne', 'q', 'siets.php?q={k}'), + // Mozbot + 'www.mozbot.fr' => array('mozbot', 'q', 'results.php?q={k}'), + 'www.mozbot.co.uk' => array('mozbot'), + 'www.mozbot.com' => array('mozbot'), - // La Toile Du Québec via Google - 'www.toile.com' => array('La Toile Du Québec (Google)', 'q', 'search?q={k}'), - 'web.toile.com' => array('La Toile Du Québec (Google)'), - - // Looksmart - 'www.looksmart.com' => array('Looksmart', 'key'), - - // Lo.st (Enhanced by Google) - 'lo.st' => array('Lo.st', 'x_query', 'cgi-bin/eolost.cgi?x_query={k}'), - - // Lycos - 'search.lycos.com' => array('Lycos', 'query', '?query={k}'), - 'lycos.{}' => array('Lycos'), - - // maailm.com - 'www.maailm.com' => array('maailm.com', 'tekst'), - - // Mail.ru - 'go.mail.ru' => array('Mailru', 'q', 'search?rch=e&q={k}', array('UTF-8', 'windows-1251')), - - // Mamma - 'www.mamma.com' => array('Mamma', 'query', 'result.php?q={k}'), - 'mamma75.mamma.com' => array('Mamma'), - - // Meta - 'meta.ua' => array('Meta.ua', 'q', 'search.asp?q={k}'), - - // MetaCrawler.de - 's1.metacrawler.de' => array('MetaCrawler DE', 'qry', '?qry={k}'), - 's2.metacrawler.de' => array('MetaCrawler DE'), - 's3.metacrawler.de' => array('MetaCrawler DE'), - - // Metager - 'meta.rrzn.uni-hannover.de' => array('Metager', 'eingabe', 'meta/cgi-bin/meta.ger1?eingabe={k}'), - 'www.metager.de' => array('Metager'), - - // Metager2 - 'metager2.de' => array('Metager2', 'q', 'search/index.php?q={k}'), - - // Meinestadt - 'www.meinestadt.de' => array('Meinestadt.de', 'words'), + // El Mundo + 'ariadna.elmundo.es' => array('El Mundo', 'q'), - // Mister Wong - 'www.mister-wong.com' => array('Mister Wong', 'keywords', 'search/?keywords={k}'), - 'www.mister-wong.de' => array('Mister Wong'), + // MySpace + 'searchservice.myspace.com' => array('MySpace', 'qry', 'index.cfm?fuseaction=sitesearch.results&type=Web&qry={k}'), - // Monstercrawler - 'www.monstercrawler.com' => array('Monstercrawler', 'qry'), + // MySearch / MyWay / MyWebSearch (default: powered by Ask.com) + 'www.mysearch.com' => array('MyWebSearch', array('searchfor', 'searchFor'), 'search/Ajmain.jhtml?searchfor={k}'), + 'ms114.mysearch.com' => array('MyWebSearch'), + 'ms146.mysearch.com' => array('MyWebSearch'), + 'kf.mysearch.myway.com' => array('MyWebSearch'), + 'ki.mysearch.myway.com' => array('MyWebSearch'), + 'search.myway.com' => array('MyWebSearch'), + 'search.mywebsearch.com' => array('MyWebSearch'), - // Mozbot - 'www.mozbot.fr' => array('mozbot', 'q', 'results.php?q={k}'), - 'www.mozbot.co.uk' => array('mozbot'), - 'www.mozbot.com' => array('mozbot'), - // El Mundo - 'ariadna.elmundo.es' => array('El Mundo', 'q'), + // Najdi + 'www.najdi.si' => array('Najdi.si', 'q', 'search.jsp?q={k}'), - // MySpace - 'searchservice.myspace.com' => array('MySpace', 'qry', 'index.cfm?fuseaction=sitesearch.results&type=Web&qry={k}'), + // Nate + 'search.nate.com' => array('Nate', 'q', 'search/all.html?q={k}', 'EUC-KR'), - // MySearch / MyWay / MyWebSearch (default: powered by Ask.com) - 'www.mysearch.com' => array('MyWebSearch', array('searchfor', 'searchFor'), 'search/Ajmain.jhtml?searchfor={k}'), - 'ms114.mysearch.com' => array('MyWebSearch'), - 'ms146.mysearch.com' => array('MyWebSearch'), - 'kf.mysearch.myway.com' => array('MyWebSearch'), - 'ki.mysearch.myway.com' => array('MyWebSearch'), - 'search.myway.com' => array('MyWebSearch'), - 'search.mywebsearch.com' => array('MyWebSearch'), + // Naver + 'search.naver.com' => array('Naver', 'query', 'search.naver?query={k}', 'EUC-KR'), + // Needtofind + 'ko.search.need2find.com' => array('Needtofind', 'searchfor', 'search/AJmain.jhtml?searchfor={k}'), - // Najdi - 'www.najdi.si' => array('Najdi.si', 'q', 'search.jsp?q={k}'), + // Neti + 'www.neti.ee' => array('Neti', 'query', 'cgi-bin/otsing?query={k}', 'iso-8859-1'), - // Nate - 'search.nate.com' => array('Nate', 'q', 'search/all.html?q={k}', 'EUC-KR'), + // Nifty + 'search.nifty.com' => array('Nifty', 'q', 'websearch/search?q={k}'), - // Naver - 'search.naver.com' => array('Naver', 'query', 'search.naver?query={k}', 'EUC-KR'), + // Nigma + 'nigma.ru' => array('Nigma', 's', 'index.php?s={k}'), - // Needtofind - 'ko.search.need2find.com' => array('Needtofind', 'searchfor', 'search/AJmain.jhtml?searchfor={k}'), + // Onet + 'szukaj.onet.pl' => array('Onet.pl', 'qt', 'query.html?qt={k}'), - // Neti - 'www.neti.ee' => array('Neti', 'query', 'cgi-bin/otsing?query={k}', 'iso-8859-1'), + // Online.no + 'online.no' => array('Online.no', 'q', 'google/index.jsp?q={k}'), - // Nifty - 'search.nifty.com' => array('Nifty', 'q', 'websearch/search?q={k}'), + // Opplysningen 1881 + 'www.1881.no' => array('Opplysningen 1881', 'Query', 'Multi/?Query={k}'), - // Nigma - 'nigma.ru' => array('Nigma', 's', 'index.php?s={k}'), + // Orange + 'busca.orange.es' => array('Orange', 'q', 'search?q={k}'), - // Onet - 'szukaj.onet.pl' => array('Onet.pl', 'qt', 'query.html?qt={k}'), + // Paperball + 'www.paperball.de' => array('Paperball', 'q', 'suche/s/?q={k}'), - // Online.no - 'online.no' => array('Online.no', 'q', 'google/index.jsp?q={k}'), + // PeoplePC + 'search.peoplepc.com' => array('PeoplePC', 'q', 'search?q={k}'), - // Opplysningen 1881 - 'www.1881.no' => array('Opplysningen 1881', 'Query', 'Multi/?Query={k}'), + // Picsearch + 'www.picsearch.com' => array('Picsearch', 'q', 'index.cgi?q={k}'), - // Orange - 'busca.orange.es' => array('Orange', 'q', 'search?q={k}'), - - // Paperball - 'www.paperball.de' => array('Paperball', 'q', 'suche/s/?q={k}'), + // Plazoo + 'www.plazoo.com' => array('Plazoo', 'q'), - // PeoplePC - 'search.peoplepc.com' => array('PeoplePC', 'q', 'search?q={k}'), + // Poisk.Ru + 'poisk.ru' => array('Poisk.Ru', 'text', 'cgi-bin/poisk?text={k}', 'windows-1251'), - // Picsearch - 'www.picsearch.com' => array('Picsearch', 'q', 'index.cgi?q={k}'), + // qip + 'search.qip.ru' => array('qip.ru', 'query', 'search?query={k}'), - // Plazoo - 'www.plazoo.com' => array('Plazoo', 'q'), + // Qualigo + 'www.qualigo.at' => array('Qualigo', 'q'), + 'www.qualigo.ch' => array('Qualigo'), + 'www.qualigo.de' => array('Qualigo'), + 'www.qualigo.nl' => array('Qualigo'), - // Poisk.Ru - 'poisk.ru' => array('Poisk.Ru', 'text', 'cgi-bin/poisk?text={k}', 'windows-1251'), + // Rakuten + 'websearch.rakuten.co.jp' => array('Rakuten', 'qt', 'WebIS?qt={k}'), - // qip - 'search.qip.ru' => array('qip.ru', 'query', 'search?query={k}'), + // Rambler + 'nova.rambler.ru' => array('Rambler', array('query', 'words'), 'search?query={k}'), - // Qualigo - 'www.qualigo.at' => array('Qualigo', 'q'), - 'www.qualigo.ch' => array('Qualigo'), - 'www.qualigo.de' => array('Qualigo'), - 'www.qualigo.nl' => array('Qualigo'), + // RPMFind + 'rpmfind.net' => array('rpmfind', 'query', 'linux/rpm2html/search.php?query={k}'), + 'fr2.rpmfind.net' => array('rpmfind'), - // Rakuten - 'websearch.rakuten.co.jp' => array('Rakuten', 'qt', 'WebIS?qt={k}'), + // Road Runner Search + 'search.rr.com' => array('Road Runner', 'q', '?q={k}'), - // Rambler - 'nova.rambler.ru' => array('Rambler', array('query', 'words'), 'search?query={k}'), + // Sapo + 'pesquisa.sapo.pt' => array('Sapo', 'q', '?q={k}'), - // RPMFind - 'rpmfind.net' => array('rpmfind', 'query', 'linux/rpm2html/search.php?query={k}'), - 'fr2.rpmfind.net' => array('rpmfind'), + // scour.com + 'scour.com' => array('Scour.com', '/search\/[^\/]+\/(.*)/', 'search/web/{k}'), - // Road Runner Search - 'search.rr.com' => array('Road Runner', 'q', '?q={k}'), + // Search.com + 'www.search.com' => array('Search.com', 'q', 'search?q={k}'), - // Sapo - 'pesquisa.sapo.pt' => array('Sapo', 'q', '?q={k}'), + // Search.ch + 'www.search.ch' => array('Search.ch', 'q', '?q={k}'), - // scour.com - 'scour.com' => array('Scour.com', '/search\/[^\/]+\/(.*)/', 'search/web/{k}'), + // Searchalot + 'searchalot.com' => array('Searchalot', 'q', '?q={k}'), - // Search.com - 'www.search.com' => array('Search.com', 'q', 'search?q={k}'), + // SearchCanvas + 'www.searchcanvas.com' => array('SearchCanvas', 'q', 'web?q={k}'), - // Search.ch - 'www.search.ch' => array('Search.ch', 'q', '?q={k}'), + // Searchy + 'www.searchy.co.uk' => array('Searchy', 'q', 'index.html?q={k}'), - // Searchalot - 'searchalot.com' => array('Searchalot', 'q', '?q={k}'), + // Setooz + // 2010-09-13: the mismatches are because subdomains are language codes + // (not country codes) + 'bg.setooz.com' => array('Setooz', 'query', 'search?query={k}'), + 'da.setooz.com' => array('Setooz'), + 'el.setooz.com' => array('Setooz'), + 'fa.setooz.com' => array('Setooz'), + 'ur.setooz.com' => array('Setooz'), + '{}.setooz.com' => array('Setooz'), - // SearchCanvas - 'www.searchcanvas.com' => array('SearchCanvas', 'q', 'web?q={k}'), + // Seznam + 'search.seznam.cz' => array('Seznam', 'q', '?q={k}'), - // Searchy - 'www.searchy.co.uk' => array('Searchy', 'q', 'index.html?q={k}'), + // Sharelook + 'www.sharelook.fr' => array('Sharelook', 'keyword'), - // Setooz - // 2010-09-13: the mismatches are because subdomains are language codes - // (not country codes) - 'bg.setooz.com' => array('Setooz', 'query', 'search?query={k}'), - 'da.setooz.com' => array('Setooz'), - 'el.setooz.com' => array('Setooz'), - 'fa.setooz.com' => array('Setooz'), - 'ur.setooz.com' => array('Setooz'), - '{}.setooz.com' => array('Setooz'), + // Skynet + 'www.skynet.be' => array('Skynet', 'q', 'services/recherche/google?q={k}'), - // Seznam - 'search.seznam.cz' => array('Seznam', 'q', '?q={k}'), + // Sogou + 'www.sogou.com' => array('Sogou', 'query', 'web?query={k}'), - // Sharelook - 'www.sharelook.fr' => array('Sharelook', 'keyword'), + // Softonic + 'search.softonic.com' => array('Softonic', 'q', 'default/default?q={k}'), - // Skynet - 'www.skynet.be' => array('Skynet', 'q', 'services/recherche/google?q={k}'), + // soso.com + 'www.soso.com' => array('Soso', 'w', 'q?w={k}', 'gb2312'), - // Sogou - 'www.sogou.com' => array('Sogou', 'query', 'web?query={k}'), + // Startpagina + 'startgoogle.startpagina.nl' => array('Startpagina (Google)', 'q', '?q={k}'), - // Softonic - 'search.softonic.com' => array('Softonic', 'q', 'default/default?q={k}'), + // Startsiden + 'www.startsiden.no' => array('Startsiden', 'q', 'sok/index.html?q={k}'), - // soso.com - 'www.soso.com' => array('Soso', 'w', 'q?w={k}', 'gb2312'), + // suche.info + 'suche.info' => array('Suche.info', 'Keywords', 'suche.php?Keywords={k}'), - // Startpagina - 'startgoogle.startpagina.nl'=> array('Startpagina (Google)', 'q', '?q={k}'), + // Suchmaschine.com + 'www.suchmaschine.com' => array('Suchmaschine.com', 'suchstr', 'cgi-bin/wo.cgi?suchstr={k}'), - // Startsiden - 'www.startsiden.no' => array('Startsiden', 'q', 'sok/index.html?q={k}'), + // Suchnase + 'www.suchnase.de' => array('Suchnase', 'q'), - // suche.info - 'suche.info' => array('Suche.info', 'Keywords', 'suche.php?Keywords={k}'), - - // Suchmaschine.com - 'www.suchmaschine.com' => array('Suchmaschine.com', 'suchstr', 'cgi-bin/wo.cgi?suchstr={k}'), + // TalkTalk + 'www.talktalk.co.uk' => array('TalkTalk', 'query', 'search/results.html?query={k}'), - // Suchnase - 'www.suchnase.de' => array('Suchnase', 'q'), + // Technorati + 'technorati.com' => array('Technorati', 'q', 'search?return=sites&authority=all&q={k}'), - // TalkTalk - 'www.talktalk.co.uk' => array('TalkTalk', 'query', 'search/results.html?query={k}'), + // Teoma + 'www.teoma.com' => array('Teoma', 'q', 'web?q={k}'), - // Technorati - 'technorati.com' => array('Technorati', 'q', 'search?return=sites&authority=all&q={k}'), + // Terra -- referer does not contain search phrase (keywords) + 'buscador.terra.es' => array('Terra', 'query', 'Default.aspx?source=Search&query={k}'), + 'buscador.terra.cl' => array('Terra'), + 'buscador.terra.com.br' => array('Terra'), - // Teoma - 'www.teoma.com' => array('Teoma', 'q', 'web?q={k}'), + // Tiscali + 'search.tiscali.it' => array('Tiscali', array('q', 'key'), '?q={k}'), + 'search-dyn.tiscali.it' => array('Tiscali'), + 'hledani.tiscali.cz' => array('Tiscali', 'query'), - // Terra -- referer does not contain search phrase (keywords) - 'buscador.terra.es' => array('Terra', 'query', 'Default.aspx?source=Search&query={k}'), - 'buscador.terra.cl' => array('Terra'), - 'buscador.terra.com.br' => array('Terra'), + // Tixuma + 'www.tixuma.de' => array('Tixuma', 'sc', 'index.php?mp=search&stp=&sc={k}&tg=0'), - // Tiscali - 'search.tiscali.it' => array('Tiscali', array('q', 'key'), '?q={k}'), - 'search-dyn.tiscali.it' => array('Tiscali'), - 'hledani.tiscali.cz' => array('Tiscali', 'query'), + // T-Online + 'suche.t-online.de' => array('T-Online', 'q', 'fast-cgi/tsc?mandant=toi&context=internet-tab&q={k}'), + 'brisbane.t-online.de' => array('T-Online'), + 'navigationshilfe.t-online.de' => array('T-Online', 'q', 'dtag/dns/results?mode=search_top&q={k}'), - // Tixuma - 'www.tixuma.de' => array('Tixuma', 'sc', 'index.php?mp=search&stp=&sc={k}&tg=0'), + // Toolbarhome + 'www.toolbarhome.com' => array('Toolbarhome', 'q', 'search.aspx?q={k}'), - // T-Online - 'suche.t-online.de' => array('T-Online', 'q', 'fast-cgi/tsc?mandant=toi&context=internet-tab&q={k}'), - 'brisbane.t-online.de' => array('T-Online'), - 'navigationshilfe.t-online.de'=> array('T-Online', 'q', 'dtag/dns/results?mode=search_top&q={k}'), + 'vshare.toolbarhome.com' => array('Toolbarhome'), - // Toolbarhome - 'www.toolbarhome.com' => array('Toolbarhome', 'q', 'search.aspx?q={k}'), - - 'vshare.toolbarhome.com' => array('Toolbarhome'), + // Trouvez.com + 'www.trouvez.com' => array('Trouvez.com', 'query'), - // Trouvez.com - 'www.trouvez.com' => array('Trouvez.com', 'query'), + // TrovaRapido + 'www.trovarapido.com' => array('TrovaRapido', 'q', 'result.php?q={k}'), - // TrovaRapido - 'www.trovarapido.com' => array('TrovaRapido', 'q', 'result.php?q={k}'), - - // Trusted-Search - 'www.trusted--search.com' => array('Trusted Search', 'w', 'search?w={k}'), + // Trusted-Search + 'www.trusted--search.com' => array('Trusted Search', 'w', 'search?w={k}'), - // Twingly - 'www.twingly.com' => array('Twingly', 'q', 'search?q={k}'), + // Twingly + 'www.twingly.com' => array('Twingly', 'q', 'search?q={k}'), - // uol.com.br - 'busca.uol.com.br' => array('uol.com.br', 'q', '/web/?q={k}'), + // uol.com.br + 'busca.uol.com.br' => array('uol.com.br', 'q', '/web/?q={k}'), - // URL.ORGanzier - 'www.url.org' => array('URL.ORGanzier', 'q', '?l=de&q={k}'), + // URL.ORGanzier + 'www.url.org' => array('URL.ORGanzier', 'q', '?l=de&q={k}'), - // Vinden - 'www.vinden.nl' => array('Vinden', 'q', '?q={k}'), + // Vinden + 'www.vinden.nl' => array('Vinden', 'q', '?q={k}'), - // Vindex - 'www.vindex.nl' => array('Vindex', 'search_for', '/web?search_for={k}'), - 'search.vindex.nl' => array('Vindex'), + // Vindex + 'www.vindex.nl' => array('Vindex', 'search_for', '/web?search_for={k}'), + 'search.vindex.nl' => array('Vindex'), - // Virgilio - 'ricerca.virgilio.it' => array('Virgilio', 'qs', 'ricerca?qs={k}'), - 'ricercaimmagini.virgilio.it'=> array('Virgilio'), - 'ricercavideo.virgilio.it' => array('Virgilio'), - 'ricercanews.virgilio.it' => array('Virgilio'), - 'mobile.virgilio.it' => array('Virgilio', 'qrs'), + // Virgilio + 'ricerca.virgilio.it' => array('Virgilio', 'qs', 'ricerca?qs={k}'), + 'ricercaimmagini.virgilio.it' => array('Virgilio'), + 'ricercavideo.virgilio.it' => array('Virgilio'), + 'ricercanews.virgilio.it' => array('Virgilio'), + 'mobile.virgilio.it' => array('Virgilio', 'qrs'), - // Voila - 'search.ke.voila.fr' => array('Voila', 'rdata', 'S/voila?rdata={k}'), - 'www.lemoteur.fr' => array('Voila'), // uses voila search + // Voila + 'search.ke.voila.fr' => array('Voila', 'rdata', 'S/voila?rdata={k}'), + 'www.lemoteur.fr' => array('Voila'), // uses voila search - // Volny - 'web.volny.cz' => array('Volny', 'search', 'fulltext/?search={k}', 'windows-1250'), + // Volny + 'web.volny.cz' => array('Volny', 'search', 'fulltext/?search={k}', 'windows-1250'), - // Walhello - 'www.walhello.info' => array('Walhello', 'key', 'search?key={k}'), - 'www.walhello.com' => array('Walhello'), - 'www.walhello.de' => array('Walhello'), - 'www.walhello.nl' => array('Walhello'), + // Walhello + 'www.walhello.info' => array('Walhello', 'key', 'search?key={k}'), + 'www.walhello.com' => array('Walhello'), + 'www.walhello.de' => array('Walhello'), + 'www.walhello.nl' => array('Walhello'), - // Web.de - 'suche.web.de' => array('Web.de', array('su', 'q'), 'search/web/?su={k}'), + // Web.de + 'suche.web.de' => array('Web.de', array('su', 'q'), 'search/web/?su={k}'), - // Web.nl - 'www.web.nl' => array('Web.nl', 'zoekwoord'), + // Web.nl + 'www.web.nl' => array('Web.nl', 'zoekwoord'), - // Weborama - 'www.weborama.fr' => array('weborama', 'QUERY'), + // Weborama + 'www.weborama.fr' => array('weborama', 'QUERY'), - // WebSearch - 'www.websearch.com' => array('WebSearch', array('qkw', 'q'), 'search/results2.aspx?q={k}'), + // WebSearch + 'www.websearch.com' => array('WebSearch', array('qkw', 'q'), 'search/results2.aspx?q={k}'), - // Wedoo - // 2011-02-15 - keyword no longer appears to be in Referer URL; candidate for removal? - 'fr.wedoo.com' => array('Wedoo', 'keyword'), - 'en.wedoo.com' => array('Wedoo'), - 'es.wedoo.com' => array('Wedoo'), + // Wedoo + // 2011-02-15 - keyword no longer appears to be in Referer URL; candidate for removal? + 'fr.wedoo.com' => array('Wedoo', 'keyword'), + 'en.wedoo.com' => array('Wedoo'), + 'es.wedoo.com' => array('Wedoo'), - // Winamp (Enhanced by Google) - 'search.winamp.com' => array('Winamp', 'q', 'search/search?q={k}'), + // Winamp (Enhanced by Google) + 'search.winamp.com' => array('Winamp', 'q', 'search/search?q={k}'), - // Witch - 'www.witch.de' => array('Witch', 'search', 'search-result.php?cn=0&search={k}'), + // Witch + 'www.witch.de' => array('Witch', 'search', 'search-result.php?cn=0&search={k}'), - // Wirtualna Polska - 'szukaj.wp.pl' => array('Wirtualna Polska', 'szukaj', 'http://szukaj.wp.pl/szukaj.html?szukaj={k}'), + // Wirtualna Polska + 'szukaj.wp.pl' => array('Wirtualna Polska', 'szukaj', 'http://szukaj.wp.pl/szukaj.html?szukaj={k}'), - // WWW - 'search.www.ee' => array('www värav', 'query'), + // WWW + 'search.www.ee' => array('www värav', 'query'), - // X-recherche - 'www.x-recherche.com' => array('X-Recherche', 'MOTS', 'cgi-bin/websearch?MOTS={k}'), + // X-recherche + 'www.x-recherche.com' => array('X-Recherche', 'MOTS', 'cgi-bin/websearch?MOTS={k}'), - // Yahoo - 'search.yahoo.com' => array('Yahoo!', array('p', 'q'), 'search?p={k}'), + // Yahoo + 'search.yahoo.com' => array('Yahoo!', array('p', 'q'), 'search?p={k}'), // '*.search.yahoo.com' => array('Yahoo!'), // see built-in helper in Common.php - 'yahoo.com' => array('Yahoo!'), - 'yahoo.{}' => array('Yahoo!'), - '{}.yahoo.com' => array('Yahoo!'), - 'cade.yahoo.com' => array('Yahoo!'), - 'espanol.yahoo.com' => array('Yahoo!'), - 'qc.yahoo.com' => array('Yahoo!'), - 'one.cn.yahoo.com' => array('Yahoo!'), - - // Powered by Yahoo APIs - 'www.cercato.it' => array('Yahoo!', 'q'), - 'search.offerbox.com' => array('Yahoo!', 'q'), - - // Powered by Yahoo! Search Marketing (Overture) - 'ys.mirostart.com' => array('Yahoo!', 'q'), - - // Yahoo! Directory - 'search.yahoo.com/search/dir' => array('Yahoo! Directory', 'p', '?p={k}'), + 'yahoo.com' => array('Yahoo!'), + 'yahoo.{}' => array('Yahoo!'), + '{}.yahoo.com' => array('Yahoo!'), + 'cade.yahoo.com' => array('Yahoo!'), + 'espanol.yahoo.com' => array('Yahoo!'), + 'qc.yahoo.com' => array('Yahoo!'), + 'one.cn.yahoo.com' => array('Yahoo!'), + + // Powered by Yahoo APIs + 'www.cercato.it' => array('Yahoo!', 'q'), + 'search.offerbox.com' => array('Yahoo!', 'q'), + + // Powered by Yahoo! Search Marketing (Overture) + 'ys.mirostart.com' => array('Yahoo!', 'q'), + + // Yahoo! Directory + 'search.yahoo.com/search/dir' => array('Yahoo! Directory', 'p', '?p={k}'), // '{}.dir.yahoo.com' => array('Yahoo! Directory'), - // Yahoo! Images - 'images.search.yahoo.com' => array('Yahoo! Images', 'p', 'search/images?p={k}'), + // Yahoo! Images + 'images.search.yahoo.com' => array('Yahoo! Images', 'p', 'search/images?p={k}'), // '*.images.search.yahoo.com'=> array('Yahoo! Images'), // see built-in helper in Common.php - '{}.images.yahoo.com' => array('Yahoo! Images'), - 'cade.images.yahoo.com' => array('Yahoo! Images'), - 'espanol.images.yahoo.com' => array('Yahoo! Images'), - 'qc.images.yahoo.com' => array('Yahoo! Images'), - - // Yam - 'search.yam.com' => array('Yam', 'k', 'Search/Web/?SearchType=web&k={k}'), - - // Yandex - 'yandex.ru' => array('Yandex', 'text', 'yandsearch?text={k}'), - 'yandex.com' => array('Yandex'), - 'yandex.{}' => array('Yandex'), - - // Yandex Images - 'images.yandex.ru' => array('Yandex Images', 'text', 'yandsearch?text={k}'), - 'images.yandex.com' => array('Yandex Images'), - 'images.yandex.{}' => array('Yandex Images'), - - // Yasni - 'www.yasni.de' => array('Yasni', 'query'), - 'www.yasni.com' => array('Yasni'), - 'www.yasni.co.uk' => array('Yasni'), - 'www.yasni.ch' => array('Yasni'), - 'www.yasni.at' => array('Yasni'), - - // Yatedo - 'www.yatedo.com' => array('Yatedo', 'q', 'search/profil?q={k}'), - 'www.yatedo.fr' => array('Yatedo'), - - // Yellowmap - 'yellowmap.de' => array('Yellowmap', ' '), - - // Yippy - 'search.yippy.com' => array('Yippy', 'query', 'search?query={k}'), - - // YouGoo - 'www.yougoo.fr' => array('YouGoo', 'q', '?cx=search&q={k}'), - - // Zapmeta - 'www.zapmeta.com' => array('Zapmeta', array('q', 'query'), '?q={k}'), - 'www.zapmeta.nl' => array('Zapmeta'), - 'www.zapmeta.de' => array('Zapmeta'), - 'uk.zapmeta.com' => array('Zapmeta'), - - // Zoek - 'www3.zoek.nl' => array('Zoek', 'q'), - - // Zhongsou - 'p.zhongsou.com' => array('Zhongsou', 'w', 'p?w={k}'), - - // Zoeken - 'www.zoeken.nl' => array('Zoeken', 'q', '?q={k}'), - - // Zoohoo - 'zoohoo.cz' => array('Zoohoo', 'q', '?q={k}', 'windows-1250'), - - // Zoznam - 'www.zoznam.sk' => array('Zoznam', 's', 'hladaj.fcgi?s={k}&co=svet'), - ); - - $GLOBALS['Piwik_SearchEngines_NameToUrl'] = array(); - foreach($GLOBALS['Piwik_SearchEngines'] as $url => $info) - { - if(!isset($GLOBALS['Piwik_SearchEngines_NameToUrl'][$info[0]])) - { - $GLOBALS['Piwik_SearchEngines_NameToUrl'][$info[0]] = $url; - } - } + '{}.images.yahoo.com' => array('Yahoo! Images'), + 'cade.images.yahoo.com' => array('Yahoo! Images'), + 'espanol.images.yahoo.com' => array('Yahoo! Images'), + 'qc.images.yahoo.com' => array('Yahoo! Images'), + + // Yam + 'search.yam.com' => array('Yam', 'k', 'Search/Web/?SearchType=web&k={k}'), + + // Yandex + 'yandex.ru' => array('Yandex', 'text', 'yandsearch?text={k}'), + 'yandex.com' => array('Yandex'), + 'yandex.{}' => array('Yandex'), + + // Yandex Images + 'images.yandex.ru' => array('Yandex Images', 'text', 'yandsearch?text={k}'), + 'images.yandex.com' => array('Yandex Images'), + 'images.yandex.{}' => array('Yandex Images'), + + // Yasni + 'www.yasni.de' => array('Yasni', 'query'), + 'www.yasni.com' => array('Yasni'), + 'www.yasni.co.uk' => array('Yasni'), + 'www.yasni.ch' => array('Yasni'), + 'www.yasni.at' => array('Yasni'), + + // Yatedo + 'www.yatedo.com' => array('Yatedo', 'q', 'search/profil?q={k}'), + 'www.yatedo.fr' => array('Yatedo'), + + // Yellowmap + 'yellowmap.de' => array('Yellowmap', ' '), + + // Yippy + 'search.yippy.com' => array('Yippy', 'query', 'search?query={k}'), + + // YouGoo + 'www.yougoo.fr' => array('YouGoo', 'q', '?cx=search&q={k}'), + + // Zapmeta + 'www.zapmeta.com' => array('Zapmeta', array('q', 'query'), '?q={k}'), + 'www.zapmeta.nl' => array('Zapmeta'), + 'www.zapmeta.de' => array('Zapmeta'), + 'uk.zapmeta.com' => array('Zapmeta'), + + // Zoek + 'www3.zoek.nl' => array('Zoek', 'q'), + + // Zhongsou + 'p.zhongsou.com' => array('Zhongsou', 'w', 'p?w={k}'), + + // Zoeken + 'www.zoeken.nl' => array('Zoeken', 'q', '?q={k}'), + + // Zoohoo + 'zoohoo.cz' => array('Zoohoo', 'q', '?q={k}', 'windows-1250'), + + // Zoznam + 'www.zoznam.sk' => array('Zoznam', 's', 'hladaj.fcgi?s={k}&co=svet'), + ); + + $GLOBALS['Piwik_SearchEngines_NameToUrl'] = array(); + foreach ($GLOBALS['Piwik_SearchEngines'] as $url => $info) { + if (!isset($GLOBALS['Piwik_SearchEngines_NameToUrl'][$info[0]])) { + $GLOBALS['Piwik_SearchEngines_NameToUrl'][$info[0]] = $url; + } + } } diff --git a/core/DataFiles/Socials.php b/core/DataFiles/Socials.php index 3644d501fe..5299990add 100755 --- a/core/DataFiles/Socials.php +++ b/core/DataFiles/Socials.php @@ -8,200 +8,199 @@ * @category Piwik * @package DataFiles */ - -if (!isset($GLOBALS['Piwik_socialUrl'])) -{ - // Note: the key of the array should have max 3 elements eg. sub.domain.ext - $GLOBALS['Piwik_socialUrl'] = array ( - - // Facebook - 'facebook.com' => 'Facebook', - 'fb.me' => 'Facebook', - - // Ozone - 'qzone.qq.com' => 'Qzone', - - // Haboo - 'habbo.com' => 'Haboo', - - // Twitter - 'twitter.com' => 'Twitter', - 't.co' => 'Twitter', - - // Renren - 'renren.com' => 'Renren', - - // Windows Live Spaces - 'login.live.com' => 'Windows Live Spaces', - - // LinkedIn - 'linkedin.com' => 'LinkedIn', - - // Bebo - 'bebo.com' => 'Bebo', - - // Vkontakte - 'vk.com' => 'Vkontakte', - 'vkontakte.ru' => 'Vkontakte', - - // Tagged - 'login.tagged.com' => 'Tagged', - - // Orkut - 'orkut.com' => 'Orkut', - - // Myspace - 'myspace.com' => 'Myspace', - - // Frinedster - 'friendster.com' => 'Friendster', - - // Badoo - 'badoo.com' => 'Badoo', - - // hi5 - 'hi5.com' => 'hi5', - - // Netlog - 'netlog.com' => 'Netlog', - - // Flixster - 'flixster.com' => 'Flixster', - - // MyLife - 'mylife.ru' => 'MyLife', - - // Classmates.com - 'classmates.com' => 'Classmates.com', - - // Github - 'github.com' => 'Github', - - // Google+ - 'url.google.com' => 'Google+', - - // douban - 'douban.com' => 'douban', - - // Odnoklassniki - 'odnoklassniki.ru' => 'Odnoklassniki', - - // Viadeo - 'viadeo.com' => 'Viadeo', - - // Flickr - 'flickr.com' => 'Flickr', - - // WeeWorld - 'weeworld.com' => 'WeeWorld', - - // Last.fm - 'lastfm.ru' => 'Last.fm', - - // MyHeritage - 'myheritage.com' => 'MyHeritage', - - // Xanga - 'xanga.com' => 'Xanga', - - // Mixi - 'mixi.jp' => 'Mixi', - - // Cyworld - 'global.cyworld.com' => 'Cyworld', - - // Gaia Online - 'gaiaonline.com' => 'Gaia Online', - - // Skyrock - 'skyrock.com' => 'Skyrock', - - // BlackPlanet - 'blackplanet.com' => 'BlackPlanet', - - // myYearbook - 'myyearbook.com' => 'myYearbook', - - // Fotolog - 'fotolog.com' => 'Fotolog', - - // Friends Reunited - 'friendsreunited.com' => 'Friends Reunited', - - // LiveJournal - 'livejournal.ru' => 'LiveJournal', - - // StudiVZ - 'studivz.net' => 'StudiVZ', - - // StackOverflow - 'stackoverflow.com' => 'StackOverflow', - - // Sonico.com - 'sonico.com' => 'Sonico.com', - - // Pinterest - 'pinterest.com' => 'Pinterest', - - // Plaxo - 'plaxo.com' => 'Plaxo', - - // Geni.com - 'geni.com' => 'Geni.com', - - // Tuenti - 'tuenti.com' => 'Tuenti', - - // XING - 'xing.com' => 'XING', - - // Taringa! - 'taringa.net' => 'Taringa!', - - // Nasza-klasa.pl - 'nk.pl' => 'Nasza-klasa.pl', - - // StumbleUpon - 'stumbleupon.com' => 'StumbleUpon', - - // Sourceforge - 'sourceforge.net' => 'SourceForge', - - // Hyves - 'hyves.nl' => 'Hyves', - - // WAYN - 'wayn.com' => 'WAYN', - - // Buzznet - 'buzznet.com' => 'Buzznet', - - // Multiply - 'multiply.com' => 'Multiply', - - // Foursquare - 'ru.foursquare.com' => 'Foursquare', - - // vkrugudruzei.ru - 'vkrugudruzei.ru' => 'vkrugudruzei.ru', - - // my.mail.ru - 'my.mail.ru' => 'my.mail.ru', - - //MoiKrug.ru - 'moikrug.ru' => 'my.mail.ru', - - // Reddit - 'reddit.com' => 'reddit', - - // HackerNews - 'news.ycombinator.com' => 'Hacker News', - - // Identi.ca - 'identi.ca' => 'identi.ca', - - // Weibo - 'weibo.com' => 'Weibo', - 't.cn' => 'Weibo', + +if (!isset($GLOBALS['Piwik_socialUrl'])) { + // Note: the key of the array should have max 3 elements eg. sub.domain.ext + $GLOBALS['Piwik_socialUrl'] = array( + + // Facebook + 'facebook.com' => 'Facebook', + 'fb.me' => 'Facebook', + + // Ozone + 'qzone.qq.com' => 'Qzone', + + // Haboo + 'habbo.com' => 'Haboo', + + // Twitter + 'twitter.com' => 'Twitter', + 't.co' => 'Twitter', + + // Renren + 'renren.com' => 'Renren', + + // Windows Live Spaces + 'login.live.com' => 'Windows Live Spaces', + + // LinkedIn + 'linkedin.com' => 'LinkedIn', + + // Bebo + 'bebo.com' => 'Bebo', + + // Vkontakte + 'vk.com' => 'Vkontakte', + 'vkontakte.ru' => 'Vkontakte', + + // Tagged + 'login.tagged.com' => 'Tagged', + + // Orkut + 'orkut.com' => 'Orkut', + + // Myspace + 'myspace.com' => 'Myspace', + + // Frinedster + 'friendster.com' => 'Friendster', + + // Badoo + 'badoo.com' => 'Badoo', + + // hi5 + 'hi5.com' => 'hi5', + + // Netlog + 'netlog.com' => 'Netlog', + + // Flixster + 'flixster.com' => 'Flixster', + + // MyLife + 'mylife.ru' => 'MyLife', + + // Classmates.com + 'classmates.com' => 'Classmates.com', + + // Github + 'github.com' => 'Github', + + // Google+ + 'url.google.com' => 'Google+', + + // douban + 'douban.com' => 'douban', + + // Odnoklassniki + 'odnoklassniki.ru' => 'Odnoklassniki', + + // Viadeo + 'viadeo.com' => 'Viadeo', + + // Flickr + 'flickr.com' => 'Flickr', + + // WeeWorld + 'weeworld.com' => 'WeeWorld', + + // Last.fm + 'lastfm.ru' => 'Last.fm', + + // MyHeritage + 'myheritage.com' => 'MyHeritage', + + // Xanga + 'xanga.com' => 'Xanga', + + // Mixi + 'mixi.jp' => 'Mixi', + + // Cyworld + 'global.cyworld.com' => 'Cyworld', + + // Gaia Online + 'gaiaonline.com' => 'Gaia Online', + + // Skyrock + 'skyrock.com' => 'Skyrock', + + // BlackPlanet + 'blackplanet.com' => 'BlackPlanet', + + // myYearbook + 'myyearbook.com' => 'myYearbook', + + // Fotolog + 'fotolog.com' => 'Fotolog', + + // Friends Reunited + 'friendsreunited.com' => 'Friends Reunited', + + // LiveJournal + 'livejournal.ru' => 'LiveJournal', + + // StudiVZ + 'studivz.net' => 'StudiVZ', + + // StackOverflow + 'stackoverflow.com' => 'StackOverflow', + + // Sonico.com + 'sonico.com' => 'Sonico.com', + + // Pinterest + 'pinterest.com' => 'Pinterest', + + // Plaxo + 'plaxo.com' => 'Plaxo', + + // Geni.com + 'geni.com' => 'Geni.com', + + // Tuenti + 'tuenti.com' => 'Tuenti', + + // XING + 'xing.com' => 'XING', + + // Taringa! + 'taringa.net' => 'Taringa!', + + // Nasza-klasa.pl + 'nk.pl' => 'Nasza-klasa.pl', + + // StumbleUpon + 'stumbleupon.com' => 'StumbleUpon', + + // Sourceforge + 'sourceforge.net' => 'SourceForge', + + // Hyves + 'hyves.nl' => 'Hyves', + + // WAYN + 'wayn.com' => 'WAYN', + + // Buzznet + 'buzznet.com' => 'Buzznet', + + // Multiply + 'multiply.com' => 'Multiply', + + // Foursquare + 'ru.foursquare.com' => 'Foursquare', + + // vkrugudruzei.ru + 'vkrugudruzei.ru' => 'vkrugudruzei.ru', + + // my.mail.ru + 'my.mail.ru' => 'my.mail.ru', + + //MoiKrug.ru + 'moikrug.ru' => 'my.mail.ru', + + // Reddit + 'reddit.com' => 'reddit', + + // HackerNews + 'news.ycombinator.com' => 'Hacker News', + + // Identi.ca + 'identi.ca' => 'identi.ca', + + // Weibo + 'weibo.com' => 'Weibo', + 't.cn' => 'Weibo', ); } diff --git a/core/DataTable.php b/core/DataTable.php index e12e5347b6..ad27bc4a71 100644 --- a/core/DataTable.php +++ b/core/DataTable.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,1528 +15,1415 @@ require_once PIWIK_INCLUDE_PATH . '/core/Common.php'; /** - * + * * ---- DataTable * A DataTable is a data structure used to store complex tables of data. - * + * * A DataTable is composed of multiple DataTable_Row. * A DataTable can be applied one or several DataTable_Filter. * A DataTable can be given to a DataTable_Renderer that would export the data under a given format (XML, HTML, etc.). - * + * * A DataTable has the following features: * - serializable to be stored in the DB * - loadable from the serialized version * - efficient way of loading data from an external source (from a PHP array structure) * - very simple interface to get data from the table - * + * * ---- DataTable_Row * A DataTableRow in the table is defined by * - multiple columns (a label, multiple values, ...) * - optional metadata * - optional - a sub DataTable associated to this row - * + * * Simple row example: - * - columns = array( 'label' => 'Firefox', - * 'visitors' => 155, - * 'pages' => 214, - * 'bounce_rate' => 67) + * - columns = array( 'label' => 'Firefox', + * 'visitors' => 155, + * 'pages' => 214, + * 'bounce_rate' => 67) * - metadata = array('logo' => '/img/browsers/FF.png') * - no sub DataTable - * + * * A more complex example would be a DataTable_Row that is associated to a sub DataTable. - * For example, for the row of the search engine Google, + * For example, for the row of the search engine Google, * we want to get the list of keywords associated, with their statistics. * - columns = array( 'label' => 'Google', - * 'visits' => 1550, - * 'visits_length' => 514214, - * 'returning_visits' => 77) - * - metadata = array( 'logo' => '/img/search/google.png', - * 'url' => 'http://google.com') + * 'visits' => 1550, + * 'visits_length' => 514214, + * 'returning_visits' => 77) + * - metadata = array( 'logo' => '/img/search/google.png', + * 'url' => 'http://google.com') * - DataTable = DataTable containing several DataTable_Row containing the keywords information for this search engine - * Example of one DataTable_Row - * - the keyword columns specific to this search engine = - * array( 'label' => 'Piwik', // the keyword - * 'visitors' => 155, // Piwik has been searched on Google by 155 visitors - * 'pages' => 214 // Visitors coming from Google with the kwd Piwik have seen 214 pages - * ) - * - the keyword metadata = array() // nothing here, but we could imagining storing the URL of the search in Google for example - * - no subTable - * - * + * Example of one DataTable_Row + * - the keyword columns specific to this search engine = + * array( 'label' => 'Piwik', // the keyword + * 'visitors' => 155, // Piwik has been searched on Google by 155 visitors + * 'pages' => 214 // Visitors coming from Google with the kwd Piwik have seen 214 pages + * ) + * - the keyword metadata = array() // nothing here, but we could imagining storing the URL of the search in Google for example + * - no subTable + * + * * ---- DataTable_Filter - * A DataTable_Filter is a applied to a DataTable and so + * A DataTable_Filter is a applied to a DataTable and so * can filter information in the multiple DataTable_Row. - * + * * For example a DataTable_Filter can: - * - remove rows from the table, - * for example the rows' labels that do not match a given searched pattern - * for example the rows' values that are less than a given percentage (low population) - * - return a subset of the DataTable - * for example a function that apply a limit: $offset, $limit + * - remove rows from the table, + * for example the rows' labels that do not match a given searched pattern + * for example the rows' values that are less than a given percentage (low population) + * - return a subset of the DataTable + * for example a function that apply a limit: $offset, $limit * - add / remove columns - * for example adding a column that gives the percentage of a given value + * for example adding a column that gives the percentage of a given value * - add some metadata - * for example the 'logo' path if the filter detects the logo + * for example the 'logo' path if the filter detects the logo * - edit the value, the label * - change the rows order - * for example if we want to sort by Label alphabetical order, or by any column value - * + * for example if we want to sort by Label alphabetical order, or by any column value + * * When several DataTable_Filter are to be applied to a DataTable they are applied sequentially. - * A DataTable_Filter is assigned a priority. - * For example, filters that - * - sort rows should be applied with the highest priority - * - remove rows should be applied with a high priority as they prune the data and improve performance. - * + * A DataTable_Filter is assigned a priority. + * For example, filters that + * - sort rows should be applied with the highest priority + * - remove rows should be applied with a high priority as they prune the data and improve performance. + * * ---- Code example - * + * * $table = new DataTable(); * $table->addRowsFromArray( array(...) ); - * + * * # sort the table by visits asc * $filter = new DataTable_Filter_Sort( $table, 'visits', 'asc'); * $tableFiltered = $filter->getTableFiltered(); - * + * * # add a filter to select only the website with a label matching '*.com' (regular expression) * $filter = new DataTable_Filter_Pattern( $table, 'label', '*(.com)'); * $tableFiltered = $filter->getTableFiltered(); - * + * * # keep the 20 elements from offset 15 * $filter = new DataTable_Filter_Limit( $tableFiltered, 15, 20); * $tableFiltered = $filter->getTableFiltered(); - * + * * # add a column computing the percentage of visits * # params = table, column containing the value, new column name to add, number of total visits to use to compute the % * $filter = new DataTable_Filter_AddColumnPercentage( $tableFiltered, 'visits', 'visits_percentage', 2042); * $tableFiltered = $filter->getTableFiltered(); - * + * * # we get the table as XML * $xmlOutput = new DataTable_Exporter_Xml( $table ); * $xmlOutput->setHeader( ... ); * $xmlOutput->setColumnsToExport( array('visits', 'visits_percent', 'label') ); * $XMLstring = $xmlOutput->getOutput(); - * - * + * + * * ---- Other (ideas) * We can also imagine building a DataTable_Compare which would take N DataTable that have the same * structure and would compare them, by computing the percentages of differences, etc. - * - * For example + * + * For example * DataTable1 = [ keyword1, 1550 visits] - * [ keyword2, 154 visits ] + * [ keyword2, 154 visits ] * DataTable2 = [ keyword1, 1004 visits ] - * [ keyword3, 659 visits ] + * [ keyword3, 659 visits ] * DataTable_Compare = result of comparison of table1 with table2 - * [ keyword1, +154% ] - * [ keyword2, +1000% ] - * [ keyword3, -430% ] - * + * [ keyword1, +154% ] + * [ keyword2, +1000% ] + * [ keyword3, -430% ] + * * @see Piwik_DataTable_Row A Piwik_DataTable is composed of Piwik_DataTable_Row - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable { - /** Name for metadata that describes when a report was archived. */ - const ARCHIVED_DATE_METADATA_NAME = 'archived_date'; - const MAX_DEPTH_DEFAULT = 15; - /** Name for metadata that describes which columns are empty and should not be shown. */ - const EMPTY_COLUMNS_METADATA_NAME = 'empty_column'; - - /** - * Maximum nesting level. - */ - static private $maximumDepthLevelAllowed = self::MAX_DEPTH_DEFAULT; - - /** - * Array of Piwik_DataTable_Row - * - * @var Piwik_DataTable_Row[] - */ - protected $rows = array(); - - /** - * Array of parent IDs - * - * @var array - */ - protected $parents = null; - - /** - * Id assigned to the DataTable, used to lookup the table using the DataTable_Manager - * - * @var int - */ - protected $currentId; - - /** - * Current depth level of this data table - * 0 is the parent data table - * - * @var int - */ - protected $depthLevel = 0; - - /** - * This flag is set to false once we modify the table in a way that outdates the index - * - * @var bool - */ - protected $indexNotUpToDate = true; - - /** - * This flag sets the index to be rebuild whenever a new row is added, - * as opposed to re-building the full index when getRowFromLabel is called. - * This is to optimize and not rebuild the full Index in the case where we - * add row, getRowFromLabel, addRow, getRowFromLabel thousands of times. - * - * @var bool - */ - protected $rebuildIndexContinuously = false; - - /** - * Column name of last time the table was sorted - * - * @var string - */ - protected $tableSortedBy = false; - - /** - * List of Piwik_DataTable_Filter queued to this table - * - * @var array - */ - protected $queuedFilters = array(); - - /** - * We keep track of the number of rows before applying the LIMIT filter that deletes some rows - * - * @var int - */ - protected $rowsCountBeforeLimitFilter = 0; - - /** - * Defaults to false for performance reasons (most of the time we don't need recursive sorting so we save a looping over the dataTable) - * - * @var bool - */ - protected $enableRecursiveSort = false; - - /** - * When the table and all subtables are loaded, this flag will be set to true to ensure filters are applied to all subtables - * - * @var bool - */ - protected $enableRecursiveFilters = false; - - /** - * @var array - */ - protected $rowsIndexByLabel = array(); - - /** - * @var Piwik_DataTable_Row - */ - protected $summaryRow = null; - - /** - * Table metadata. - * - * @var array - */ - public $metadata = array(); - - /** - * Maximum number of rows allowed in this datatable (including the summary row). - * If adding more rows is attempted, the extra rows get summed to the summary row. - * - * @var int - */ - protected $maximumAllowedRows = 0; - - const ID_SUMMARY_ROW = -1; - const LABEL_SUMMARY_ROW = -1; - const ID_PARENTS = -2; - - /** - * Builds the DataTable, registers itself to the manager - * - */ - public function __construct() - { - $this->currentId = Piwik_DataTable_Manager::getInstance()->addTable($this); - } - - /** - * At destruction we free all memory - */ - public function __destruct() - { - static $depth = 0; - // destruct can be called several times - if($depth < self::$maximumDepthLevelAllowed - && isset($this->rows)) - { - $depth++; - foreach($this->getRows() as $row) { - destroy($row); - } - unset($this->rows); - Piwik_DataTable_Manager::getInstance()->setTableDeleted($this->getId()); - $depth--; - } - } - - /** - * Sort the dataTable rows using the php callback function - * - * @param string $functionCallback - * @param string $columnSortedBy The column name. Used to then ask the datatable what column are you sorted by - */ - public function sort( $functionCallback, $columnSortedBy ) - { - $this->indexNotUpToDate = true; - $this->tableSortedBy = $columnSortedBy; - usort( $this->rows, $functionCallback ); - - if($this->enableRecursiveSort === true) - { - foreach($this->getRows() as $row) - { - if(($idSubtable = $row->getIdSubDataTable()) !== null) - { - $table = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable); - $table->enableRecursiveSort(); - $table->sort($functionCallback, $columnSortedBy); - } - } - } - } - - /** - * Returns the name of the column the tables is sorted by - * - * @return bool|string - */ - public function getSortedByColumnName() - { - return $this->tableSortedBy; - } - - /** - * Enables the recursive sort. Means that when using $table->sort() - * it will also sort all subtables using the same callback - */ - public function enableRecursiveSort() - { - $this->enableRecursiveSort = true; - } - - /** - * Enables the recursive filter. Means that when using $table->filter() - * it will also filter all subtables using the same callback - */ - public function enableRecursiveFilters() - { - $this->enableRecursiveFilters = true; - } - - /** - * Returns the number of rows before we applied the limit filter - * - * @return int - */ - public function getRowsCountBeforeLimitFilter() - { - $toReturn = $this->rowsCountBeforeLimitFilter; - if($toReturn == 0) - { - return $this->getRowsCount(); - } - return $toReturn; - } - - /** - * Saves the current number of rows - */ - function setRowsCountBeforeLimitFilter() - { - $this->rowsCountBeforeLimitFilter = $this->getRowsCount(); - } - - /** - * Apply a filter to this datatable - * - * @param string $className Class name, eg. "Sort" or "Piwik_DataTable_Filter_Sort" - * @param array $parameters Array of parameters to the filter, eg. array('nb_visits', 'asc') - */ - public function filter( $className, $parameters = array() ) - { - if(!class_exists($className, false)) - { - $className = "Piwik_DataTable_Filter_" . $className; - } - $reflectionObj = new ReflectionClass($className); - - // the first parameter of a filter is the DataTable - // we add the current datatable as the parameter - $parameters = array_merge(array($this), $parameters); - - $filter = $reflectionObj->newInstanceArgs($parameters); - - $filter->enableRecursive( $this->enableRecursiveFilters ); - - $filter->filter($this); - } - - /** - * Queue a DataTable_Filter that will be applied when applyQueuedFilters() is called. - * (just before sending the datatable back to the browser (or API, etc.) - * - * @param string $className The class name of the filter, eg. Piwik_DataTable_Filter_Limit - * @param array $parameters The parameters to give to the filter, eg. array( $offset, $limit) for the filter Piwik_DataTable_Filter_Limit - */ - public function queueFilter( $className, $parameters = array() ) - { - if(!is_array($parameters)) - { - $parameters = array($parameters); - } - $this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters); - } - - /** - * Apply all filters that were previously queued to this table - * @see queueFilter() - */ - public function applyQueuedFilters() - { - foreach($this->queuedFilters as $filter) - { - $this->filter($filter['className'], $filter['parameters']); - } - $this->queuedFilters = array(); - } - - /** - * Adds a new DataTable to this DataTable - * Go through all the rows of the new DataTable and applies the algorithm: - * - if a row in $table doesnt exist in $this we add the new row to $this - * - if a row exists in both $table and $this we sum the columns values into $this - * - if a row in $this doesnt exist in $table we add in $this the row of $table without modification - * - * A common row to 2 DataTable is defined by the same label - * - * @example tests/core/DataTable.test.php - * - * @param Piwik_DataTable $tableToSum - */ - public function addDataTable( Piwik_DataTable $tableToSum ) - { - foreach($tableToSum->getRows() as $row) - { - $labelToLookFor = $row->getColumn('label'); - $rowFound = $this->getRowFromLabel( $labelToLookFor ); - if($rowFound === false) - { - if( $labelToLookFor === self::LABEL_SUMMARY_ROW ) - { - $this->addSummaryRow( $row ); - } - else - { - $this->addRow( $row ); - } - } - else - { - $rowFound->sumRow( $row ); - - // if the row to add has a subtable whereas the current row doesn't - // we simply add it (cloning the subtable) - // if the row has the subtable already - // then we have to recursively sum the subtables - if(($idSubTable = $row->getIdSubDataTable()) !== null) - { - $rowFound->sumSubtable( Piwik_DataTable_Manager::getInstance()->getTable($idSubTable) ); - } - } - } - } - - /** - * Returns the Piwik_DataTable_Row that has a column 'label' with the value $label - * - * @param string $label Value of the column 'label' of the row to return - * @return Piwik_DataTable_Row|false The row if found, false otherwise - */ - public function getRowFromLabel( $label ) - { - $rowId = $this->getRowIdFromLabel($label); - if($rowId instanceof Piwik_DataTable_Row) - { - return $rowId; - } - if(is_int($rowId) && isset($this->rows[$rowId])) - { - return $this->rows[$rowId]; - } - return false; - } - - /** - * Returns the row id for the givel label - * - * @param string $label Value of the column 'label' of the row to return - * @return int|Row - */ - public function getRowIdFromLabel($label) - { - $this->rebuildIndexContinuously = true; - if($this->indexNotUpToDate) - { - $this->rebuildIndex(); - } - - if($label === self::LABEL_SUMMARY_ROW - && !is_null($this->summaryRow)) - { - return $this->summaryRow; - } - - $label = (string)$label; - if(!isset($this->rowsIndexByLabel[$label])) - { - return false; - } - return $this->rowsIndexByLabel[$label]; - } - - /** - * Get an empty table with the same properties as this one - * - * @return Piwik_DataTable - */ - public function getEmptyClone() - { - $clone = new Piwik_DataTable; - $clone->queuedFilters = $this->queuedFilters; - $clone->metadata = $this->metadata; - return $clone; - } - - /** - * Rebuilds the index used to lookup a row by label - */ - private function rebuildIndex() - { - foreach($this->rows as $id => $row) - { - $label = $row->getColumn('label'); - if($label !== false) - { - $this->rowsIndexByLabel[$label] = $id; - } - } - $this->indexNotUpToDate = false; - } - - /** - * Returns the ith row in the array - * - * @param int $id - * @return Piwik_DataTable_Row or false if not found - */ - public function getRowFromId($id) - { - if(!isset($this->rows[$id])) - { - if($id == self::ID_SUMMARY_ROW - && !is_null($this->summaryRow)) - { - return $this->summaryRow; - } - return false; - } - return $this->rows[$id]; - } - - /** - * Returns a row that has the subtable ID matching the parameter - * - * @param int $idSubTable - * @return Piwik_DataTable_Row|false if not found - */ - public function getRowFromIdSubDataTable($idSubTable) - { - $idSubTable = (int)$idSubTable; - foreach($this->rows as $row) - { - if($row->getIdSubDataTable() === $idSubTable) - { - return $row; - } - } - return false; - } - - /** - * Add a row to the table and rebuild the index if necessary - * - * @param Piwik_DataTable_Row $row to add at the end of the array - */ - public function addRow( Piwik_DataTable_Row $row ) - { - // if there is a upper limit on the number of allowed rows and the table is full, - // add the new row to the summary row - if ($this->maximumAllowedRows > 0 - && $this->getRowsCount() >= $this->maximumAllowedRows - 1) - { - if ($this->summaryRow === null) // create the summary row if necessary - { - $this->addSummaryRow(new Piwik_DataTable_Row(array( - Piwik_DataTable_Row::COLUMNS => $row->getColumns() - ))); - $this->summaryRow->setColumn('label', self::LABEL_SUMMARY_ROW); - } - else - { - $this->summaryRow->sumRow($row, $enableCopyMetadata = false); - } - return $this->summaryRow; - } - - $this->rows[] = $row; - if(!$this->indexNotUpToDate - && $this->rebuildIndexContinuously) - { - $label = $row->getColumn('label'); - if($label !== false) - { - $this->rowsIndexByLabel[$label] = count($this->rows)-1; - } - $this->indexNotUpToDate = false; - } - return $row; - } - - /** - * Sets the summary row (a dataTable can have only one summary row) - * - * @param Piwik_DataTable_Row $row - * @return Piwik_DataTable_Row Returns $row. - */ - public function addSummaryRow( Piwik_DataTable_Row $row ) - { - $this->summaryRow = $row; - return $row; - } - - /** - * Returns the dataTable ID - * - * @return int - */ - public function getId() - { - return $this->currentId; - } - - /** - * Adds a new row from a PHP array data structure - * - * @param array $row eg. array(Piwik_DataTable_Row::COLUMNS => array( 'visits' => 13, 'test' => 'toto'),) - */ - public function addRowFromArray( $row ) - { - $this->addRowsFromArray(array($row)); - } - - /** - * Adds a new row a PHP array data structure - * - * @param array $row eg. array('name' => 'google analytics', 'license' => 'commercial') - */ - public function addRowFromSimpleArray( $row ) - { - $this->addRowsFromSimpleArray(array($row)); - } - - /** - * Returns the array of Piwik_DataTable_Row - * - * @return Piwik_DataTable_Row[] - */ - public function getRows() - { - if(is_null($this->summaryRow)) - { - return $this->rows; - } - else - { - return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow); - } - } - - /** - * Returns the array containing all rows values for the requested column - * - * @param string $name - * @return array - */ - public function getColumn( $name ) - { - $columnValues = array(); - foreach($this->getRows() as $row) - { - $columnValues[] = $row->getColumn($name); - } - return $columnValues; - } - - /** - * Returns an array containing the rows Metadata values - * - * @param string $name Metadata column to return - * @return array - */ - public function getRowsMetadata( $name ) - { - $metadataValues = array(); - foreach($this->getRows() as $row) - { - $metadataValues[] = $row->getMetadata($name); - } - return $metadataValues; - } - - /** - * Returns the number of rows in the table - * - * @return int - */ - public function getRowsCount() - { - if(is_null($this->summaryRow)) - { - return count($this->rows); - } - else - { - return count($this->rows) + 1; - } - } - - /** - * Returns the first row of the DataTable - * - * @return Piwik_DataTable_Row - */ - public function getFirstRow() - { - if(count($this->rows) == 0) - { - if(!is_null($this->summaryRow)) - { - return $this->summaryRow; - } - return false; - } - $row = array_slice($this->rows, 0, 1); - return $row[0]; - } - - /** - * Returns the last row of the DataTable - * - * @return Piwik_DataTable_Row - */ - public function getLastRow() - { - if(!is_null($this->summaryRow)) - { - return $this->summaryRow; - } - - if(count($this->rows) == 0) - { - return false; - } - $row = array_slice($this->rows, -1); - return $row[0]; - } - - /** - * Returns the sum of the number of rows of all the subtables - * + the number of rows in the parent table - * - * @return int - */ - public function getRowsCountRecursive() - { - $totalCount = 0; - foreach($this->rows as $row) - { - if(($idSubTable = $row->getIdSubDataTable()) !== null) - { - $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); - $count = $subTable->getRowsCountRecursive(); - $totalCount += $count; - } - } - - $totalCount += $this->getRowsCount(); - return $totalCount; - } - - /** - * Delete a given column $name in all the rows - * - * @param string $name - */ - public function deleteColumn( $name ) - { - $this->deleteColumns(array($name)); - } - - public function __sleep() - { - return array('rows', 'parents', 'summaryRow'); - } - - /** - * Rename a column in all rows - * - * @param string $oldName Old column name - * @param string $newName New column name - */ - public function renameColumn( $oldName, $newName ) - { - foreach($this->getRows() as $row) - { - $row->renameColumn($oldName, $newName); - if(($idSubDataTable = $row->getIdSubDataTable()) !== null) - { - Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->renameColumn($oldName, $newName); - } - } - if(!is_null($this->summaryRow)) - { - $this->summaryRow->renameColumn($oldName, $newName); - } - } - - /** - * Delete columns by name in all rows - * - * @param array $names - * @param bool $deleteRecursiveInSubtables - */ - public function deleteColumns($names, $deleteRecursiveInSubtables = false) - { - foreach($this->getRows() as $row) - { - foreach($names as $name) - { - $row->deleteColumn($name); - } - if(($idSubDataTable = $row->getIdSubDataTable()) !== null) - { - Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->deleteColumns($names, $deleteRecursiveInSubtables); - } - } - if(!is_null($this->summaryRow)) - { - foreach($names as $name) - { - $this->summaryRow->deleteColumn($name); - } - } - } - - /** - * Deletes the ith row - * - * @param int $id - * @throws Exception if the row $id cannot be found - * @return - */ - public function deleteRow( $id ) - { - if($id === self::ID_SUMMARY_ROW) - { - $this->summaryRow = null; - return; - } - if(!isset($this->rows[$id])) - { - throw new Exception("Trying to delete unknown row with idkey = $id"); - } - unset($this->rows[$id]); - } - - /** - * Deletes all row from offset, offset + limit. - * If limit is null then limit = $table->getRowsCount() - * - * @param int $offset - * @param int $limit - * @return int - */ - public function deleteRowsOffset( $offset, $limit = null ) - { - if($limit === 0) - { - return 0; - } - - $count = $this->getRowsCount(); - if($offset >= $count) - { - return 0; - } - - // if we delete until the end, we delete the summary row as well - if( is_null($limit) - || $limit >= $count ) - { - $this->summaryRow = null; - } - - if(is_null($limit)) - { - $spliced = array_splice($this->rows, $offset); - } - else - { - $spliced = array_splice($this->rows, $offset, $limit); - } - $countDeleted = count($spliced); - return $countDeleted; - } - - /** - * Deletes the rows from the list of rows ID - * - * @param array $aKeys ID of the rows to delete - * @throws Exception if any of the row to delete couldn't be found - */ - public function deleteRows( array $aKeys ) - { - foreach($aKeys as $key) - { - $this->deleteRow($key); - } - } - - /** - * Returns a simple output of the DataTable for easy visualization - * Example: echo $datatable; - * - * @return string - */ - public function __toString() - { - $renderer = new Piwik_DataTable_Renderer_Html(); - $renderer->setTable($this); - return (string)$renderer; - } - - /** - * Returns true if both DataTable are exactly the same. - * Used in unit tests. - * - * @param Piwik_DataTable $table1 - * @param Piwik_DataTable $table2 - * @return bool - */ - static public function isEqual(Piwik_DataTable $table1, Piwik_DataTable $table2) - { - $rows1 = $table1->getRows(); - $rows2 = $table2->getRows(); - - $table1->rebuildIndex(); - $table2->rebuildIndex(); - - if($table1->getRowsCount() != $table2->getRowsCount()) - { - return false; - } - - foreach($rows1 as $row1) - { - $row2 = $table2->getRowFromLabel($row1->getColumn('label')); - if($row2 === false - || !Piwik_DataTable_Row::isEqual($row1,$row2)) - { - return false; - } - } - - return true; - } - - /** - * The serialization returns a one dimension array containing all the - * serialized DataTable contained in this DataTable. - * We save DataTable in serialized format in the Database. - * Each row of this returned PHP array will be a row in the DB table. - * At the end of the method execution, the dataTable may be truncated (if $maximum* parameters are set). - * - * The keys of the array are very important as they are used to define the DataTable - * - * IMPORTANT: The main table (level 0, parent of all tables) will always be indexed by 0 - * even it was created after some other tables. - * It also means that all the parent tables (level 0) will be indexed with 0 in their respective - * serialized arrays. You should never lookup a parent table using the getTable( $id = 0) as it - * won't work. - * - * @throws Exception if an infinite recursion is found (a table row's has a subtable that is one of its parent table) - * @param int $maximumRowsInDataTable If not null, defines the number of rows maximum of the serialized dataTable - * @param int $maximumRowsInSubDataTable If not null, defines the number of rows maximum of the serialized subDataTable - * @param string $columnToSortByBeforeTruncation Column to sort by before truncation - * @return array Serialized arrays - * array( // Datatable level0 - * 0 => 'eghuighahgaueytae78yaet7yaetae', - * - * // first Datatable level1 - * 1 => 'gaegae gh gwrh guiwh uigwhuige', - * - * //second Datatable level1 - * 2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE', - * - * //first Datatable level3 (child of second Datatable level1 for example) - * 3 => 'eghuighahgaueytae78yaet7yaetaeGRQWUBGUIQGH&QE', - * ); - */ - public function getSerialized( $maximumRowsInDataTable = null, - $maximumRowsInSubDataTable = null, - $columnToSortByBeforeTruncation = null ) - { - static $depth = 0; - - if($depth > self::$maximumDepthLevelAllowed) - { - $depth = 0; - throw new Exception("Maximum recursion level of ".self::$maximumDepthLevelAllowed. " reached. Maybe you have set a DataTable_Row with an associated DataTable belonging already to one of its parent tables?"); - } - if( !is_null($maximumRowsInDataTable) ) - { - $this->filter('AddSummaryRow', - array( $maximumRowsInDataTable - 1, - Piwik_DataTable::LABEL_SUMMARY_ROW, - $columnToSortByBeforeTruncation) - ); - } - - // For each row, get the serialized row - // If it is associated to a sub table, get the serialized table recursively ; - // but returns all serialized tables and subtable in an array of 1 dimension - $aSerializedDataTable = array(); - foreach($this->rows as $row) - { - if(($idSubTable = $row->getIdSubDataTable()) !== null) - { - $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); - $depth++; - $aSerializedDataTable = $aSerializedDataTable + $subTable->getSerialized( $maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation ); - $depth--; - } - } - // we load the current Id of the DataTable - $forcedId = $this->getId(); - - // if the datatable is the parent we force the Id at 0 (this is part of the specification) - if($depth == 0) - { - $forcedId = 0; - } - - // we then serialize the rows and store them in the serialized dataTable - $addToRows = array( self::ID_SUMMARY_ROW => $this->summaryRow ); - if ($this->parents && Piwik_Config::getInstance()->General['enable_archive_parents_of_datatable']) - { - $addToRows[self::ID_PARENTS] = $this->parents; - } - $aSerializedDataTable[$forcedId] = serialize($this->rows + $addToRows); - foreach($this->rows as &$row) - { - $row->cleanPostSerialize(); - } - - return $aSerializedDataTable; - } - - /** - * Load a serialized string of a datatable. - * - * Does not load recursively all the sub DataTable. - * They will be loaded only when requesting them specifically. - * - * The function creates all the necessary DataTable_Row - * - * @param string $stringSerialized string of serialized datatable - * @throws Exception - */ - public function addRowsFromSerializedArray( $stringSerialized ) - { - $serialized = unserialize($stringSerialized); - if($serialized === false) - { - throw new Exception("The unserialization has failed!"); - } - $this->addRowsFromArray($serialized); - } - - /** - * Loads the DataTable from a PHP array data structure - * - * @param array $array Array with the following structure - * array( - * // row1 - * array( - * Piwik_DataTable_Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...), - * Piwik_DataTable_Row::METADATA => array( metadata1_name => value1, ...), // see Piwik_DataTable_Row - * ), - * // row2 - * array( ... ), - * ) - */ - public function addRowsFromArray( $array ) - { - foreach($array as $id => $row) - { - if($id == self::ID_PARENTS) - { - $this->parents = $row; - continue; - } - - if(is_array($row)) - { - $row = new Piwik_DataTable_Row($row); - } - if($id == self::ID_SUMMARY_ROW) - { - $this->summaryRow = $row; - } - else - { - $this->addRow($row); - } - } - } - - /** - * Loads the data from a simple php array. - * Basically maps a simple multidimensional php array to a DataTable. - * Not recursive (if a row contains a php array itself, it won't be loaded) - * - * @param array $array Array with the simple structure: - * array( - * array( col1_name => valueA, col2_name => valueC, ...), - * array( col1_name => valueB, col2_name => valueD, ...), - * ) - * @throws Exception - * @return - */ - public function addRowsFromSimpleArray( $array ) - { - if(count($array) === 0) - { - return; - } - - // we define an exception we may throw if at one point we notice that we cannot handle the data structure - $e = new Exception(" Data structure returned is not convertible in the requested format.". - " Try to call this method with the parameters '&format=original&serialize=1'". - "; you will get the original php data structure serialized.". - " The data structure looks like this: \n \$data = " . var_export($array, true) . "; "); - - - // first pass to see if the array has the structure - // array(col1_name => val1, col2_name => val2, etc.) - // with val* that are never arrays (only strings/numbers/bool/etc.) - // if we detect such a "simple" data structure we convert it to a row with the correct columns' names - $thisIsNotThatSimple = false; - - foreach($array as $columnValue ) - { - if(is_array($columnValue) || is_object($columnValue)) - { - $thisIsNotThatSimple = true; - break; - } - } - if($thisIsNotThatSimple === false) - { - // case when the array is indexed by the default numeric index - if( array_keys($array) == array_keys(array_fill(0, count($array), true)) ) - { - foreach($array as $row) - { - $this->addRow( new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => array($row) ) ) ); - } - } - else - { - $this->addRow( new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => $array ) ) ); - } - // we have converted our simple array to one single row - // => we exit the method as the job is now finished - return; - } - - foreach($array as $key => $row) - { - // stuff that looks like a line - if(is_array($row)) - { - /** - * We make sure we can convert this PHP array without losing information. - * We are able to convert only simple php array (no strings keys, no sub arrays, etc.) - * - */ - - // if the key is a string it means that some information was contained in this key. - // it cannot be lost during the conversion. Because we are not able to handle properly - // this key, we throw an explicit exception. - if(is_string($key)) - { - throw $e; - } - // if any of the sub elements of row is an array we cannot handle this data structure... - foreach($row as $subRow) - { - if(is_array($subRow)) - { - throw $e; - } - } - $row = new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => $row ) ); - } - // other (string, numbers...) => we build a line from this value - else - { - $row = new Piwik_DataTable_Row( array( Piwik_DataTable_Row::COLUMNS => array($key => $row)) ); - } - $this->addRow($row); - } - } - - /** - * Rewrites the input $array - * array ( - * LABEL => array(col1 => X, col2 => Y), - * LABEL2 => array(col1 => X, col2 => Y), - * ) - * to the structure - * array ( - * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)), - * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)), - * ) - * - * It also works with array having only one value per row, eg. - * array ( - * LABEL => X, - * LABEL2 => Y, - * ) - * would be converted to the structure - * array ( - * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, 'value' => X)), - * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, 'value' => Y)), - * ) - * - * The optional parameter $subtablePerLabel is an array of subTable associated to the rows of the $array - * For example if $subtablePerLabel is given - * array( - * LABEL => #Piwik_DataTable_ForLABEL, - * LABEL2 => #Piwik_DataTable_ForLABEL2, - * ) - * - * the $array would become - * array ( - * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y), - * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID DataTable For LABEL - * ), - * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y) - * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID2 DataTable For LABEL2 - * ), - * ) - * - * @param array $array See method description - * @param array|null $subtablePerLabel See method description - */ - public function addRowsFromArrayWithIndexLabel( $array, $subtablePerLabel = null) - { - $cleanRow = array(); - foreach($array as $label => $row) - { - if(!is_array($row)) - { - $row = array('value' => $row); - } - $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = null; - // we put the 'label' column first as it looks prettier in API results - $cleanRow[Piwik_DataTable_Row::COLUMNS] = array('label' => $label) + $row; - if(!is_null($subtablePerLabel) - // some rows of this table don't have subtables - // (for example case of campaigns without keywords) - && isset($subtablePerLabel[$label]) - ) - { - $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label]; - } - $this->addRow( new Piwik_DataTable_Row($cleanRow) ); - } - } - - /** - * Set the array of parent ids - * - * @param array $parents - */ - public function setParents($parents) - { - $this->parents = $parents; - } - - /** - * Get parents - * - * @return array of all parents, root level first - */ - public function getParents() { - if ($this->parents == null) - { - return array(); - } - return $this->parents; - } - - /** - * Sets the maximum nesting level to at least a certain value. If the current value is - * greater than the supplied level, the maximum nesting level is not changed. - * - * @param int $atLeastLevel - */ - static public function setMaximumDepthLevelAllowedAtLeast( $atLeastLevel ) - { - self::$maximumDepthLevelAllowed = max($atLeastLevel, self::$maximumDepthLevelAllowed); - if(self::$maximumDepthLevelAllowed < 1) { - self::$maximumDepthLevelAllowed = 1; - } - } - - /** - * Returns all table metadata. - * - * @return array - */ - public function getAllTableMetadata() - { - return $this->metadata; - } - - /** - * Returns metadata by name. - * - * @param string $name The metadata name. - * @return mixed - */ - public function getMetadata( $name ) - { - if (!isset($this->metadata[$name])) - { - return false; - } - return $this->metadata[$name]; - } - - /** - * Sets a metadata value by name. - * - * @param string $name The metadata name. - * @param mixed $value - */ - public function setMetadata( $name, $value ) - { - $this->metadata[$name] = $value; - } - - /** - * Sets the maximum number of rows allowed in this datatable (including the summary - * row). If adding more then the allowed number of rows is attempted, the extra - * rows are added to the summary row. - * - * @param int|null $maximumAllowedRows - */ - public function setMaximumAllowedRows( $maximumAllowedRows ) - { - $this->maximumAllowedRows = $maximumAllowedRows; - } - - /** - * Traverses a DataTable tree using an array of labels and returns the row - * it finds or false if it cannot find one, and the number of segments of - * the path successfully walked. - * - * If $missingRowColumns is supplied, the specified path is created. When - * a subtable is encountered w/o the queried label, a new row is created - * with the label, and a subtable is added to the row. - * - * @param array $path The path to walk. An array of label values. - * @param array|false $missingRowColumns - * The default columns to use when creating new arrays. - * If this parameter is supplied, new rows will be - * created if labels cannot be found. - * @param int $maxSubtableRows The maximum number of allowed rows in new - * subtables. - * @return array First element is the found row or false. Second element is - * the number of path segments walked. If a row is found, this - * will be == to count($path). Otherwise, it will be the index - * of the path segment that we could not find. - */ - public function walkPath( $path, $missingRowColumns = false, $maxSubtableRows = 0 ) - { - $pathLength = count($path); - - $table = $this; - $next = false; - for ($i = 0; $i < $pathLength; ++$i) - { - $segment = $path[$i]; - - $next = $table->getRowFromLabel($segment); - if ($next === false) - { - // if there is no table to advance to, and we're not adding missing rows, return false - if ($missingRowColumns === false) - { - return array(false, $i); - } - else // if we're adding missing rows, add a new row - { - $row = new Piwik_DataTable_Row_DataTableSummary(); - $row->setColumns(array('label' => $segment) + $missingRowColumns); - - $next = $table->addRow($row); - - if ($next !== $row) // if the row wasn't added, the table is full - { - // Summary row, has no metadata - $next->deleteMetadata(); - return array($next, $i); - } - } - } - - $table = $next->getSubtable(); - if ($table === false) - { - // if the row has no table (and thus no child rows), and we're not adding - // missing rows, return false - if ($missingRowColumns === false) - { - return array(false, $i); - } - else if ($i != $pathLength - 1) // create subtable if missing, but only if not on the last segment - { - $table = new Piwik_DataTable(); - $table->setMaximumAllowedRows($maxSubtableRows); - $next->setSubtable($table); - // Summary row, has no metadata - $next->deleteMetadata(); - } - } - } - - return array($next, $i); - } - - /** - * Returns a new DataTable that contains the rows of each of this table's - * subtables. - * - * @param string|false $labelColumn If supplied the label of the parent row will be - * added to a new column in each subtable row. If set to, - * 'label' each subtable row's label will be prepended w/ - * the parent row's label. - * @param bool $useMetadataColumn If true and if $labelColumn is supplied, the parent row's - * label will be added as metadata. - * @return Piwik_DataTable - */ - public function mergeSubtables( $labelColumn = false, $useMetadataColumn = false ) - { - $result = new Piwik_DataTable(); - foreach ($this->getRows() as $row) - { - $subtable = $row->getSubtable(); - if ($subtable !== false) - { - $parentLabel = $row->getColumn('label'); - - // add a copy of each subtable row to the new datatable - foreach ($subtable->getRows() as $id => $subRow) - { - $copy = clone $subRow; - - // if the summary row, add it to the existing summary row (or add a new one) - if ($id == self::ID_SUMMARY_ROW) - { - $existing = $result->getRowFromId(self::ID_SUMMARY_ROW); - if ($existing === false) - { - $result->addSummaryRow($copy); - } - else - { - $existing->sumRow($copy); - } - } - else - { - if ($labelColumn !== false) - { - // if we're modifying the subtable's rows' label column, then we make - // sure to prepend the existing label w/ the parent row's label. otherwise - // we're just adding the parent row's label as a new column/metadata. - $newLabel = $parentLabel; - if ($labelColumn == 'label') - { - $newLabel .= ' - '.$copy->getColumn('label'); - } - - // modify the child row's label or add new column/metadata - if ($useMetadataColumn) - { - $copy->setMetadata($labelColumn, $newLabel); - } - else - { - $copy->setColumn($labelColumn, $newLabel); - } - } - - $result->addRow($copy); - } - } - } - } - return $result; - } - - /** - * Returns a new DataTable created with data from a 'simple' array. - * - * @param array $array - * @return Piwik_DataTable - */ - public static function makeFromSimpleArray( $array ) - { - $dataTable = new Piwik_DataTable(); - $dataTable->addRowsFromSimpleArray($array); - return $dataTable; - } + /** Name for metadata that describes when a report was archived. */ + const ARCHIVED_DATE_METADATA_NAME = 'archived_date'; + const MAX_DEPTH_DEFAULT = 15; + /** Name for metadata that describes which columns are empty and should not be shown. */ + const EMPTY_COLUMNS_METADATA_NAME = 'empty_column'; + + /** + * Maximum nesting level. + */ + static private $maximumDepthLevelAllowed = self::MAX_DEPTH_DEFAULT; + + /** + * Array of Piwik_DataTable_Row + * + * @var Piwik_DataTable_Row[] + */ + protected $rows = array(); + + /** + * Array of parent IDs + * + * @var array + */ + protected $parents = null; + + /** + * Id assigned to the DataTable, used to lookup the table using the DataTable_Manager + * + * @var int + */ + protected $currentId; + + /** + * Current depth level of this data table + * 0 is the parent data table + * + * @var int + */ + protected $depthLevel = 0; + + /** + * This flag is set to false once we modify the table in a way that outdates the index + * + * @var bool + */ + protected $indexNotUpToDate = true; + + /** + * This flag sets the index to be rebuild whenever a new row is added, + * as opposed to re-building the full index when getRowFromLabel is called. + * This is to optimize and not rebuild the full Index in the case where we + * add row, getRowFromLabel, addRow, getRowFromLabel thousands of times. + * + * @var bool + */ + protected $rebuildIndexContinuously = false; + + /** + * Column name of last time the table was sorted + * + * @var string + */ + protected $tableSortedBy = false; + + /** + * List of Piwik_DataTable_Filter queued to this table + * + * @var array + */ + protected $queuedFilters = array(); + + /** + * We keep track of the number of rows before applying the LIMIT filter that deletes some rows + * + * @var int + */ + protected $rowsCountBeforeLimitFilter = 0; + + /** + * Defaults to false for performance reasons (most of the time we don't need recursive sorting so we save a looping over the dataTable) + * + * @var bool + */ + protected $enableRecursiveSort = false; + + /** + * When the table and all subtables are loaded, this flag will be set to true to ensure filters are applied to all subtables + * + * @var bool + */ + protected $enableRecursiveFilters = false; + + /** + * @var array + */ + protected $rowsIndexByLabel = array(); + + /** + * @var Piwik_DataTable_Row + */ + protected $summaryRow = null; + + /** + * Table metadata. + * + * @var array + */ + public $metadata = array(); + + /** + * Maximum number of rows allowed in this datatable (including the summary row). + * If adding more rows is attempted, the extra rows get summed to the summary row. + * + * @var int + */ + protected $maximumAllowedRows = 0; + + const ID_SUMMARY_ROW = -1; + const LABEL_SUMMARY_ROW = -1; + const ID_PARENTS = -2; + + /** + * Builds the DataTable, registers itself to the manager + * + */ + public function __construct() + { + $this->currentId = Piwik_DataTable_Manager::getInstance()->addTable($this); + } + + /** + * At destruction we free all memory + */ + public function __destruct() + { + static $depth = 0; + // destruct can be called several times + if ($depth < self::$maximumDepthLevelAllowed + && isset($this->rows) + ) { + $depth++; + foreach ($this->getRows() as $row) { + destroy($row); + } + unset($this->rows); + Piwik_DataTable_Manager::getInstance()->setTableDeleted($this->getId()); + $depth--; + } + } + + /** + * Sort the dataTable rows using the php callback function + * + * @param string $functionCallback + * @param string $columnSortedBy The column name. Used to then ask the datatable what column are you sorted by + */ + public function sort($functionCallback, $columnSortedBy) + { + $this->indexNotUpToDate = true; + $this->tableSortedBy = $columnSortedBy; + usort($this->rows, $functionCallback); + + if ($this->enableRecursiveSort === true) { + foreach ($this->getRows() as $row) { + if (($idSubtable = $row->getIdSubDataTable()) !== null) { + $table = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable); + $table->enableRecursiveSort(); + $table->sort($functionCallback, $columnSortedBy); + } + } + } + } + + /** + * Returns the name of the column the tables is sorted by + * + * @return bool|string + */ + public function getSortedByColumnName() + { + return $this->tableSortedBy; + } + + /** + * Enables the recursive sort. Means that when using $table->sort() + * it will also sort all subtables using the same callback + */ + public function enableRecursiveSort() + { + $this->enableRecursiveSort = true; + } + + /** + * Enables the recursive filter. Means that when using $table->filter() + * it will also filter all subtables using the same callback + */ + public function enableRecursiveFilters() + { + $this->enableRecursiveFilters = true; + } + + /** + * Returns the number of rows before we applied the limit filter + * + * @return int + */ + public function getRowsCountBeforeLimitFilter() + { + $toReturn = $this->rowsCountBeforeLimitFilter; + if ($toReturn == 0) { + return $this->getRowsCount(); + } + return $toReturn; + } + + /** + * Saves the current number of rows + */ + function setRowsCountBeforeLimitFilter() + { + $this->rowsCountBeforeLimitFilter = $this->getRowsCount(); + } + + /** + * Apply a filter to this datatable + * + * @param string $className Class name, eg. "Sort" or "Piwik_DataTable_Filter_Sort" + * @param array $parameters Array of parameters to the filter, eg. array('nb_visits', 'asc') + */ + public function filter($className, $parameters = array()) + { + if (!class_exists($className, false)) { + $className = "Piwik_DataTable_Filter_" . $className; + } + $reflectionObj = new ReflectionClass($className); + + // the first parameter of a filter is the DataTable + // we add the current datatable as the parameter + $parameters = array_merge(array($this), $parameters); + + $filter = $reflectionObj->newInstanceArgs($parameters); + + $filter->enableRecursive($this->enableRecursiveFilters); + + $filter->filter($this); + } + + /** + * Queue a DataTable_Filter that will be applied when applyQueuedFilters() is called. + * (just before sending the datatable back to the browser (or API, etc.) + * + * @param string $className The class name of the filter, eg. Piwik_DataTable_Filter_Limit + * @param array $parameters The parameters to give to the filter, eg. array( $offset, $limit) for the filter Piwik_DataTable_Filter_Limit + */ + public function queueFilter($className, $parameters = array()) + { + if (!is_array($parameters)) { + $parameters = array($parameters); + } + $this->queuedFilters[] = array('className' => $className, 'parameters' => $parameters); + } + + /** + * Apply all filters that were previously queued to this table + * @see queueFilter() + */ + public function applyQueuedFilters() + { + foreach ($this->queuedFilters as $filter) { + $this->filter($filter['className'], $filter['parameters']); + } + $this->queuedFilters = array(); + } + + /** + * Adds a new DataTable to this DataTable + * Go through all the rows of the new DataTable and applies the algorithm: + * - if a row in $table doesnt exist in $this we add the new row to $this + * - if a row exists in both $table and $this we sum the columns values into $this + * - if a row in $this doesnt exist in $table we add in $this the row of $table without modification + * + * A common row to 2 DataTable is defined by the same label + * + * @example tests/core/DataTable.test.php + * + * @param Piwik_DataTable $tableToSum + */ + public function addDataTable(Piwik_DataTable $tableToSum) + { + foreach ($tableToSum->getRows() as $row) { + $labelToLookFor = $row->getColumn('label'); + $rowFound = $this->getRowFromLabel($labelToLookFor); + if ($rowFound === false) { + if ($labelToLookFor === self::LABEL_SUMMARY_ROW) { + $this->addSummaryRow($row); + } else { + $this->addRow($row); + } + } else { + $rowFound->sumRow($row); + + // if the row to add has a subtable whereas the current row doesn't + // we simply add it (cloning the subtable) + // if the row has the subtable already + // then we have to recursively sum the subtables + if (($idSubTable = $row->getIdSubDataTable()) !== null) { + $rowFound->sumSubtable(Piwik_DataTable_Manager::getInstance()->getTable($idSubTable)); + } + } + } + } + + /** + * Returns the Piwik_DataTable_Row that has a column 'label' with the value $label + * + * @param string $label Value of the column 'label' of the row to return + * @return Piwik_DataTable_Row|false The row if found, false otherwise + */ + public function getRowFromLabel($label) + { + $rowId = $this->getRowIdFromLabel($label); + if ($rowId instanceof Piwik_DataTable_Row) { + return $rowId; + } + if (is_int($rowId) && isset($this->rows[$rowId])) { + return $this->rows[$rowId]; + } + return false; + } + + /** + * Returns the row id for the givel label + * + * @param string $label Value of the column 'label' of the row to return + * @return int|Row + */ + public function getRowIdFromLabel($label) + { + $this->rebuildIndexContinuously = true; + if ($this->indexNotUpToDate) { + $this->rebuildIndex(); + } + + if ($label === self::LABEL_SUMMARY_ROW + && !is_null($this->summaryRow) + ) { + return $this->summaryRow; + } + + $label = (string)$label; + if (!isset($this->rowsIndexByLabel[$label])) { + return false; + } + return $this->rowsIndexByLabel[$label]; + } + + /** + * Get an empty table with the same properties as this one + * + * @return Piwik_DataTable + */ + public function getEmptyClone() + { + $clone = new Piwik_DataTable; + $clone->queuedFilters = $this->queuedFilters; + $clone->metadata = $this->metadata; + return $clone; + } + + /** + * Rebuilds the index used to lookup a row by label + */ + private function rebuildIndex() + { + foreach ($this->rows as $id => $row) { + $label = $row->getColumn('label'); + if ($label !== false) { + $this->rowsIndexByLabel[$label] = $id; + } + } + $this->indexNotUpToDate = false; + } + + /** + * Returns the ith row in the array + * + * @param int $id + * @return Piwik_DataTable_Row or false if not found + */ + public function getRowFromId($id) + { + if (!isset($this->rows[$id])) { + if ($id == self::ID_SUMMARY_ROW + && !is_null($this->summaryRow) + ) { + return $this->summaryRow; + } + return false; + } + return $this->rows[$id]; + } + + /** + * Returns a row that has the subtable ID matching the parameter + * + * @param int $idSubTable + * @return Piwik_DataTable_Row|false if not found + */ + public function getRowFromIdSubDataTable($idSubTable) + { + $idSubTable = (int)$idSubTable; + foreach ($this->rows as $row) { + if ($row->getIdSubDataTable() === $idSubTable) { + return $row; + } + } + return false; + } + + /** + * Add a row to the table and rebuild the index if necessary + * + * @param Piwik_DataTable_Row $row to add at the end of the array + */ + public function addRow(Piwik_DataTable_Row $row) + { + // if there is a upper limit on the number of allowed rows and the table is full, + // add the new row to the summary row + if ($this->maximumAllowedRows > 0 + && $this->getRowsCount() >= $this->maximumAllowedRows - 1 + ) { + if ($this->summaryRow === null) // create the summary row if necessary + { + $this->addSummaryRow(new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => $row->getColumns() + ))); + $this->summaryRow->setColumn('label', self::LABEL_SUMMARY_ROW); + } else { + $this->summaryRow->sumRow($row, $enableCopyMetadata = false); + } + return $this->summaryRow; + } + + $this->rows[] = $row; + if (!$this->indexNotUpToDate + && $this->rebuildIndexContinuously + ) { + $label = $row->getColumn('label'); + if ($label !== false) { + $this->rowsIndexByLabel[$label] = count($this->rows) - 1; + } + $this->indexNotUpToDate = false; + } + return $row; + } + + /** + * Sets the summary row (a dataTable can have only one summary row) + * + * @param Piwik_DataTable_Row $row + * @return Piwik_DataTable_Row Returns $row. + */ + public function addSummaryRow(Piwik_DataTable_Row $row) + { + $this->summaryRow = $row; + return $row; + } + + /** + * Returns the dataTable ID + * + * @return int + */ + public function getId() + { + return $this->currentId; + } + + /** + * Adds a new row from a PHP array data structure + * + * @param array $row eg. array(Piwik_DataTable_Row::COLUMNS => array( 'visits' => 13, 'test' => 'toto'),) + */ + public function addRowFromArray($row) + { + $this->addRowsFromArray(array($row)); + } + + /** + * Adds a new row a PHP array data structure + * + * @param array $row eg. array('name' => 'google analytics', 'license' => 'commercial') + */ + public function addRowFromSimpleArray($row) + { + $this->addRowsFromSimpleArray(array($row)); + } + + /** + * Returns the array of Piwik_DataTable_Row + * + * @return Piwik_DataTable_Row[] + */ + public function getRows() + { + if (is_null($this->summaryRow)) { + return $this->rows; + } else { + return $this->rows + array(self::ID_SUMMARY_ROW => $this->summaryRow); + } + } + + /** + * Returns the array containing all rows values for the requested column + * + * @param string $name + * @return array + */ + public function getColumn($name) + { + $columnValues = array(); + foreach ($this->getRows() as $row) { + $columnValues[] = $row->getColumn($name); + } + return $columnValues; + } + + /** + * Returns an array containing the rows Metadata values + * + * @param string $name Metadata column to return + * @return array + */ + public function getRowsMetadata($name) + { + $metadataValues = array(); + foreach ($this->getRows() as $row) { + $metadataValues[] = $row->getMetadata($name); + } + return $metadataValues; + } + + /** + * Returns the number of rows in the table + * + * @return int + */ + public function getRowsCount() + { + if (is_null($this->summaryRow)) { + return count($this->rows); + } else { + return count($this->rows) + 1; + } + } + + /** + * Returns the first row of the DataTable + * + * @return Piwik_DataTable_Row + */ + public function getFirstRow() + { + if (count($this->rows) == 0) { + if (!is_null($this->summaryRow)) { + return $this->summaryRow; + } + return false; + } + $row = array_slice($this->rows, 0, 1); + return $row[0]; + } + + /** + * Returns the last row of the DataTable + * + * @return Piwik_DataTable_Row + */ + public function getLastRow() + { + if (!is_null($this->summaryRow)) { + return $this->summaryRow; + } + + if (count($this->rows) == 0) { + return false; + } + $row = array_slice($this->rows, -1); + return $row[0]; + } + + /** + * Returns the sum of the number of rows of all the subtables + * + the number of rows in the parent table + * + * @return int + */ + public function getRowsCountRecursive() + { + $totalCount = 0; + foreach ($this->rows as $row) { + if (($idSubTable = $row->getIdSubDataTable()) !== null) { + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + $count = $subTable->getRowsCountRecursive(); + $totalCount += $count; + } + } + + $totalCount += $this->getRowsCount(); + return $totalCount; + } + + /** + * Delete a given column $name in all the rows + * + * @param string $name + */ + public function deleteColumn($name) + { + $this->deleteColumns(array($name)); + } + + public function __sleep() + { + return array('rows', 'parents', 'summaryRow'); + } + + /** + * Rename a column in all rows + * + * @param string $oldName Old column name + * @param string $newName New column name + */ + public function renameColumn($oldName, $newName) + { + foreach ($this->getRows() as $row) { + $row->renameColumn($oldName, $newName); + if (($idSubDataTable = $row->getIdSubDataTable()) !== null) { + Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->renameColumn($oldName, $newName); + } + } + if (!is_null($this->summaryRow)) { + $this->summaryRow->renameColumn($oldName, $newName); + } + } + + /** + * Delete columns by name in all rows + * + * @param array $names + * @param bool $deleteRecursiveInSubtables + */ + public function deleteColumns($names, $deleteRecursiveInSubtables = false) + { + foreach ($this->getRows() as $row) { + foreach ($names as $name) { + $row->deleteColumn($name); + } + if (($idSubDataTable = $row->getIdSubDataTable()) !== null) { + Piwik_DataTable_Manager::getInstance()->getTable($idSubDataTable)->deleteColumns($names, $deleteRecursiveInSubtables); + } + } + if (!is_null($this->summaryRow)) { + foreach ($names as $name) { + $this->summaryRow->deleteColumn($name); + } + } + } + + /** + * Deletes the ith row + * + * @param int $id + * @throws Exception if the row $id cannot be found + * @return + */ + public function deleteRow($id) + { + if ($id === self::ID_SUMMARY_ROW) { + $this->summaryRow = null; + return; + } + if (!isset($this->rows[$id])) { + throw new Exception("Trying to delete unknown row with idkey = $id"); + } + unset($this->rows[$id]); + } + + /** + * Deletes all row from offset, offset + limit. + * If limit is null then limit = $table->getRowsCount() + * + * @param int $offset + * @param int $limit + * @return int + */ + public function deleteRowsOffset($offset, $limit = null) + { + if ($limit === 0) { + return 0; + } + + $count = $this->getRowsCount(); + if ($offset >= $count) { + return 0; + } + + // if we delete until the end, we delete the summary row as well + if (is_null($limit) + || $limit >= $count + ) { + $this->summaryRow = null; + } + + if (is_null($limit)) { + $spliced = array_splice($this->rows, $offset); + } else { + $spliced = array_splice($this->rows, $offset, $limit); + } + $countDeleted = count($spliced); + return $countDeleted; + } + + /** + * Deletes the rows from the list of rows ID + * + * @param array $aKeys ID of the rows to delete + * @throws Exception if any of the row to delete couldn't be found + */ + public function deleteRows(array $aKeys) + { + foreach ($aKeys as $key) { + $this->deleteRow($key); + } + } + + /** + * Returns a simple output of the DataTable for easy visualization + * Example: echo $datatable; + * + * @return string + */ + public function __toString() + { + $renderer = new Piwik_DataTable_Renderer_Html(); + $renderer->setTable($this); + return (string)$renderer; + } + + /** + * Returns true if both DataTable are exactly the same. + * Used in unit tests. + * + * @param Piwik_DataTable $table1 + * @param Piwik_DataTable $table2 + * @return bool + */ + static public function isEqual(Piwik_DataTable $table1, Piwik_DataTable $table2) + { + $rows1 = $table1->getRows(); + $rows2 = $table2->getRows(); + + $table1->rebuildIndex(); + $table2->rebuildIndex(); + + if ($table1->getRowsCount() != $table2->getRowsCount()) { + return false; + } + + foreach ($rows1 as $row1) { + $row2 = $table2->getRowFromLabel($row1->getColumn('label')); + if ($row2 === false + || !Piwik_DataTable_Row::isEqual($row1, $row2) + ) { + return false; + } + } + + return true; + } + + /** + * The serialization returns a one dimension array containing all the + * serialized DataTable contained in this DataTable. + * We save DataTable in serialized format in the Database. + * Each row of this returned PHP array will be a row in the DB table. + * At the end of the method execution, the dataTable may be truncated (if $maximum* parameters are set). + * + * The keys of the array are very important as they are used to define the DataTable + * + * IMPORTANT: The main table (level 0, parent of all tables) will always be indexed by 0 + * even it was created after some other tables. + * It also means that all the parent tables (level 0) will be indexed with 0 in their respective + * serialized arrays. You should never lookup a parent table using the getTable( $id = 0) as it + * won't work. + * + * @throws Exception if an infinite recursion is found (a table row's has a subtable that is one of its parent table) + * @param int $maximumRowsInDataTable If not null, defines the number of rows maximum of the serialized dataTable + * @param int $maximumRowsInSubDataTable If not null, defines the number of rows maximum of the serialized subDataTable + * @param string $columnToSortByBeforeTruncation Column to sort by before truncation + * @return array Serialized arrays + * array( // Datatable level0 + * 0 => 'eghuighahgaueytae78yaet7yaetae', + * + * // first Datatable level1 + * 1 => 'gaegae gh gwrh guiwh uigwhuige', + * + * //second Datatable level1 + * 2 => 'gqegJHUIGHEQjkgneqjgnqeugUGEQHGUHQE', + * + * //first Datatable level3 (child of second Datatable level1 for example) + * 3 => 'eghuighahgaueytae78yaet7yaetaeGRQWUBGUIQGH&QE', + * ); + */ + public function getSerialized($maximumRowsInDataTable = null, + $maximumRowsInSubDataTable = null, + $columnToSortByBeforeTruncation = null) + { + static $depth = 0; + + if ($depth > self::$maximumDepthLevelAllowed) { + $depth = 0; + throw new Exception("Maximum recursion level of " . self::$maximumDepthLevelAllowed . " reached. Maybe you have set a DataTable_Row with an associated DataTable belonging already to one of its parent tables?"); + } + if (!is_null($maximumRowsInDataTable)) { + $this->filter('AddSummaryRow', + array($maximumRowsInDataTable - 1, + Piwik_DataTable::LABEL_SUMMARY_ROW, + $columnToSortByBeforeTruncation) + ); + } + + // For each row, get the serialized row + // If it is associated to a sub table, get the serialized table recursively ; + // but returns all serialized tables and subtable in an array of 1 dimension + $aSerializedDataTable = array(); + foreach ($this->rows as $row) { + if (($idSubTable = $row->getIdSubDataTable()) !== null) { + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + $depth++; + $aSerializedDataTable = $aSerializedDataTable + $subTable->getSerialized($maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation); + $depth--; + } + } + // we load the current Id of the DataTable + $forcedId = $this->getId(); + + // if the datatable is the parent we force the Id at 0 (this is part of the specification) + if ($depth == 0) { + $forcedId = 0; + } + + // we then serialize the rows and store them in the serialized dataTable + $addToRows = array(self::ID_SUMMARY_ROW => $this->summaryRow); + if ($this->parents && Piwik_Config::getInstance()->General['enable_archive_parents_of_datatable']) { + $addToRows[self::ID_PARENTS] = $this->parents; + } + $aSerializedDataTable[$forcedId] = serialize($this->rows + $addToRows); + foreach ($this->rows as &$row) { + $row->cleanPostSerialize(); + } + + return $aSerializedDataTable; + } + + /** + * Load a serialized string of a datatable. + * + * Does not load recursively all the sub DataTable. + * They will be loaded only when requesting them specifically. + * + * The function creates all the necessary DataTable_Row + * + * @param string $stringSerialized string of serialized datatable + * @throws Exception + */ + public function addRowsFromSerializedArray($stringSerialized) + { + $serialized = unserialize($stringSerialized); + if ($serialized === false) { + throw new Exception("The unserialization has failed!"); + } + $this->addRowsFromArray($serialized); + } + + /** + * Loads the DataTable from a PHP array data structure + * + * @param array $array Array with the following structure + * array( + * // row1 + * array( + * Piwik_DataTable_Row::COLUMNS => array( col1_name => value1, col2_name => value2, ...), + * Piwik_DataTable_Row::METADATA => array( metadata1_name => value1, ...), // see Piwik_DataTable_Row + * ), + * // row2 + * array( ... ), + * ) + */ + public function addRowsFromArray($array) + { + foreach ($array as $id => $row) { + if ($id == self::ID_PARENTS) { + $this->parents = $row; + continue; + } + + if (is_array($row)) { + $row = new Piwik_DataTable_Row($row); + } + if ($id == self::ID_SUMMARY_ROW) { + $this->summaryRow = $row; + } else { + $this->addRow($row); + } + } + } + + /** + * Loads the data from a simple php array. + * Basically maps a simple multidimensional php array to a DataTable. + * Not recursive (if a row contains a php array itself, it won't be loaded) + * + * @param array $array Array with the simple structure: + * array( + * array( col1_name => valueA, col2_name => valueC, ...), + * array( col1_name => valueB, col2_name => valueD, ...), + * ) + * @throws Exception + * @return + */ + public function addRowsFromSimpleArray($array) + { + if (count($array) === 0) { + return; + } + + // we define an exception we may throw if at one point we notice that we cannot handle the data structure + $e = new Exception(" Data structure returned is not convertible in the requested format." . + " Try to call this method with the parameters '&format=original&serialize=1'" . + "; you will get the original php data structure serialized." . + " The data structure looks like this: \n \$data = " . var_export($array, true) . "; "); + + + // first pass to see if the array has the structure + // array(col1_name => val1, col2_name => val2, etc.) + // with val* that are never arrays (only strings/numbers/bool/etc.) + // if we detect such a "simple" data structure we convert it to a row with the correct columns' names + $thisIsNotThatSimple = false; + + foreach ($array as $columnValue) { + if (is_array($columnValue) || is_object($columnValue)) { + $thisIsNotThatSimple = true; + break; + } + } + if ($thisIsNotThatSimple === false) { + // case when the array is indexed by the default numeric index + if (array_keys($array) == array_keys(array_fill(0, count($array), true))) { + foreach ($array as $row) { + $this->addRow(new Piwik_DataTable_Row(array(Piwik_DataTable_Row::COLUMNS => array($row)))); + } + } else { + $this->addRow(new Piwik_DataTable_Row(array(Piwik_DataTable_Row::COLUMNS => $array))); + } + // we have converted our simple array to one single row + // => we exit the method as the job is now finished + return; + } + + foreach ($array as $key => $row) { + // stuff that looks like a line + if (is_array($row)) { + /** + * We make sure we can convert this PHP array without losing information. + * We are able to convert only simple php array (no strings keys, no sub arrays, etc.) + * + */ + + // if the key is a string it means that some information was contained in this key. + // it cannot be lost during the conversion. Because we are not able to handle properly + // this key, we throw an explicit exception. + if (is_string($key)) { + throw $e; + } + // if any of the sub elements of row is an array we cannot handle this data structure... + foreach ($row as $subRow) { + if (is_array($subRow)) { + throw $e; + } + } + $row = new Piwik_DataTable_Row(array(Piwik_DataTable_Row::COLUMNS => $row)); + } // other (string, numbers...) => we build a line from this value + else { + $row = new Piwik_DataTable_Row(array(Piwik_DataTable_Row::COLUMNS => array($key => $row))); + } + $this->addRow($row); + } + } + + /** + * Rewrites the input $array + * array ( + * LABEL => array(col1 => X, col2 => Y), + * LABEL2 => array(col1 => X, col2 => Y), + * ) + * to the structure + * array ( + * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y)), + * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y)), + * ) + * + * It also works with array having only one value per row, eg. + * array ( + * LABEL => X, + * LABEL2 => Y, + * ) + * would be converted to the structure + * array ( + * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, 'value' => X)), + * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, 'value' => Y)), + * ) + * + * The optional parameter $subtablePerLabel is an array of subTable associated to the rows of the $array + * For example if $subtablePerLabel is given + * array( + * LABEL => #Piwik_DataTable_ForLABEL, + * LABEL2 => #Piwik_DataTable_ForLABEL2, + * ) + * + * the $array would become + * array ( + * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL, col1 => X, col2 => Y), + * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID DataTable For LABEL + * ), + * array( Piwik_DataTable_Row::COLUMNS => array('label' => LABEL2, col1 => X, col2 => Y) + * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #ID2 DataTable For LABEL2 + * ), + * ) + * + * @param array $array See method description + * @param array|null $subtablePerLabel See method description + */ + public function addRowsFromArrayWithIndexLabel($array, $subtablePerLabel = null) + { + $cleanRow = array(); + foreach ($array as $label => $row) { + if (!is_array($row)) { + $row = array('value' => $row); + } + $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = null; + // we put the 'label' column first as it looks prettier in API results + $cleanRow[Piwik_DataTable_Row::COLUMNS] = array('label' => $label) + $row; + if (!is_null($subtablePerLabel) + // some rows of this table don't have subtables + // (for example case of campaigns without keywords) + && isset($subtablePerLabel[$label]) + ) { + $cleanRow[Piwik_DataTable_Row::DATATABLE_ASSOCIATED] = $subtablePerLabel[$label]; + } + $this->addRow(new Piwik_DataTable_Row($cleanRow)); + } + } + + /** + * Set the array of parent ids + * + * @param array $parents + */ + public function setParents($parents) + { + $this->parents = $parents; + } + + /** + * Get parents + * + * @return array of all parents, root level first + */ + public function getParents() + { + if ($this->parents == null) { + return array(); + } + return $this->parents; + } + + /** + * Sets the maximum nesting level to at least a certain value. If the current value is + * greater than the supplied level, the maximum nesting level is not changed. + * + * @param int $atLeastLevel + */ + static public function setMaximumDepthLevelAllowedAtLeast($atLeastLevel) + { + self::$maximumDepthLevelAllowed = max($atLeastLevel, self::$maximumDepthLevelAllowed); + if (self::$maximumDepthLevelAllowed < 1) { + self::$maximumDepthLevelAllowed = 1; + } + } + + /** + * Returns all table metadata. + * + * @return array + */ + public function getAllTableMetadata() + { + return $this->metadata; + } + + /** + * Returns metadata by name. + * + * @param string $name The metadata name. + * @return mixed + */ + public function getMetadata($name) + { + if (!isset($this->metadata[$name])) { + return false; + } + return $this->metadata[$name]; + } + + /** + * Sets a metadata value by name. + * + * @param string $name The metadata name. + * @param mixed $value + */ + public function setMetadata($name, $value) + { + $this->metadata[$name] = $value; + } + + /** + * Sets the maximum number of rows allowed in this datatable (including the summary + * row). If adding more then the allowed number of rows is attempted, the extra + * rows are added to the summary row. + * + * @param int|null $maximumAllowedRows + */ + public function setMaximumAllowedRows($maximumAllowedRows) + { + $this->maximumAllowedRows = $maximumAllowedRows; + } + + /** + * Traverses a DataTable tree using an array of labels and returns the row + * it finds or false if it cannot find one, and the number of segments of + * the path successfully walked. + * + * If $missingRowColumns is supplied, the specified path is created. When + * a subtable is encountered w/o the queried label, a new row is created + * with the label, and a subtable is added to the row. + * + * @param array $path The path to walk. An array of label values. + * @param array|false $missingRowColumns + * The default columns to use when creating new arrays. + * If this parameter is supplied, new rows will be + * created if labels cannot be found. + * @param int $maxSubtableRows The maximum number of allowed rows in new + * subtables. + * @return array First element is the found row or false. Second element is + * the number of path segments walked. If a row is found, this + * will be == to count($path). Otherwise, it will be the index + * of the path segment that we could not find. + */ + public function walkPath($path, $missingRowColumns = false, $maxSubtableRows = 0) + { + $pathLength = count($path); + + $table = $this; + $next = false; + for ($i = 0; $i < $pathLength; ++$i) { + $segment = $path[$i]; + + $next = $table->getRowFromLabel($segment); + if ($next === false) { + // if there is no table to advance to, and we're not adding missing rows, return false + if ($missingRowColumns === false) { + return array(false, $i); + } else // if we're adding missing rows, add a new row + { + $row = new Piwik_DataTable_Row_DataTableSummary(); + $row->setColumns(array('label' => $segment) + $missingRowColumns); + + $next = $table->addRow($row); + + if ($next !== $row) // if the row wasn't added, the table is full + { + // Summary row, has no metadata + $next->deleteMetadata(); + return array($next, $i); + } + } + } + + $table = $next->getSubtable(); + if ($table === false) { + // if the row has no table (and thus no child rows), and we're not adding + // missing rows, return false + if ($missingRowColumns === false) { + return array(false, $i); + } else if ($i != $pathLength - 1) // create subtable if missing, but only if not on the last segment + { + $table = new Piwik_DataTable(); + $table->setMaximumAllowedRows($maxSubtableRows); + $next->setSubtable($table); + // Summary row, has no metadata + $next->deleteMetadata(); + } + } + } + + return array($next, $i); + } + + /** + * Returns a new DataTable that contains the rows of each of this table's + * subtables. + * + * @param string|false $labelColumn If supplied the label of the parent row will be + * added to a new column in each subtable row. If set to, + * 'label' each subtable row's label will be prepended w/ + * the parent row's label. + * @param bool $useMetadataColumn If true and if $labelColumn is supplied, the parent row's + * label will be added as metadata. + * @return Piwik_DataTable + */ + public function mergeSubtables($labelColumn = false, $useMetadataColumn = false) + { + $result = new Piwik_DataTable(); + foreach ($this->getRows() as $row) { + $subtable = $row->getSubtable(); + if ($subtable !== false) { + $parentLabel = $row->getColumn('label'); + + // add a copy of each subtable row to the new datatable + foreach ($subtable->getRows() as $id => $subRow) { + $copy = clone $subRow; + + // if the summary row, add it to the existing summary row (or add a new one) + if ($id == self::ID_SUMMARY_ROW) { + $existing = $result->getRowFromId(self::ID_SUMMARY_ROW); + if ($existing === false) { + $result->addSummaryRow($copy); + } else { + $existing->sumRow($copy); + } + } else { + if ($labelColumn !== false) { + // if we're modifying the subtable's rows' label column, then we make + // sure to prepend the existing label w/ the parent row's label. otherwise + // we're just adding the parent row's label as a new column/metadata. + $newLabel = $parentLabel; + if ($labelColumn == 'label') { + $newLabel .= ' - ' . $copy->getColumn('label'); + } + + // modify the child row's label or add new column/metadata + if ($useMetadataColumn) { + $copy->setMetadata($labelColumn, $newLabel); + } else { + $copy->setColumn($labelColumn, $newLabel); + } + } + + $result->addRow($copy); + } + } + } + } + return $result; + } + + /** + * Returns a new DataTable created with data from a 'simple' array. + * + * @param array $array + * @return Piwik_DataTable + */ + public static function makeFromSimpleArray($array) + { + $dataTable = new Piwik_DataTable(); + $dataTable->addRowsFromSimpleArray($array); + return $dataTable; + } } diff --git a/core/DataTable/Array.php b/core/DataTable/Array.php index e07867dfc2..367a1b1aa6 100644 --- a/core/DataTable/Array.php +++ b/core/DataTable/Array.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,421 +12,398 @@ /** * The DataTable_Array is a way to store an array of dataTable. * The Piwik_DataTable_Array implements some of the features of the Piwik_DataTable such as queueFilter, getRowsCount. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Array { - /** - * Array containing the DataTable withing this Piwik_DataTable_Array - * - * @var Piwik_DataTable[] - */ - protected $array = array(); - - /** - * This is the label used to index the tables. - * For example if the tables are indexed using the timestamp of each period - * eg. $this->array[1045886960] = new Piwik_DataTable(); - * the keyName would be 'timestamp'. - * - * This label is used in the Renderer (it becomes a column name or the XML description tag) - * - * @var string - */ - protected $keyName = 'defaultKeyName'; - - /** - * Returns the keyName string @see self::$keyName - * - * @return string - */ - public function getKeyName() - { - return $this->keyName; - } - - /** - * Set the keyName @see self::$keyName - * - * @param string $name - */ - public function setKeyName($name) - { - $this->keyName = $name; - } - - /** - * Returns the number of DataTable in this DataTable_Array - * - * @return int - */ - public function getRowsCount() - { - return count($this->array); - } - - /** - * Queue a filter to the DataTable_Array will queue this filter to every DataTable of the DataTable_Array. - * - * @param string $className Filter name, eg. Piwik_DataTable_Filter_Limit - * @param array $parameters Filter parameters, eg. array( 50, 10 ) - */ - public function queueFilter( $className, $parameters = array() ) - { - foreach($this->array as $table) - { - $table->queueFilter($className, $parameters); - } - } - - /** - * Apply the filters previously queued to each of the DataTable of this DataTable_Array. - */ - public function applyQueuedFilters() - { - foreach($this->array as $table) - { - $table->applyQueuedFilters(); - } - } - - /** - * Apply a filter to all tables in the array - * - * @param string $className Name of filter class - * @param array $parameters Filter parameters - */ - public function filter($className, $parameters = array()) - { - foreach($this->array as $id => $table) - { - $table->filter($className, $parameters); - } - } - - /** - * Returns the array of DataTable - * - * @return Piwik_DataTable[] - */ - public function getArray() - { - return $this->array; - } - - /** - * Returns the table with the specified label. - * - * @param string $label - * @return Piwik_DataTable - */ - public function getTable($label) - { - return $this->array[$label]; + /** + * Array containing the DataTable withing this Piwik_DataTable_Array + * + * @var Piwik_DataTable[] + */ + protected $array = array(); + + /** + * This is the label used to index the tables. + * For example if the tables are indexed using the timestamp of each period + * eg. $this->array[1045886960] = new Piwik_DataTable(); + * the keyName would be 'timestamp'. + * + * This label is used in the Renderer (it becomes a column name or the XML description tag) + * + * @var string + */ + protected $keyName = 'defaultKeyName'; + + /** + * Returns the keyName string @see self::$keyName + * + * @return string + */ + public function getKeyName() + { + return $this->keyName; + } + + /** + * Set the keyName @see self::$keyName + * + * @param string $name + */ + public function setKeyName($name) + { + $this->keyName = $name; + } + + /** + * Returns the number of DataTable in this DataTable_Array + * + * @return int + */ + public function getRowsCount() + { + return count($this->array); + } + + /** + * Queue a filter to the DataTable_Array will queue this filter to every DataTable of the DataTable_Array. + * + * @param string $className Filter name, eg. Piwik_DataTable_Filter_Limit + * @param array $parameters Filter parameters, eg. array( 50, 10 ) + */ + public function queueFilter($className, $parameters = array()) + { + foreach ($this->array as $table) { + $table->queueFilter($className, $parameters); + } + } + + /** + * Apply the filters previously queued to each of the DataTable of this DataTable_Array. + */ + public function applyQueuedFilters() + { + foreach ($this->array as $table) { + $table->applyQueuedFilters(); + } + } + + /** + * Apply a filter to all tables in the array + * + * @param string $className Name of filter class + * @param array $parameters Filter parameters + */ + public function filter($className, $parameters = array()) + { + foreach ($this->array as $id => $table) { + $table->filter($className, $parameters); + } + } + + /** + * Returns the array of DataTable + * + * @return Piwik_DataTable[] + */ + public function getArray() + { + return $this->array; + } + + /** + * Returns the table with the specified label. + * + * @param string $label + * @return Piwik_DataTable + */ + public function getTable($label) + { + return $this->array[$label]; + } + + /** + * Returns the first row + * This method can be used to treat DataTable and DataTable_Array in the same way + * + * @return Piwik_DataTable_Row + */ + public function getFirstRow() + { + foreach ($this->array as $table) { + $row = $table->getFirstRow(); + if ($row !== false) { + return $row; + } + } + return false; } - + + /** + * Adds a new DataTable to the DataTable_Array + * + * @param Piwik_DataTable $table + * @param string $label Label used to index this table in the array + */ + public function addTable($table, $label) + { + $this->array[$label] = $table; + } + + /** + * Returns a string output of this DataTable_Array (applying the default renderer to every DataTable + * of this DataTable_Array). + * + * @return string + */ + public function __toString() + { + $renderer = new Piwik_DataTable_Renderer_Console(); + $renderer->setTable($this); + return (string)$renderer; + } + /** - * Returns the first row - * This method can be used to treat DataTable and DataTable_Array in the same way - * - * @return Piwik_DataTable_Row - */ - public function getFirstRow() - { - foreach ($this->array as $table) - { - $row = $table->getFirstRow(); - if ($row !== false) - { - return $row; - } - } - return false; - } - - /** - * Adds a new DataTable to the DataTable_Array - * - * @param Piwik_DataTable $table - * @param string $label Label used to index this table in the array - */ - public function addTable( $table, $label ) - { - $this->array[$label] = $table; - } - - /** - * Returns a string output of this DataTable_Array (applying the default renderer to every DataTable - * of this DataTable_Array). - * - * @return string - */ - public function __toString() - { - $renderer = new Piwik_DataTable_Renderer_Console(); - $renderer->setTable($this); - return (string)$renderer; - } - - /** - * @see Piwik_DataTable::enableRecursiveSort() - */ - public function enableRecursiveSort() - { - foreach($this->array as $table) - { - $table->enableRecursiveSort(); - } - } - - /** - * Renames the given column - * - * @see Piwik_DataTable::renameColumn - * @param string $oldName - * @param string $newName - */ - public function renameColumn($oldName, $newName) - { - foreach($this->array as $table) - { - $table->renameColumn($oldName, $newName); - } - } - - /** - * Deletes the given columns - * - * @see Piwik_DataTable::deleteColumns - * @param array $columns - */ - public function deleteColumns($columns) - { - foreach($this->array as $table) - { - $table->deleteColumns($columns); - } - } + * @see Piwik_DataTable::enableRecursiveSort() + */ + public function enableRecursiveSort() + { + foreach ($this->array as $table) { + $table->enableRecursiveSort(); + } + } + + /** + * Renames the given column + * + * @see Piwik_DataTable::renameColumn + * @param string $oldName + * @param string $newName + */ + public function renameColumn($oldName, $newName) + { + foreach ($this->array as $table) { + $table->renameColumn($oldName, $newName); + } + } + + /** + * Deletes the given columns + * + * @see Piwik_DataTable::deleteColumns + * @param array $columns + */ + public function deleteColumns($columns) + { + foreach ($this->array as $table) { + $table->deleteColumns($columns); + } + } public function deleteRow($id) { - foreach($this->array as $table) - { + foreach ($this->array as $table) { $table->deleteRow($id); } } - /** - * Deletes the given column - * - * @see Piwik_DataTable::deleteColumn - * @param string $column - */ - public function deleteColumn($column) - { - foreach($this->array as $table) - { - $table->deleteColumn($column); - } - } - - /** - * Returns the array containing all rows values in all data tables for the requested column - * - * @param string $name - * @return array - */ - public function getColumn( $name ) - { - $values = array(); - foreach($this->array as $table) - { - $moreValues = $table->getColumn($name); - foreach ($moreValues as &$value) { - $values[] = $value; - } - } - return $values; - } - - /** - * Merges the rows of every child DataTable into a new DataTable and - * returns it. This function will also set the label of the merged rows - * to the label of the DataTable they were originally from. - * - * The result of this function is determined by the type of DataTable - * this instance holds. If this DataTable_Array instance holds an array - * of DataTables, this function will transform it from: - * <code> - * Label 0: - * DataTable(row1) - * Label 1: - * DataTable(row2) - * </code> - * to: - * <code> - * DataTable(row1[label = 'Label 0'], row2[label = 'Label 1']) - * </code> - * - * If this instance holds an array of DataTable_Arrays, this function will - * transform it from: - * <code> - * Outer Label 0: // the outer DataTable_Array - * Inner Label 0: // one of the inner DataTable_Arrays - * DataTable(row1) - * Inner Label 1: - * DataTable(row2) - * Outer Label 1: - * Inner Label 0: - * DataTable(row3) - * Inner Label 1: - * DataTable(row4) - * </code> - * to: - * <code> - * Inner Label 0: - * DataTable(row1[label = 'Outer Label 0'], row3[label = 'Outer Label 1']) - * Inner Label 1: - * DataTable(row2[label = 'Outer Label 0'], row4[label = 'Outer Label 1']) - * </code> - * - * In addition, if this instance holds an array of DataTable_Arrays, the - * metadata of the first child is used as the metadata of the result. - * - * This function can be used, for example, to smoosh IndexedBySite archive - * query results into one DataTable w/ different rows differentiated by site ID. - * - * @return Piwik_DataTable|Piwik_DataTable_Array - */ - public function mergeChildren() - { - $firstChild = reset($this->array); - - if ($firstChild instanceof Piwik_DataTable_Array) - { - $result = $firstChild->getEmptyClone(); - - foreach ($this->array as $label => $subTableArray) - { - foreach ($subTableArray->array as $innerLabel => $subTable) - { - if (!isset($result->array[$innerLabel])) - { - $dataTable = new Piwik_DataTable(); - $dataTable->metadata = $subTable->metadata; - - $result->addTable($dataTable, $innerLabel); - } - - $this->copyRowsAndSetLabel($result->array[$innerLabel], $subTable, $label); - } - } - } - else - { - $result = new Piwik_DataTable(); - - foreach ($this->array as $label => $subTable) - { - $this->copyRowsAndSetLabel($result, $subTable, $label); - } - } - - return $result; - } - - /** - * Utility function used by mergeChildren. Copies the rows from one table, - * sets their 'label' columns to a value and adds them to another table. - * - * @param Piwik_DataTable $toTable The table to copy rows to. - * @param Piwik_DataTable $fromTable The table to copy rows from. - * @param string $label The value to set the 'label' column of every copied row. - */ - private function copyRowsAndSetLabel($toTable, $fromTable, $label) - { - foreach ($fromTable->getRows() as $fromRow) - { - $oldColumns = $fromRow->getColumns(); - unset($oldColumns['label']); - - $columns = array_merge(array('label' => $label), $oldColumns); - $row = new Piwik_DataTable_Row(array( - Piwik_DataTable_Row::COLUMNS => $columns, - Piwik_DataTable_Row::METADATA => $fromRow->getMetadata(), - Piwik_DataTable_Row::DATATABLE_ASSOCIATED => $fromRow->getIdSubDataTable() - )); - $toTable->addRow($row); - } - } - - /** - * Adds a DataTable to all the tables in this array - * NOTE: Will only add $tableToSum if the childTable has some rows - * - * @param Piwik_DataTable $tableToSum - */ - public function addDataTable( Piwik_DataTable $tableToSum ) - { - foreach ($this->getArray() as $childTable) - { - if($childTable->getRowsCount() > 0) - { - $childTable->addDataTable($tableToSum); - } - } - } - - /** - * Returns a new DataTable_Array w/ child tables that have had their - * subtables merged. - * - * @see Piwik_DataTable::mergeSubtables - * - * @return Piwik_DataTable_Array - */ - public function mergeSubtables() - { - $result = $this->getEmptyClone(); - foreach ($this->array as $label => $childTable) - { - $result->addTable($childTable->mergeSubtables(), $label); - } - return $result; - } - - /** - * Returns a new DataTable_Array w/o any child DataTables, but with - * the same key name as this instance. - * - * @return Piwik_DataTable_Array - */ - public function getEmptyClone() - { - $newTableArray = new Piwik_DataTable_Array; - $newTableArray->setKeyName($this->getKeyName()); - return $newTableArray; - } - - /** - * Returns the intersection of cildrends' meta data arrays - * - * @param string $name The metadata name. - * @return mixed - */ - public function getMetadataIntersectArray( $name ) - { - $data = array(); - foreach ($this->getArray() as $childTable) - { - $childData = $childTable->getMetadata($name); - if (is_array($childData)) - { - $data = array_intersect($data, $childData); - } - } - return array_values($data); - } - + + /** + * Deletes the given column + * + * @see Piwik_DataTable::deleteColumn + * @param string $column + */ + public function deleteColumn($column) + { + foreach ($this->array as $table) { + $table->deleteColumn($column); + } + } + + /** + * Returns the array containing all rows values in all data tables for the requested column + * + * @param string $name + * @return array + */ + public function getColumn($name) + { + $values = array(); + foreach ($this->array as $table) { + $moreValues = $table->getColumn($name); + foreach ($moreValues as &$value) { + $values[] = $value; + } + } + return $values; + } + + /** + * Merges the rows of every child DataTable into a new DataTable and + * returns it. This function will also set the label of the merged rows + * to the label of the DataTable they were originally from. + * + * The result of this function is determined by the type of DataTable + * this instance holds. If this DataTable_Array instance holds an array + * of DataTables, this function will transform it from: + * <code> + * Label 0: + * DataTable(row1) + * Label 1: + * DataTable(row2) + * </code> + * to: + * <code> + * DataTable(row1[label = 'Label 0'], row2[label = 'Label 1']) + * </code> + * + * If this instance holds an array of DataTable_Arrays, this function will + * transform it from: + * <code> + * Outer Label 0: // the outer DataTable_Array + * Inner Label 0: // one of the inner DataTable_Arrays + * DataTable(row1) + * Inner Label 1: + * DataTable(row2) + * Outer Label 1: + * Inner Label 0: + * DataTable(row3) + * Inner Label 1: + * DataTable(row4) + * </code> + * to: + * <code> + * Inner Label 0: + * DataTable(row1[label = 'Outer Label 0'], row3[label = 'Outer Label 1']) + * Inner Label 1: + * DataTable(row2[label = 'Outer Label 0'], row4[label = 'Outer Label 1']) + * </code> + * + * In addition, if this instance holds an array of DataTable_Arrays, the + * metadata of the first child is used as the metadata of the result. + * + * This function can be used, for example, to smoosh IndexedBySite archive + * query results into one DataTable w/ different rows differentiated by site ID. + * + * @return Piwik_DataTable|Piwik_DataTable_Array + */ + public function mergeChildren() + { + $firstChild = reset($this->array); + + if ($firstChild instanceof Piwik_DataTable_Array) { + $result = $firstChild->getEmptyClone(); + + foreach ($this->array as $label => $subTableArray) { + foreach ($subTableArray->array as $innerLabel => $subTable) { + if (!isset($result->array[$innerLabel])) { + $dataTable = new Piwik_DataTable(); + $dataTable->metadata = $subTable->metadata; + + $result->addTable($dataTable, $innerLabel); + } + + $this->copyRowsAndSetLabel($result->array[$innerLabel], $subTable, $label); + } + } + } else { + $result = new Piwik_DataTable(); + + foreach ($this->array as $label => $subTable) { + $this->copyRowsAndSetLabel($result, $subTable, $label); + } + } + + return $result; + } + + /** + * Utility function used by mergeChildren. Copies the rows from one table, + * sets their 'label' columns to a value and adds them to another table. + * + * @param Piwik_DataTable $toTable The table to copy rows to. + * @param Piwik_DataTable $fromTable The table to copy rows from. + * @param string $label The value to set the 'label' column of every copied row. + */ + private function copyRowsAndSetLabel($toTable, $fromTable, $label) + { + foreach ($fromTable->getRows() as $fromRow) { + $oldColumns = $fromRow->getColumns(); + unset($oldColumns['label']); + + $columns = array_merge(array('label' => $label), $oldColumns); + $row = new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => $columns, + Piwik_DataTable_Row::METADATA => $fromRow->getMetadata(), + Piwik_DataTable_Row::DATATABLE_ASSOCIATED => $fromRow->getIdSubDataTable() + )); + $toTable->addRow($row); + } + } + + /** + * Adds a DataTable to all the tables in this array + * NOTE: Will only add $tableToSum if the childTable has some rows + * + * @param Piwik_DataTable $tableToSum + */ + public function addDataTable(Piwik_DataTable $tableToSum) + { + foreach ($this->getArray() as $childTable) { + if ($childTable->getRowsCount() > 0) { + $childTable->addDataTable($tableToSum); + } + } + } + + /** + * Returns a new DataTable_Array w/ child tables that have had their + * subtables merged. + * + * @see Piwik_DataTable::mergeSubtables + * + * @return Piwik_DataTable_Array + */ + public function mergeSubtables() + { + $result = $this->getEmptyClone(); + foreach ($this->array as $label => $childTable) { + $result->addTable($childTable->mergeSubtables(), $label); + } + return $result; + } + + /** + * Returns a new DataTable_Array w/o any child DataTables, but with + * the same key name as this instance. + * + * @return Piwik_DataTable_Array + */ + public function getEmptyClone() + { + $newTableArray = new Piwik_DataTable_Array; + $newTableArray->setKeyName($this->getKeyName()); + return $newTableArray; + } + + /** + * Returns the intersection of cildrends' meta data arrays + * + * @param string $name The metadata name. + * @return mixed + */ + public function getMetadataIntersectArray($name) + { + $data = array(); + foreach ($this->getArray() as $childTable) { + $childData = $childTable->getMetadata($name); + if (is_array($childData)) { + $data = array_intersect($data, $childData); + } + } + return array_values($data); + } + } diff --git a/core/DataTable/Filter.php b/core/DataTable/Filter.php index adaeea453a..455c5aa6e7 100644 --- a/core/DataTable/Filter.php +++ b/core/DataTable/Filter.php @@ -1,80 +1,77 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * A filter is applied instantly to a given DataTable and can - * - remove rows + * A filter is applied instantly to a given DataTable and can + * - remove rows * - change columns values (lowercase the strings, truncate, etc.) * - add/remove columns or metadata (compute percentage values, add an 'icon' metadata based on the label, etc.) * - add/remove/edit sub DataTable associated to some rows * - whatever you can imagine - * - * The concept is very simple: the filter is given the DataTable + * + * The concept is very simple: the filter is given the DataTable * and can do whatever is necessary on the data (in the filter() method). - * + * * @package Piwik * @subpackage Piwik_DataTable */ abstract class Piwik_DataTable_Filter { - /** - * @var bool - */ - protected $enableRecursive = false; + /** + * @var bool + */ + protected $enableRecursive = false; - /** - * @throws Exception - * @param Piwik_DataTable $table - */ - public function __construct($table) - { - if(!($table instanceof Piwik_DataTable)) - { - throw new Exception("The filter accepts only a Piwik_DataTable object."); - } - } + /** + * @throws Exception + * @param Piwik_DataTable $table + */ + public function __construct($table) + { + if (!($table instanceof Piwik_DataTable)) { + throw new Exception("The filter accepts only a Piwik_DataTable object."); + } + } - /** - * Filters the given data table - * - * @param Piwik_DataTable $table - */ - abstract public function filter($table); + /** + * Filters the given data table + * + * @param Piwik_DataTable $table + */ + abstract public function filter($table); - /** - * Enables/Disables the recursive mode - * - * @param bool $bool - */ - public function enableRecursive($bool) - { - $this->enableRecursive = (bool)$bool; - } + /** + * Enables/Disables the recursive mode + * + * @param bool $bool + */ + public function enableRecursive($bool) + { + $this->enableRecursive = (bool)$bool; + } - /** - * Filters a subtable - * - * @param Piwik_DataTable_Row $row - * @return mixed - */ - public function filterSubTable(Piwik_DataTable_Row $row) - { - if(!$this->enableRecursive) - { - return; - } - if($row->isSubtableLoaded()) - { - $subTable = Piwik_DataTable_Manager::getInstance()->getTable( $row->getIdSubDataTable() ); - $this->filter($subTable); - } - } + /** + * Filters a subtable + * + * @param Piwik_DataTable_Row $row + * @return mixed + */ + public function filterSubTable(Piwik_DataTable_Row $row) + { + if (!$this->enableRecursive) { + return; + } + if ($row->isSubtableLoaded()) { + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($row->getIdSubDataTable()); + $this->filter($subTable); + } + } } diff --git a/core/DataTable/Filter/AddColumnsProcessedMetrics.php b/core/DataTable/Filter/AddColumnsProcessedMetrics.php index 9929c64df7..63dbf4898d 100644 --- a/core/DataTable/Filter/AddColumnsProcessedMetrics.php +++ b/core/DataTable/Filter/AddColumnsProcessedMetrics.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,120 +15,112 @@ */ class Piwik_DataTable_Filter_AddColumnsProcessedMetrics extends Piwik_DataTable_Filter { - protected $invalidDivision = 0; - protected $roundPrecision = 2; - protected $deleteRowsWithNoVisit = true; + protected $invalidDivision = 0; + protected $roundPrecision = 2; + protected $deleteRowsWithNoVisit = true; - /** - * @param Piwik_DataTable $table - * @param bool $deleteRowsWithNoVisit Automatically set to true when filter_add_columns_when_show_all_columns is found in the API request - * @return Piwik_DataTable_Filter_AddColumnsProcessedMetrics - */ - public function __construct( $table, $deleteRowsWithNoVisit = true ) - { - $this->deleteRowsWithNoVisit = $deleteRowsWithNoVisit; - parent::__construct($table); - } + /** + * @param Piwik_DataTable $table + * @param bool $deleteRowsWithNoVisit Automatically set to true when filter_add_columns_when_show_all_columns is found in the API request + * @return Piwik_DataTable_Filter_AddColumnsProcessedMetrics + */ + public function __construct($table, $deleteRowsWithNoVisit = true) + { + $this->deleteRowsWithNoVisit = $deleteRowsWithNoVisit; + parent::__construct($table); + } - /** - * Filters the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - $rowsIdToDelete = array(); - foreach($table->getRows() as $key => $row) - { - $nbVisits = $this->getColumn($row, Piwik_Archive::INDEX_NB_VISITS); - $nbActions = $this->getColumn($row, Piwik_Archive::INDEX_NB_ACTIONS); - if($nbVisits == 0 - && $nbActions == 0 - && $this->deleteRowsWithNoVisit) - { - // case of keyword/website/campaign with a conversion for this day, - // but no visit, we don't show it - $rowsIdToDelete[] = $key; - continue; - } - - $nbVisitsConverted = (int)$this->getColumn($row, Piwik_Archive::INDEX_NB_VISITS_CONVERTED); - if($nbVisitsConverted > 0) - { - $conversionRate = round(100 * $nbVisitsConverted / $nbVisits, $this->roundPrecision); - try { - $row->addColumn('conversion_rate', $conversionRate."%"); - } catch(Exception $e) { - // conversion_rate can be defined upstream apparently? FIXME - } - } - - if($nbVisits == 0) - { - $actionsPerVisit = $averageTimeOnSite = $bounceRate = $this->invalidDivision; - } - else - { - // nb_actions / nb_visits => Actions/visit - // sum_visit_length / nb_visits => Avg. Time on Site - // bounce_count / nb_visits => Bounce Rate - $actionsPerVisit = round($nbActions / $nbVisits, $this->roundPrecision); - $visitLength = $this->getColumn($row, Piwik_Archive::INDEX_SUM_VISIT_LENGTH); - $averageTimeOnSite = round($visitLength / $nbVisits, $rounding = 0); - $bounceRate = round(100 * $this->getColumn($row, Piwik_Archive::INDEX_BOUNCE_COUNT) / $nbVisits, $this->roundPrecision); - } - try { - $row->addColumn('nb_actions_per_visit', $actionsPerVisit); - $row->addColumn('avg_time_on_site', $averageTimeOnSite); - // It could be useful for API users to have raw sum length value. - //$row->addMetadata('sum_visit_length', $visitLength); - } catch(Exception $e) {} - - try { - $row->addColumn('bounce_rate', $bounceRate."%"); - } catch(Exception $e) {} - - $this->filterSubTable($row); - } - $table->deleteRows($rowsIdToDelete); - } + /** + * Filters the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + $rowsIdToDelete = array(); + foreach ($table->getRows() as $key => $row) { + $nbVisits = $this->getColumn($row, Piwik_Archive::INDEX_NB_VISITS); + $nbActions = $this->getColumn($row, Piwik_Archive::INDEX_NB_ACTIONS); + if ($nbVisits == 0 + && $nbActions == 0 + && $this->deleteRowsWithNoVisit + ) { + // case of keyword/website/campaign with a conversion for this day, + // but no visit, we don't show it + $rowsIdToDelete[] = $key; + continue; + } + + $nbVisitsConverted = (int)$this->getColumn($row, Piwik_Archive::INDEX_NB_VISITS_CONVERTED); + if ($nbVisitsConverted > 0) { + $conversionRate = round(100 * $nbVisitsConverted / $nbVisits, $this->roundPrecision); + try { + $row->addColumn('conversion_rate', $conversionRate . "%"); + } catch (Exception $e) { + // conversion_rate can be defined upstream apparently? FIXME + } + } + + if ($nbVisits == 0) { + $actionsPerVisit = $averageTimeOnSite = $bounceRate = $this->invalidDivision; + } else { + // nb_actions / nb_visits => Actions/visit + // sum_visit_length / nb_visits => Avg. Time on Site + // bounce_count / nb_visits => Bounce Rate + $actionsPerVisit = round($nbActions / $nbVisits, $this->roundPrecision); + $visitLength = $this->getColumn($row, Piwik_Archive::INDEX_SUM_VISIT_LENGTH); + $averageTimeOnSite = round($visitLength / $nbVisits, $rounding = 0); + $bounceRate = round(100 * $this->getColumn($row, Piwik_Archive::INDEX_BOUNCE_COUNT) / $nbVisits, $this->roundPrecision); + } + try { + $row->addColumn('nb_actions_per_visit', $actionsPerVisit); + $row->addColumn('avg_time_on_site', $averageTimeOnSite); + // It could be useful for API users to have raw sum length value. + //$row->addMetadata('sum_visit_length', $visitLength); + } catch (Exception $e) { + } + + try { + $row->addColumn('bounce_rate', $bounceRate . "%"); + } catch (Exception $e) { + } + + $this->filterSubTable($row); + } + $table->deleteRows($rowsIdToDelete); + } + + /** + * Returns column from a given row. + * Will work with 2 types of datatable + * - raw datatables coming from the archive DB, which columns are int indexed + * - datatables processed resulting of API calls, which columns have human readable english names + * + * @param Piwik_DataTable_Row $row + * @param int $columnIdRaw see consts in Piwik_Archive:: + * @param bool $mappingIdToName + * @return mixed Value of column, false if not found + */ + protected function getColumn($row, $columnIdRaw, $mappingIdToName = false) + { + if (empty($mappingIdToName)) { + $mappingIdToName = Piwik_Archive::$mappingFromIdToName; + } + $columnIdReadable = $mappingIdToName[$columnIdRaw]; + if ($row instanceof Piwik_DataTable_Row) { + $raw = $row->getColumn($columnIdRaw); + if ($raw !== false) { + return $raw; + } + return $row->getColumn($columnIdReadable); + } + if (isset($row[$columnIdRaw])) { + return $row[$columnIdRaw]; + } + if (isset($row[$columnIdReadable])) { + return $row[$columnIdReadable]; + } + return false; + } - /** - * Returns column from a given row. - * Will work with 2 types of datatable - * - raw datatables coming from the archive DB, which columns are int indexed - * - datatables processed resulting of API calls, which columns have human readable english names - * - * @param Piwik_DataTable_Row $row - * @param int $columnIdRaw see consts in Piwik_Archive:: - * @param bool $mappingIdToName - * @return mixed Value of column, false if not found - */ - protected function getColumn($row, $columnIdRaw, $mappingIdToName = false) - { - if(empty($mappingIdToName)) - { - $mappingIdToName = Piwik_Archive::$mappingFromIdToName; - } - $columnIdReadable = $mappingIdToName[$columnIdRaw]; - if($row instanceof Piwik_DataTable_Row) - { - $raw = $row->getColumn($columnIdRaw); - if($raw !== false) - { - return $raw; - } - return $row->getColumn($columnIdReadable); - } - if(isset($row[$columnIdRaw])) - { - return $row[$columnIdRaw]; - } - if(isset($row[$columnIdReadable])) - { - return $row[$columnIdReadable]; - } - return false; - } - } diff --git a/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php b/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php index b49b69fd72..2d9e6cf02a 100644 --- a/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php +++ b/core/DataTable/Filter/AddColumnsProcessedMetricsGoal.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,200 +15,182 @@ */ class Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal extends Piwik_DataTable_Filter_AddColumnsProcessedMetrics { - /** - * Process main goal metrics: conversion rate, revenue per visit - */ - const GOALS_MINIMAL_REPORT = -2; - - /** - * Process main goal metrics, and conversion rate per goal - */ - const GOALS_OVERVIEW = -1; - - /** - * Process all goal and per-goal metrics - */ - const GOALS_FULL_TABLE = 0; - - /** - * Adds processed goal metrics to a table: - * - global conversion rate, - * - global revenue per visit. - * Can also process per-goal metrics: - * - conversion rate - * - nb conversions - * - revenue per visit - * - * @param Piwik_DataTable $table - * @param bool $enable should be true (automatically set to true when filter_update_columns_when_show_all_goals is found in the API request) - * @param string $processOnlyIdGoal Defines what metrics to add (don't process metrics when you don't display them) - * If self::GOALS_FULL_TABLE, all Goal metrics (and per goal metrics) will be processed - * If self::GOALS_OVERVIEW, only the main goal metrics will be added - * If an int > 0, then will process only metrics for this specific Goal - * @return Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal - */ - public function __construct( $table, $enable = true, $processOnlyIdGoal ) - { - $this->processOnlyIdGoal = $processOnlyIdGoal; - $this->isEcommerce = $this->processOnlyIdGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER || $this->processOnlyIdGoal == Piwik_Archive::LABEL_ECOMMERCE_CART; - parent::__construct($table); - // Ensure that all rows with no visit but conversions will be displayed - $this->deleteRowsWithNoVisit = false; - } - - /** - * Filters the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - // Add standard processed metrics - parent::filter($table); - $roundingPrecision = Piwik_Tracker_GoalManager::REVENUE_PRECISION; - $expectedColumns = array(); - foreach($table->getRows() as $key => $row) - { - $currentColumns = $row->getColumns(); - $newColumns = array(); - - // visits could be undefined when there is a conversion but no visit - $nbVisits = (int)$this->getColumn($row, Piwik_Archive::INDEX_NB_VISITS); - $conversions = (int)$this->getColumn($row, Piwik_Archive::INDEX_NB_CONVERSIONS); - $goals = $this->getColumn($currentColumns, Piwik_Archive::INDEX_GOALS); - if($goals) - { - $revenue = 0; - foreach($goals as $goalId => $columnValue) - { - if($goalId == Piwik_Archive::LABEL_ECOMMERCE_CART) - { - continue; - } - if($goalId >= Piwik_Tracker_GoalManager::IDGOAL_ORDER - || $goalId == Piwik_Archive::LABEL_ECOMMERCE_ORDER - ) - { - $revenue += (int)$this->getColumn($columnValue, Piwik_Archive::INDEX_GOAL_REVENUE, Piwik_Archive::$mappingFromIdToNameGoal); - } - } - - if($revenue == 0) - { - $revenue = (int)$this->getColumn($currentColumns, Piwik_Archive::INDEX_REVENUE); - } - if(!isset($currentColumns['revenue_per_visit'])) - { - // If no visit for this metric, but some conversions, we still want to display some kind of "revenue per visit" - // even though it will actually be in this edge case "Revenue per conversion" - $revenuePerVisit = $this->invalidDivision; - if($nbVisits > 0 - || $conversions > 0) - { - $revenuePerVisit = round( $revenue / ($nbVisits == 0 ? $conversions : $nbVisits), $roundingPrecision ); - } - $newColumns['revenue_per_visit'] = $revenuePerVisit; - } - if($this->processOnlyIdGoal == self::GOALS_MINIMAL_REPORT) - { - $row->addColumns($newColumns); - continue; - } - // Display per goal metrics - // - conversion rate - // - conversions - // - revenue per visit - foreach($goals as $goalId => $columnValue) - { - $goalId = str_replace("idgoal=", "", $goalId); - if( ($this->processOnlyIdGoal > self::GOALS_FULL_TABLE - || $this->isEcommerce) - && $this->processOnlyIdGoal != $goalId) - { - continue; - } - $conversions = (int)$this->getColumn($columnValue, Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS, Piwik_Archive::$mappingFromIdToNameGoal); - - // Goal Conversion rate - $name = 'goal_' . $goalId . '_conversion_rate'; - if($nbVisits == 0) - { - $value = $this->invalidDivision; - } - else - { - $value = round(100 * $conversions / $nbVisits, $roundingPrecision); - } - $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] = $conversions; - $expectedColumns[$name] = true; - - // Goal Revenue per visit - $name = 'goal_' . $goalId . '_revenue_per_visit'; - // See comment above for $revenuePerVisit - $goalRevenue = (float)$this->getColumn($columnValue, Piwik_Archive::INDEX_GOAL_REVENUE, Piwik_Archive::$mappingFromIdToNameGoal); - $revenuePerVisit = round( $goalRevenue / ($nbVisits == 0 ? $conversions : $nbVisits), $roundingPrecision ); - $newColumns[$name] = $revenuePerVisit; - $expectedColumns[$name] = true; - - // Total revenue - $name = 'goal_' . $goalId . '_revenue'; - $newColumns[$name] = $goalRevenue; - $expectedColumns[$name] = true; - - if($this->isEcommerce ) - { - - // AOV Average Order Value - $name = 'goal_' . $goalId . '_avg_order_revenue'; - $newColumns[$name] = $goalRevenue / $conversions; - $expectedColumns[$name] = true; - - // Items qty - $name = 'goal_' . $goalId . '_items'; - $newColumns[$name] = $this->getColumn($columnValue, Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS, Piwik_Archive::$mappingFromIdToNameGoal); - $expectedColumns[$name] = true; - } - } - } - - // conversion_rate can be defined upstream apparently? FIXME - try { - $row->addColumns($newColumns); - }catch(Exception $e) { - } - } - $expectedColumns['revenue_per_visit'] = true; - - // make sure all goals values are set, 0 by default - // if no value then sorting would put at the end - $expectedColumns = array_keys($expectedColumns); - $rows = $table->getRows(); - foreach($rows as &$row) - { - foreach($expectedColumns as $name) - { - if(false === $row->getColumn($name)) - { - $value = 0; - if(strpos($name, 'conversion_rate') !== false) - { - $value = '0%'; - } - $row->addColumn($name, $value); - } - } - } - } + /** + * Process main goal metrics: conversion rate, revenue per visit + */ + const GOALS_MINIMAL_REPORT = -2; + + /** + * Process main goal metrics, and conversion rate per goal + */ + const GOALS_OVERVIEW = -1; + + /** + * Process all goal and per-goal metrics + */ + const GOALS_FULL_TABLE = 0; + + /** + * Adds processed goal metrics to a table: + * - global conversion rate, + * - global revenue per visit. + * Can also process per-goal metrics: + * - conversion rate + * - nb conversions + * - revenue per visit + * + * @param Piwik_DataTable $table + * @param bool $enable should be true (automatically set to true when filter_update_columns_when_show_all_goals is found in the API request) + * @param string $processOnlyIdGoal Defines what metrics to add (don't process metrics when you don't display them) + * If self::GOALS_FULL_TABLE, all Goal metrics (and per goal metrics) will be processed + * If self::GOALS_OVERVIEW, only the main goal metrics will be added + * If an int > 0, then will process only metrics for this specific Goal + * @return Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal + */ + public function __construct($table, $enable = true, $processOnlyIdGoal) + { + $this->processOnlyIdGoal = $processOnlyIdGoal; + $this->isEcommerce = $this->processOnlyIdGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER || $this->processOnlyIdGoal == Piwik_Archive::LABEL_ECOMMERCE_CART; + parent::__construct($table); + // Ensure that all rows with no visit but conversions will be displayed + $this->deleteRowsWithNoVisit = false; + } + + /** + * Filters the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + // Add standard processed metrics + parent::filter($table); + $roundingPrecision = Piwik_Tracker_GoalManager::REVENUE_PRECISION; + $expectedColumns = array(); + foreach ($table->getRows() as $key => $row) { + $currentColumns = $row->getColumns(); + $newColumns = array(); + + // visits could be undefined when there is a conversion but no visit + $nbVisits = (int)$this->getColumn($row, Piwik_Archive::INDEX_NB_VISITS); + $conversions = (int)$this->getColumn($row, Piwik_Archive::INDEX_NB_CONVERSIONS); + $goals = $this->getColumn($currentColumns, Piwik_Archive::INDEX_GOALS); + if ($goals) { + $revenue = 0; + foreach ($goals as $goalId => $columnValue) { + if ($goalId == Piwik_Archive::LABEL_ECOMMERCE_CART) { + continue; + } + if ($goalId >= Piwik_Tracker_GoalManager::IDGOAL_ORDER + || $goalId == Piwik_Archive::LABEL_ECOMMERCE_ORDER + ) { + $revenue += (int)$this->getColumn($columnValue, Piwik_Archive::INDEX_GOAL_REVENUE, Piwik_Archive::$mappingFromIdToNameGoal); + } + } + + if ($revenue == 0) { + $revenue = (int)$this->getColumn($currentColumns, Piwik_Archive::INDEX_REVENUE); + } + if (!isset($currentColumns['revenue_per_visit'])) { + // If no visit for this metric, but some conversions, we still want to display some kind of "revenue per visit" + // even though it will actually be in this edge case "Revenue per conversion" + $revenuePerVisit = $this->invalidDivision; + if ($nbVisits > 0 + || $conversions > 0 + ) { + $revenuePerVisit = round($revenue / ($nbVisits == 0 ? $conversions : $nbVisits), $roundingPrecision); + } + $newColumns['revenue_per_visit'] = $revenuePerVisit; + } + if ($this->processOnlyIdGoal == self::GOALS_MINIMAL_REPORT) { + $row->addColumns($newColumns); + continue; + } + // Display per goal metrics + // - conversion rate + // - conversions + // - revenue per visit + foreach ($goals as $goalId => $columnValue) { + $goalId = str_replace("idgoal=", "", $goalId); + if (($this->processOnlyIdGoal > self::GOALS_FULL_TABLE + || $this->isEcommerce) + && $this->processOnlyIdGoal != $goalId + ) { + continue; + } + $conversions = (int)$this->getColumn($columnValue, Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS, Piwik_Archive::$mappingFromIdToNameGoal); + + // Goal Conversion rate + $name = 'goal_' . $goalId . '_conversion_rate'; + if ($nbVisits == 0) { + $value = $this->invalidDivision; + } else { + $value = round(100 * $conversions / $nbVisits, $roundingPrecision); + } + $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] = $conversions; + $expectedColumns[$name] = true; + + // Goal Revenue per visit + $name = 'goal_' . $goalId . '_revenue_per_visit'; + // See comment above for $revenuePerVisit + $goalRevenue = (float)$this->getColumn($columnValue, Piwik_Archive::INDEX_GOAL_REVENUE, Piwik_Archive::$mappingFromIdToNameGoal); + $revenuePerVisit = round($goalRevenue / ($nbVisits == 0 ? $conversions : $nbVisits), $roundingPrecision); + $newColumns[$name] = $revenuePerVisit; + $expectedColumns[$name] = true; + + // Total revenue + $name = 'goal_' . $goalId . '_revenue'; + $newColumns[$name] = $goalRevenue; + $expectedColumns[$name] = true; + + if ($this->isEcommerce) { + + // AOV Average Order Value + $name = 'goal_' . $goalId . '_avg_order_revenue'; + $newColumns[$name] = $goalRevenue / $conversions; + $expectedColumns[$name] = true; + + // Items qty + $name = 'goal_' . $goalId . '_items'; + $newColumns[$name] = $this->getColumn($columnValue, Piwik_Archive::INDEX_GOAL_ECOMMERCE_ITEMS, Piwik_Archive::$mappingFromIdToNameGoal); + $expectedColumns[$name] = true; + } + } + } + + // conversion_rate can be defined upstream apparently? FIXME + try { + $row->addColumns($newColumns); + } catch (Exception $e) { + } + } + $expectedColumns['revenue_per_visit'] = true; + + // make sure all goals values are set, 0 by default + // if no value then sorting would put at the end + $expectedColumns = array_keys($expectedColumns); + $rows = $table->getRows(); + foreach ($rows as &$row) { + foreach ($expectedColumns as $name) { + if (false === $row->getColumn($name)) { + $value = 0; + if (strpos($name, 'conversion_rate') !== false) { + $value = '0%'; + } + $row->addColumn($name, $value); + } + } + } + } } diff --git a/core/DataTable/Filter/AddConstantMetadata.php b/core/DataTable/Filter/AddConstantMetadata.php index f0fc9040e2..4bff5aa813 100644 --- a/core/DataTable/Filter/AddConstantMetadata.php +++ b/core/DataTable/Filter/AddConstantMetadata.php @@ -1,17 +1,17 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Add a new metadata column to the table. - * + * * This is used to add a column containing the logo width and height of the countries flag icons. * This value is fixed for all icons so we simply add the same value for all rows. * @@ -20,30 +20,29 @@ */ class Piwik_DataTable_Filter_AddConstantMetadata extends Piwik_DataTable_Filter { - /** - * Creates a new filter and sets all required parameters - * - * @param Piwik_DataTable $table - * @param string $metadataName - * @param mixed $metadataValue - */ - public function __construct( $table, $metadataName, $metadataValue ) - { - parent::__construct($table); - $this->name = $metadataName; - $this->value = $metadataValue; - } + /** + * Creates a new filter and sets all required parameters + * + * @param Piwik_DataTable $table + * @param string $metadataName + * @param mixed $metadataValue + */ + public function __construct($table, $metadataName, $metadataValue) + { + parent::__construct($table); + $this->name = $metadataName; + $this->value = $metadataValue; + } - /** - * Filters the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $row) - { - $row->addMetadata($this->name, $this->value); - } - } + /** + * Filters the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $row) { + $row->addMetadata($this->name, $this->value); + } + } } diff --git a/core/DataTable/Filter/AddSummaryRow.php b/core/DataTable/Filter/AddSummaryRow.php index d9a9966022..5bb2a5e10c 100644 --- a/core/DataTable/Filter/AddSummaryRow.php +++ b/core/DataTable/Filter/AddSummaryRow.php @@ -27,70 +27,63 @@ */ class Piwik_DataTable_Filter_AddSummaryRow extends Piwik_DataTable_Filter { - /** - * Creates a new filter and set all required parameters - * - * @param Piwik_DataTable $table - * @param int $startRowToSummarize - * @param int $labelSummaryRow - * @param null $columnToSortByBeforeTruncating - * @param bool $deleteRows - */ - public function __construct($table, - $startRowToSummarize, - $labelSummaryRow = Piwik_DataTable::LABEL_SUMMARY_ROW, - $columnToSortByBeforeTruncating = null, - $deleteRows = true ) - { - parent::__construct($table); - $this->startRowToSummarize = $startRowToSummarize; - $this->labelSummaryRow = $labelSummaryRow; - $this->columnToSortByBeforeTruncating = $columnToSortByBeforeTruncating; - $this->deleteRows = $deleteRows; - } + /** + * Creates a new filter and set all required parameters + * + * @param Piwik_DataTable $table + * @param int $startRowToSummarize + * @param int $labelSummaryRow + * @param null $columnToSortByBeforeTruncating + * @param bool $deleteRows + */ + public function __construct($table, + $startRowToSummarize, + $labelSummaryRow = Piwik_DataTable::LABEL_SUMMARY_ROW, + $columnToSortByBeforeTruncating = null, + $deleteRows = true) + { + parent::__construct($table); + $this->startRowToSummarize = $startRowToSummarize; + $this->labelSummaryRow = $labelSummaryRow; + $this->columnToSortByBeforeTruncating = $columnToSortByBeforeTruncating; + $this->deleteRows = $deleteRows; + } - /** - * Adds a summary row to the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - if($table->getRowsCount() <= $this->startRowToSummarize + 1) - { - return; - } - $table->filter('Sort', - array( $this->columnToSortByBeforeTruncating, 'desc')); - - $rows = $table->getRows(); - $count = $table->getRowsCount(); - $newRow = new Piwik_DataTable_Row(); - for($i = $this->startRowToSummarize; $i < $count; $i++) - { - if(!isset($rows[$i])) - { - // case when the last row is a summary row, it is not indexed by $cout but by Piwik_DataTable::ID_SUMMARY_ROW - $summaryRow = $table->getRowFromId(Piwik_DataTable::ID_SUMMARY_ROW); - - //FIXME: I'm not sure why it could return false, but it was reported in: http://forum.piwik.org/read.php?2,89324,page=1#msg-89442 - if($summaryRow) - { - $newRow->sumRow($summaryRow, $enableCopyMetadata = false); - } - } - else - { - $newRow->sumRow($rows[$i], $enableCopyMetadata = false); - } - } - - $newRow->setColumns(array('label' => $this->labelSummaryRow) + $newRow->getColumns()); - if ($this->deleteRows) - { - $table->filter('Limit', array(0, $this->startRowToSummarize)); - } - $table->addSummaryRow($newRow); - unset($rows); - } + /** + * Adds a summary row to the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + if ($table->getRowsCount() <= $this->startRowToSummarize + 1) { + return; + } + $table->filter('Sort', + array($this->columnToSortByBeforeTruncating, 'desc')); + + $rows = $table->getRows(); + $count = $table->getRowsCount(); + $newRow = new Piwik_DataTable_Row(); + for ($i = $this->startRowToSummarize; $i < $count; $i++) { + if (!isset($rows[$i])) { + // case when the last row is a summary row, it is not indexed by $cout but by Piwik_DataTable::ID_SUMMARY_ROW + $summaryRow = $table->getRowFromId(Piwik_DataTable::ID_SUMMARY_ROW); + + //FIXME: I'm not sure why it could return false, but it was reported in: http://forum.piwik.org/read.php?2,89324,page=1#msg-89442 + if ($summaryRow) { + $newRow->sumRow($summaryRow, $enableCopyMetadata = false); + } + } else { + $newRow->sumRow($rows[$i], $enableCopyMetadata = false); + } + } + + $newRow->setColumns(array('label' => $this->labelSummaryRow) + $newRow->getColumns()); + if ($this->deleteRows) { + $table->filter('Limit', array(0, $this->startRowToSummarize)); + } + $table->addSummaryRow($newRow); + unset($rows); + } } diff --git a/core/DataTable/Filter/BeautifyRangeLabels.php b/core/DataTable/Filter/BeautifyRangeLabels.php index 4b8a8a3709..329bcf3308 100644 --- a/core/DataTable/Filter/BeautifyRangeLabels.php +++ b/core/DataTable/Filter/BeautifyRangeLabels.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -28,145 +28,132 @@ */ class Piwik_DataTable_Filter_BeautifyRangeLabels extends Piwik_DataTable_Filter_ColumnCallbackReplace { - /** - * The string to use when the range being beautified is between 1-1 units. - * @var string - */ - protected $labelSingular; + /** + * The string to use when the range being beautified is between 1-1 units. + * @var string + */ + protected $labelSingular; + + /** + * The format string to use when the range being beautified references more than + * one unit. + * @var string + */ + protected $labelPlural; + + /** + * Constructor. + * + * @param Piwik_DataTable $table The DataTable that will be filtered. + * @param string $labelSingular The string to use when the range being beautified + * is equal to '1-1 units'. + * @param string $labelPlural The string to use when the range being beautified + * references more than one unit. This must be a format + * string that takes one string parameter. + */ + public function __construct($table, $labelSingular, $labelPlural) + { + parent::__construct($table, 'label', array($this, 'beautify'), array()); + + $this->labelSingular = $labelSingular; + $this->labelPlural = $labelPlural; + } + + /** + * Beautifies a range label and returns the pretty result. + * + * @param string $value The range string. This must be in either a '$min-$max' format + * a '$min+' format. + * @return string The pretty range label. + */ + public function beautify($value) + { + // if there's more than one element, handle as a range w/ an upper bound + if (strpos($value, "-") !== false) { + // get the range + sscanf($value, "%d - %d", $lowerBound, $upperBound); + + // if the lower bound is the same as the upper bound make sure the singular label + // is used + if ($lowerBound == $upperBound) { + return $this->getSingleUnitLabel($value, $lowerBound); + } else { + return $this->getRangeLabel($value, $lowerBound, $upperBound); + } + } // if there's one element, handle as a range w/ no upper bound + else { + // get the lower bound + sscanf($value, "%d", $lowerBound); - /** - * The format string to use when the range being beautified references more than - * one unit. - * @var string - */ - protected $labelPlural; + if ($lowerBound !== NULL) { + $plusEncoded = urlencode('+'); + $plusLen = strlen($plusEncoded); + $len = strlen($value); - /** - * Constructor. - * - * @param Piwik_DataTable $table The DataTable that will be filtered. - * @param string $labelSingular The string to use when the range being beautified - * is equal to '1-1 units'. - * @param string $labelPlural The string to use when the range being beautified - * references more than one unit. This must be a format - * string that takes one string parameter. - */ - public function __construct( $table, $labelSingular, $labelPlural ) - { - parent::__construct($table, 'label', array($this, 'beautify'), array()); - - $this->labelSingular = $labelSingular; - $this->labelPlural = $labelPlural; - } + // if the label doesn't end with a '+', append it + if ($len < $plusLen || substr($value, $len - $plusLen) != $plusEncoded) { + $value .= $plusEncoded; + } - /** - * Beautifies a range label and returns the pretty result. - * - * @param string $value The range string. This must be in either a '$min-$max' format - * a '$min+' format. - * @return string The pretty range label. - */ - public function beautify( $value ) - { - // if there's more than one element, handle as a range w/ an upper bound - if (strpos($value, "-") !== false) - { - // get the range - sscanf($value, "%d - %d", $lowerBound, $upperBound); - - // if the lower bound is the same as the upper bound make sure the singular label - // is used - if ($lowerBound == $upperBound) - { - return $this->getSingleUnitLabel($value, $lowerBound); - } - else - { - return $this->getRangeLabel($value, $lowerBound, $upperBound); - } - } - // if there's one element, handle as a range w/ no upper bound - else - { - // get the lower bound - sscanf($value, "%d", $lowerBound); - - if ($lowerBound !== NULL) - { - $plusEncoded = urlencode('+'); - $plusLen = strlen($plusEncoded); - $len = strlen($value); + return $this->getUnboundedLabel($value, $lowerBound); + } else { + // if no lower bound can be found, this isn't a valid range. in this case + // we assume its a translation key and try to translate it. + return Piwik_Translate(trim($value)); + } + } + } - // if the label doesn't end with a '+', append it - if ($len < $plusLen || substr($value, $len - $plusLen) != $plusEncoded) - { - $value .= $plusEncoded; - } - - return $this->getUnboundedLabel($value, $lowerBound); - } - else - { - // if no lower bound can be found, this isn't a valid range. in this case - // we assume its a translation key and try to translate it. - return Piwik_Translate(trim($value)); - } - } - } + /** + * Beautifies and returns a range label whose range spans over one unit, ie + * 1-1, 2-2 or 3-3. + * + * This function can be overridden in derived types to customize beautifcation + * behavior based on the range values. + * + * @param string $oldLabel The original label value. + * @param int $lowerBound The lower bound of the range. + * @return string The pretty range label. + */ + public function getSingleUnitLabel($oldLabel, $lowerBound) + { + if ($lowerBound == 1) { + return $this->labelSingular; + } else { + return sprintf($this->labelPlural, $lowerBound); + } + } - /** - * Beautifies and returns a range label whose range spans over one unit, ie - * 1-1, 2-2 or 3-3. - * - * This function can be overridden in derived types to customize beautifcation - * behavior based on the range values. - * - * @param string $oldLabel The original label value. - * @param int $lowerBound The lower bound of the range. - * @return string The pretty range label. - */ - public function getSingleUnitLabel( $oldLabel, $lowerBound ) - { - if ($lowerBound == 1) - { - return $this->labelSingular; - } - else - { - return sprintf($this->labelPlural, $lowerBound); - } - } + /** + * Beautifies and returns a range label whose range is bounded and spans over + * more than one unit, ie 1-5, 5-10 but NOT 11+. + * + * This function can be overridden in derived types to customize beautifcation + * behavior based on the range values. + * + * @param string $oldLabel The original label value. + * @param int $lowerBound The lower bound of the range. + * @param int $upperBound The upper bound of the range. + * @return string The pretty range label. + */ + public function getRangeLabel($oldLabel, $lowerBound, $upperBound) + { + return sprintf($this->labelPlural, $oldLabel); + } - /** - * Beautifies and returns a range label whose range is bounded and spans over - * more than one unit, ie 1-5, 5-10 but NOT 11+. - * - * This function can be overridden in derived types to customize beautifcation - * behavior based on the range values. - * - * @param string $oldLabel The original label value. - * @param int $lowerBound The lower bound of the range. - * @param int $upperBound The upper bound of the range. - * @return string The pretty range label. - */ - public function getRangeLabel( $oldLabel, $lowerBound, $upperBound ) - { - return sprintf($this->labelPlural, $oldLabel); - } - - /** - * Beautifies and returns a range label whose range is unbounded, ie - * 5+, 10+, etc. - * - * This function can be overridden in derived types to customize beautifcation - * behavior based on the range values. - * - * @param string $oldLabel The original label value. - * @param int $lowerBound The lower bound of the range. - * @return string The pretty range label. - */ - public function getUnboundedLabel( $oldLabel, $lowerBound ) - { - return sprintf($this->labelPlural, $oldLabel); - } + /** + * Beautifies and returns a range label whose range is unbounded, ie + * 5+, 10+, etc. + * + * This function can be overridden in derived types to customize beautifcation + * behavior based on the range values. + * + * @param string $oldLabel The original label value. + * @param int $lowerBound The lower bound of the range. + * @return string The pretty range label. + */ + public function getUnboundedLabel($oldLabel, $lowerBound) + { + return sprintf($this->labelPlural, $oldLabel); + } } diff --git a/core/DataTable/Filter/BeautifyTimeRangeLabels.php b/core/DataTable/Filter/BeautifyTimeRangeLabels.php index 57d00c6490..1a2dffce5a 100644 --- a/core/DataTable/Filter/BeautifyTimeRangeLabels.php +++ b/core/DataTable/Filter/BeautifyTimeRangeLabels.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -19,107 +19,96 @@ */ class Piwik_DataTable_Filter_BeautifyTimeRangeLabels extends Piwik_DataTable_Filter_BeautifyRangeLabels { - /** - * A format string used to create pretty range labels when the range's - * lower bound is between 0 and 60. - * - * This format string must take two numeric parameters, one for each - * range bound. - */ - protected $labelSecondsPlural; + /** + * A format string used to create pretty range labels when the range's + * lower bound is between 0 and 60. + * + * This format string must take two numeric parameters, one for each + * range bound. + */ + protected $labelSecondsPlural; + + /** + * Constructor. + * + * @param Piwik_DataTable $table The DataTable this filter will run over. + * @param string $labelSecondsPlural A string to use when beautifying range labels + * whose lower bound is between 0 and 60. Must be + * a format string that takes two numeric params. + * @param string $labelMinutesSingular A string to use when replacing a range that + * equals 60-60 (or 1 minute - 1 minute). + * @param string $labelMinutesPlural A string to use when replacing a range that + * spans multiple minutes. This must be a + * format string that takes one string parameter. + */ + public function __construct($table, $labelSecondsPlural, $labelMinutesSingular, $labelMinutesPlural) + { + parent::__construct($table, $labelMinutesSingular, $labelMinutesPlural); + + $this->labelSecondsPlural = $labelSecondsPlural; + } - /** - * Constructor. - * - * @param Piwik_DataTable $table The DataTable this filter will run over. - * @param string $labelSecondsPlural A string to use when beautifying range labels - * whose lower bound is between 0 and 60. Must be - * a format string that takes two numeric params. - * @param string $labelMinutesSingular A string to use when replacing a range that - * equals 60-60 (or 1 minute - 1 minute). - * @param string $labelMinutesPlural A string to use when replacing a range that - * spans multiple minutes. This must be a - * format string that takes one string parameter. - */ - public function __construct( $table, $labelSecondsPlural, $labelMinutesSingular, $labelMinutesPlural ) - { - parent::__construct($table, $labelMinutesSingular, $labelMinutesPlural); - - $this->labelSecondsPlural = $labelSecondsPlural; - } + /** + * Beautifies and returns a range label whose range spans over one unit, ie + * 1-1, 2-2 or 3-3. + * + * If the lower bound of the range is less than 60 the pretty range label + * will be in seconds. Otherwise, it will be in minutes. + * + * @param string $oldLabel The original label value. + * @param int $lowerBound The lower bound of the range. + * @return string The pretty range label. + */ + public function getSingleUnitLabel($oldLabel, $lowerBound) + { + if ($lowerBound < 60) { + return sprintf($this->labelSecondsPlural, $lowerBound, $lowerBound); + } else if ($lowerBound == 60) { + return $this->labelSingular; + } else { + return sprintf($this->labelPlural, ceil($lowerBound / 60)); + } + } - /** - * Beautifies and returns a range label whose range spans over one unit, ie - * 1-1, 2-2 or 3-3. - * - * If the lower bound of the range is less than 60 the pretty range label - * will be in seconds. Otherwise, it will be in minutes. - * - * @param string $oldLabel The original label value. - * @param int $lowerBound The lower bound of the range. - * @return string The pretty range label. - */ - public function getSingleUnitLabel( $oldLabel, $lowerBound ) - { - if ($lowerBound < 60) - { - return sprintf($this->labelSecondsPlural, $lowerBound, $lowerBound); - } - else if ($lowerBound == 60) - { - return $this->labelSingular; - } - else - { - return sprintf($this->labelPlural, ceil($lowerBound / 60)); - } - } - - /** - * Beautifies and returns a range label whose range is bounded and spans over - * more than one unit, ie 1-5, 5-10 but NOT 11+. - * - * If the lower bound of the range is less than 60 the pretty range label - * will be in seconds. Otherwise, it will be in minutes. - * - * @param string $oldLabel The original label value. - * @param int $lowerBound The lower bound of the range. - * @param int $upperBound The upper bound of the range. - * @return string The pretty range label. - */ - public function getRangeLabel( $oldLabel, $lowerBound, $upperBound ) - { - if ($lowerBound < 60) - { - return sprintf($this->labelSecondsPlural, $lowerBound, $upperBound); - } - else - { - return sprintf($this->labelPlural, ceil($lowerBound / 60)."-".ceil($upperBound / 60)); - } - } + /** + * Beautifies and returns a range label whose range is bounded and spans over + * more than one unit, ie 1-5, 5-10 but NOT 11+. + * + * If the lower bound of the range is less than 60 the pretty range label + * will be in seconds. Otherwise, it will be in minutes. + * + * @param string $oldLabel The original label value. + * @param int $lowerBound The lower bound of the range. + * @param int $upperBound The upper bound of the range. + * @return string The pretty range label. + */ + public function getRangeLabel($oldLabel, $lowerBound, $upperBound) + { + if ($lowerBound < 60) { + return sprintf($this->labelSecondsPlural, $lowerBound, $upperBound); + } else { + return sprintf($this->labelPlural, ceil($lowerBound / 60) . "-" . ceil($upperBound / 60)); + } + } - /** - * Beautifies and returns a range label whose range is unbounded, ie - * 5+, 10+, etc. - * - * If the lower bound of the range is less than 60 the pretty range label - * will be in seconds. Otherwise, it will be in minutes. - * - * @param string $oldLabel The original label value. - * @param int $lowerBound The lower bound of the range. - * @return string The pretty range label. - */ - public function getUnboundedLabel( $oldLabel, $lowerBound ) - { - if ($lowerBound < 60) - { - return sprintf($this->labelSecondsPlural, $lowerBound); - } - else - { - // since we're using minutes, we use floor so 1801s+ will be 30m+ and not 31m+ - return sprintf($this->labelPlural, "".floor($lowerBound / 60).urlencode('+')); - } - } + /** + * Beautifies and returns a range label whose range is unbounded, ie + * 5+, 10+, etc. + * + * If the lower bound of the range is less than 60 the pretty range label + * will be in seconds. Otherwise, it will be in minutes. + * + * @param string $oldLabel The original label value. + * @param int $lowerBound The lower bound of the range. + * @return string The pretty range label. + */ + public function getUnboundedLabel($oldLabel, $lowerBound) + { + if ($lowerBound < 60) { + return sprintf($this->labelSecondsPlural, $lowerBound); + } else { + // since we're using minutes, we use floor so 1801s+ will be 30m+ and not 31m+ + return sprintf($this->labelPlural, "" . floor($lowerBound / 60) . urlencode('+')); + } + } } diff --git a/core/DataTable/Filter/CalculateEvolutionFilter.php b/core/DataTable/Filter/CalculateEvolutionFilter.php index 675a3cf8ef..2cacaf2698 100755 --- a/core/DataTable/Filter/CalculateEvolutionFilter.php +++ b/core/DataTable/Filter/CalculateEvolutionFilter.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,7 +12,7 @@ /** * A DataTable filter that calculates the evolution of a metric and adds * it to each row as a percentage. - * + * * This filter cannot be used as a normal filter since it requires * corresponding data from another datatable. Instead, to use it, * you must manually perform a binary filter (see the MultiSites API). @@ -22,122 +22,119 @@ */ class Piwik_DataTable_Filter_CalculateEvolutionFilter extends Piwik_DataTable_Filter_ColumnCallbackAddColumnPercentage { - /** - * The the DataTable that contains past data. - */ - private $pastDataTable; - - /** - * Tells if column being added is the revenue evolution column. - */ - private $isRevenueEvolution = null; - - /** - * Constructor. - * - * @param Piwik_DataTable $table The DataTable being filtered. - * @param string $columnToAdd - * @param string $columnToRead - * @param int $quotientPrecision - */ - function __construct($table, $pastDataTable, $columnToAdd, $columnToRead, $quotientPrecision = 0) - { - parent::__construct( - $table, $columnToAdd, $columnToRead, $columnToRead, $quotientPrecision, $shouldSkipRows = true); - - $this->pastDataTable = $pastDataTable; - - $this->isRevenueEvolution = $columnToAdd == 'revenue_evolution'; - } - - /** - * Returns the difference between the column in the specific row and its - * sister column in the past DataTable. - * - * @param Piwik_DataTable_Row $row - * @return int|float - */ - protected function getDividend($row) - { - $currentValue = $row->getColumn($this->columnValueToRead); - - // if the site this is for doesn't support ecommerce & this is for the revenue_evolution column, - // we don't add the new column - if ($currentValue === false - && $this->isRevenueEvolution - && !Piwik_Site::isEcommerceEnabledFor($row->getColumn('label'))) - { - return false; - } - - $pastRow = $this->getPastRowFromCurrent($row); - if ($pastRow) - { - $pastValue = $pastRow->getColumn($this->columnValueToRead); - } - else - { - $pastValue = 0; - } - - return $currentValue - $pastValue; - } - - /** - * Returns the value of the column in $row's sister row in the past - * DataTable. - * - * @param Piwik_DataTable_Row $row - * @return int|float - */ - protected function getDivisor($row) - { - $pastRow = $this->getPastRowFromCurrent($row); - if (!$pastRow) return 0; - - return $pastRow->getColumn($this->columnNameUsedAsDivisor); - } - - /** - * Calculates and formats a quotient based on a divisor and dividend. - * - * Unlike Piwik_DataTable_Filter_ColumnCallbackAddColumnPercentage's, - * version of this method, this method will return 100% if the past - * value of a metric is 0, and the current value is not 0. For a - * value representative of an evolution, this makes sense. - * - * @param int|float $value The dividend. - * @param int|float $divisor - * @return string - */ - protected function formatValue($value, $divisor) - { - $value = self::getPercentageValue($value, $divisor, $this->quotientPrecision); + /** + * The the DataTable that contains past data. + */ + private $pastDataTable; + + /** + * Tells if column being added is the revenue evolution column. + */ + private $isRevenueEvolution = null; + + /** + * Constructor. + * + * @param Piwik_DataTable $table The DataTable being filtered. + * @param string $columnToAdd + * @param string $columnToRead + * @param int $quotientPrecision + */ + function __construct($table, $pastDataTable, $columnToAdd, $columnToRead, $quotientPrecision = 0) + { + parent::__construct( + $table, $columnToAdd, $columnToRead, $columnToRead, $quotientPrecision, $shouldSkipRows = true); + + $this->pastDataTable = $pastDataTable; + + $this->isRevenueEvolution = $columnToAdd == 'revenue_evolution'; + } + + /** + * Returns the difference between the column in the specific row and its + * sister column in the past DataTable. + * + * @param Piwik_DataTable_Row $row + * @return int|float + */ + protected function getDividend($row) + { + $currentValue = $row->getColumn($this->columnValueToRead); + + // if the site this is for doesn't support ecommerce & this is for the revenue_evolution column, + // we don't add the new column + if ($currentValue === false + && $this->isRevenueEvolution + && !Piwik_Site::isEcommerceEnabledFor($row->getColumn('label')) + ) { + return false; + } + + $pastRow = $this->getPastRowFromCurrent($row); + if ($pastRow) { + $pastValue = $pastRow->getColumn($this->columnValueToRead); + } else { + $pastValue = 0; + } + + return $currentValue - $pastValue; + } + + /** + * Returns the value of the column in $row's sister row in the past + * DataTable. + * + * @param Piwik_DataTable_Row $row + * @return int|float + */ + protected function getDivisor($row) + { + $pastRow = $this->getPastRowFromCurrent($row); + if (!$pastRow) return 0; + + return $pastRow->getColumn($this->columnNameUsedAsDivisor); + } + + /** + * Calculates and formats a quotient based on a divisor and dividend. + * + * Unlike Piwik_DataTable_Filter_ColumnCallbackAddColumnPercentage's, + * version of this method, this method will return 100% if the past + * value of a metric is 0, and the current value is not 0. For a + * value representative of an evolution, this makes sense. + * + * @param int|float $value The dividend. + * @param int|float $divisor + * @return string + */ + protected function formatValue($value, $divisor) + { + $value = self::getPercentageValue($value, $divisor, $this->quotientPrecision); $value = self::appendPercentSign($value); return $value; - } - - /** - * Utility function. Returns the current row in the past DataTable. - * - * @param Piwik_DataTable_Row $row The row in the 'current' DataTable. - */ - private function getPastRowFromCurrent($row) - { - return $this->pastDataTable->getRowFromLabel($row->getColumn('label')); - } - - /** - * Calculates the evolution percentage for two arbitrary values. - * - * @param numeric $currentValue The current metric value. - * @param numeric $pastValue The value of the metric in the past. We measure the % change - * from this value to $currentValue. + } + + /** + * Utility function. Returns the current row in the past DataTable. + * + * @param Piwik_DataTable_Row $row The row in the 'current' DataTable. + */ + private function getPastRowFromCurrent($row) + { + return $this->pastDataTable->getRowFromLabel($row->getColumn('label')); + } + + /** + * Calculates the evolution percentage for two arbitrary values. + * + * @param numeric $currentValue The current metric value. + * @param numeric $pastValue The value of the metric in the past. We measure the % change + * from this value to $currentValue. * @param numeric $quotientPrecision The quotient precision to round to. * @return string The evolution percent 15% */ public static function calculate($currentValue, $pastValue, $quotientPrecision = 0) - { + { $number = self::getPercentageValue($currentValue - $pastValue, $pastValue, $quotientPrecision); $number = self::appendPercentSign($number); return $number; @@ -152,7 +149,7 @@ class Piwik_DataTable_Filter_CalculateEvolutionFilter extends Piwik_DataTable_Fi public static function prependPlusSignToNumber($number) { if ($number > 0) { - $number = '+'.$number; + $number = '+' . $number; } return $number; } @@ -161,17 +158,12 @@ class Piwik_DataTable_Filter_CalculateEvolutionFilter extends Piwik_DataTable_Fi * Returns an evolution percent based on a value & divisor. */ private static function getPercentageValue($value, $divisor, $quotientPrecision) - { - if($value == 0) - { + { + if ($value == 0) { $evolution = 0; - } - elseif($divisor == 0) - { + } elseif ($divisor == 0) { $evolution = 100; - } - else - { + } else { $evolution = ($value / $divisor) * 100; } diff --git a/core/DataTable/Filter/ColumnCallbackAddColumn.php b/core/DataTable/Filter/ColumnCallbackAddColumn.php index 64882283c5..4aa8b008e4 100755 --- a/core/DataTable/Filter/ColumnCallbackAddColumn.php +++ b/core/DataTable/Filter/ColumnCallbackAddColumn.php @@ -1,89 +1,86 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Adds a new column to every row of a DataTable based on the result of callback. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_ColumnCallbackAddColumn extends Piwik_DataTable_Filter { - /** - * The names of the columns to pass to the callback. - */ - private $columns; - - /** - * The name of the column to add. - */ - private $columnToAdd; - - /** - * The callback to apply to each row of the DataTable. The result is added as - * the value of a new column. - */ - private $functionToApply; - - /** - * Extra parameters to pass to the callback. - */ - private $functionParameters; - - /** - * Constructor. - * - * @param Piwik_DataTable $table The DataTable that will be filtered. - * @param array|string $columns The names of the columns to pass to the callback. - * @param string $columnToAdd The name of the column to add. - * @param mixed $functionToApply The callback to apply to each row of a DataTable. - * @param array $functionParameters Extra parameters to pass to $functionToApply. - */ - public function __construct( $table, $columns, $columnToAdd, $functionToApply, $functionParameters = array() ) - { - parent::__construct($table); - - if (!is_array($columns)) - { - $columns = array($columns); - } - - $this->columns = $columns; - $this->columnToAdd = $columnToAdd; - $this->functionToApply = $functionToApply; - $this->functionParameters = $functionParameters; - } - - /** - * Executes a callback on every row of the supplied table and adds the result of - * the callback as a new column to each row. - * - * @param Piwik_DataTable $table The table to filter. - */ - public function filter( $table ) - { - foreach ($table->getRows() as $row) - { - $columnValues = array(); - foreach ($this->columns as $column) - { - $columnValues[] = $row->getColumn($column); - } - - $parameters = array_merge($columnValues, $this->functionParameters); - $value = call_user_func_array($this->functionToApply, $parameters); - - $row->setColumn($this->columnToAdd, $value); - - $this->filterSubTable($row); - } - } + /** + * The names of the columns to pass to the callback. + */ + private $columns; + + /** + * The name of the column to add. + */ + private $columnToAdd; + + /** + * The callback to apply to each row of the DataTable. The result is added as + * the value of a new column. + */ + private $functionToApply; + + /** + * Extra parameters to pass to the callback. + */ + private $functionParameters; + + /** + * Constructor. + * + * @param Piwik_DataTable $table The DataTable that will be filtered. + * @param array|string $columns The names of the columns to pass to the callback. + * @param string $columnToAdd The name of the column to add. + * @param mixed $functionToApply The callback to apply to each row of a DataTable. + * @param array $functionParameters Extra parameters to pass to $functionToApply. + */ + public function __construct($table, $columns, $columnToAdd, $functionToApply, $functionParameters = array()) + { + parent::__construct($table); + + if (!is_array($columns)) { + $columns = array($columns); + } + + $this->columns = $columns; + $this->columnToAdd = $columnToAdd; + $this->functionToApply = $functionToApply; + $this->functionParameters = $functionParameters; + } + + /** + * Executes a callback on every row of the supplied table and adds the result of + * the callback as a new column to each row. + * + * @param Piwik_DataTable $table The table to filter. + */ + public function filter($table) + { + foreach ($table->getRows() as $row) { + $columnValues = array(); + foreach ($this->columns as $column) { + $columnValues[] = $row->getColumn($column); + } + + $parameters = array_merge($columnValues, $this->functionParameters); + $value = call_user_func_array($this->functionToApply, $parameters); + + $row->setColumn($this->columnToAdd, $value); + + $this->filterSubTable($row); + } + } } diff --git a/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php b/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php index 71e1764668..99284508e4 100644 --- a/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php +++ b/core/DataTable/Filter/ColumnCallbackAddColumnPercentage.php @@ -1,22 +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 - * + * * @category Piwik * @package Piwik */ /** - * Add a new column to the table which is a percentage based on the value resulting + * Add a new column to the table which is a percentage based on the value resulting * from a callback function with the parameter being another column's value - * - * For example in the keywords table, we can create a "nb_visits_percentage" column + * + * For example in the keywords table, we can create a "nb_visits_percentage" column * from the "nb_visits" column that will be nb_visits / $totalValueUsedToComputePercentage * You can also specify the precision of the percentage value to be displayed (defaults to 0, eg "11%") - * + * * Usage: * $nbVisits = Piwik_VisitsSummary_API::getInstance()->getVisits($idSite, $period, $date); * $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('nb_visits', 'nb_visits_percentage', $nbVisits, 1)); @@ -26,15 +26,15 @@ */ class Piwik_DataTable_Filter_ColumnCallbackAddColumnPercentage extends Piwik_DataTable_Filter_ColumnCallbackAddColumnQuotient { - /** - * Formats the given value - * - * @param number $value - * @param number $divisor - * @return string - */ - protected function formatValue($value, $divisor) - { - return Piwik::getPercentageSafe($value, $divisor, $this->quotientPrecision) . '%'; - } + /** + * Formats the given value + * + * @param number $value + * @param number $divisor + * @return string + */ + protected function formatValue($value, $divisor) + { + return Piwik::getPercentageSafe($value, $divisor, $this->quotientPrecision) . '%'; + } } diff --git a/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php b/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php index 83aba2d437..b5573bb731 100644 --- a/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php +++ b/core/DataTable/Filter/ColumnCallbackAddColumnQuotient.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,131 +12,119 @@ /** * 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 $table; - protected $columnValueToRead; - protected $columnNameToAdd; - protected $columnNameUsedAsDivisor; - protected $totalValueUsedAsDivisor; - protected $quotientPrecision; - protected $shouldSkipRows; - protected $getDivisorFromSummaryRow; - - /** - * @param Piwik_DataTable $table - * @param string $columnNameToAdd - * @param string $columnValueToRead - * @param number|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 int $quotientPrecision Division precision - * @param bool|number $shouldSkipRows Whether rows w/o the column to read should be skipped. - * @param bool $getDivisorFromSummaryRow Whether to get the divisor from the summary row or the current row. - */ - public function __construct( $table, $columnNameToAdd, $columnValueToRead, $divisorValueOrDivisorColumnName, $quotientPrecision = 0, $shouldSkipRows = false, $getDivisorFromSummaryRow = false) - { - parent::__construct($table); - $this->table = $table; - $this->columnValueToRead = $columnValueToRead; - $this->columnNameToAdd = $columnNameToAdd; - if(is_numeric($divisorValueOrDivisorColumnName)) - { - $this->totalValueUsedAsDivisor = $divisorValueOrDivisorColumnName; - } - else - { - $this->columnNameUsedAsDivisor = $divisorValueOrDivisorColumnName; - } - $this->quotientPrecision = $quotientPrecision; - $this->shouldSkipRows = $shouldSkipRows; - $this->getDivisorFromSummaryRow = $getDivisorFromSummaryRow; - } + protected $table; + protected $columnValueToRead; + protected $columnNameToAdd; + protected $columnNameUsedAsDivisor; + protected $totalValueUsedAsDivisor; + protected $quotientPrecision; + protected $shouldSkipRows; + protected $getDivisorFromSummaryRow; - /** - * Filters the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $key => $row) - { - $existingValue = $row->getColumn($this->columnNameToAdd); - if($existingValue !== false) - { - continue; - } + /** + * @param Piwik_DataTable $table + * @param string $columnNameToAdd + * @param string $columnValueToRead + * @param number|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 int $quotientPrecision Division precision + * @param bool|number $shouldSkipRows Whether rows w/o the column to read should be skipped. + * @param bool $getDivisorFromSummaryRow Whether to get the divisor from the summary row or the current row. + */ + public function __construct($table, $columnNameToAdd, $columnValueToRead, $divisorValueOrDivisorColumnName, $quotientPrecision = 0, $shouldSkipRows = false, $getDivisorFromSummaryRow = false) + { + parent::__construct($table); + $this->table = $table; + $this->columnValueToRead = $columnValueToRead; + $this->columnNameToAdd = $columnNameToAdd; + if (is_numeric($divisorValueOrDivisorColumnName)) { + $this->totalValueUsedAsDivisor = $divisorValueOrDivisorColumnName; + } else { + $this->columnNameUsedAsDivisor = $divisorValueOrDivisorColumnName; + } + $this->quotientPrecision = $quotientPrecision; + $this->shouldSkipRows = $shouldSkipRows; + $this->getDivisorFromSummaryRow = $getDivisorFromSummaryRow; + } - $value = $this->getDividend($row); - if ($value === false && $this->shouldSkipRows) - { - continue; - } + /** + * Filters the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $key => $row) { + $existingValue = $row->getColumn($this->columnNameToAdd); + if ($existingValue !== false) { + continue; + } - $divisor = $this->getDivisor($row); + $value = $this->getDividend($row); + if ($value === false && $this->shouldSkipRows) { + continue; + } - $formattedValue = $this->formatValue($value, $divisor); - $row->addColumn($this->columnNameToAdd, $formattedValue); - - $this->filterSubTable($row); - } - } + $divisor = $this->getDivisor($row); - /** - * Formats the given value - * - * @param number $value - * @param number $divisor - * @return float|int - */ - protected function formatValue($value, $divisor) - { - $quotient = 0; - if($divisor > 0 && $value > 0) - { - $quotient = round($value / $divisor, $this->quotientPrecision); - } - return $quotient; - } - - /** - * Returns the dividend to use when calculating the new column value. Can - * be overridden by descendent classes to customize behavior. - * - * @param Piwik_DataTable_Row $row The row being modified. - * @return int|float - */ - protected function getDividend($row) - { - return $row->getColumn($this->columnValueToRead); - } + $formattedValue = $this->formatValue($value, $divisor); + $row->addColumn($this->columnNameToAdd, $formattedValue); - /** - * Returns the divisor to use when calculating the new column value. Can - * be overridden by descendent classes to customize behavior. - * - * @param Piwik_DataTable_Row $row The row being modified. - * @return int|float - */ - protected function getDivisor($row) - { - if(!is_null($this->totalValueUsedAsDivisor)) - { - return $this->totalValueUsedAsDivisor; - } - else if ($this->getDivisorFromSummaryRow) - { - $summaryRow = $this->table->getRowFromId(Piwik_DataTable::ID_SUMMARY_ROW); - return $summaryRow->getColumn($this->columnNameUsedAsDivisor); - } - else - { - return $row->getColumn($this->columnNameUsedAsDivisor); - } - } + $this->filterSubTable($row); + } + } + + /** + * Formats the given value + * + * @param number $value + * @param number $divisor + * @return float|int + */ + protected function formatValue($value, $divisor) + { + $quotient = 0; + if ($divisor > 0 && $value > 0) { + $quotient = round($value / $divisor, $this->quotientPrecision); + } + return $quotient; + } + + /** + * Returns the dividend to use when calculating the new column value. Can + * be overridden by descendent classes to customize behavior. + * + * @param Piwik_DataTable_Row $row The row being modified. + * @return int|float + */ + protected function getDividend($row) + { + return $row->getColumn($this->columnValueToRead); + } + + /** + * Returns the divisor to use when calculating the new column value. Can + * be overridden by descendent classes to customize behavior. + * + * @param Piwik_DataTable_Row $row The row being modified. + * @return int|float + */ + protected function getDivisor($row) + { + if (!is_null($this->totalValueUsedAsDivisor)) { + return $this->totalValueUsedAsDivisor; + } else if ($this->getDivisorFromSummaryRow) { + $summaryRow = $this->table->getRowFromId(Piwik_DataTable::ID_SUMMARY_ROW); + return $summaryRow->getColumn($this->columnNameUsedAsDivisor); + } else { + return $row->getColumn($this->columnNameUsedAsDivisor); + } + } } diff --git a/core/DataTable/Filter/ColumnCallbackAddMetadata.php b/core/DataTable/Filter/ColumnCallbackAddMetadata.php index df35c0da00..27bd0170b5 100644 --- a/core/DataTable/Filter/ColumnCallbackAddMetadata.php +++ b/core/DataTable/Filter/ColumnCallbackAddMetadata.php @@ -1,82 +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 - * + * * @category Piwik * @package Piwik */ /** - * Add a new 'metadata' column to the table based on the value resulting + * Add a new 'metadata' column to the table based on the value resulting * from a callback function with the parameter being another column's value - * - * For example from the "label" column we can to create an "icon" 'metadata' column + * + * For example from the "label" column we can to create an "icon" 'metadata' column * with the icon URI built from the label (LINUX => UserSettings/icons/linux.png) - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_ColumnCallbackAddMetadata extends Piwik_DataTable_Filter { - private $columnToRead; - private $functionToApply; - private $functionParameters; - private $metadataToAdd; - private $applyToSummaryRow; + private $columnToRead; + private $functionToApply; + private $functionParameters; + private $metadataToAdd; + private $applyToSummaryRow; - /** - * @param Piwik_DataTable $table - * @param $columnToRead - * @param $metadataToAdd - * @param null $functionToApply - * @param null $functionParameters - */ - public function __construct( $table, $columnToRead, $metadataToAdd, $functionToApply = null, - $functionParameters = null, $applyToSummaryRow = true ) - { - parent::__construct($table); - $this->functionToApply = $functionToApply; - $this->functionParameters = $functionParameters; - $this->columnToRead = $columnToRead; - $this->metadataToAdd = $metadataToAdd; - $this->applyToSummaryRow = $applyToSummaryRow; - } + /** + * @param Piwik_DataTable $table + * @param $columnToRead + * @param $metadataToAdd + * @param null $functionToApply + * @param null $functionParameters + */ + public function __construct($table, $columnToRead, $metadataToAdd, $functionToApply = null, + $functionParameters = null, $applyToSummaryRow = true) + { + parent::__construct($table); + $this->functionToApply = $functionToApply; + $this->functionParameters = $functionParameters; + $this->columnToRead = $columnToRead; + $this->metadataToAdd = $metadataToAdd; + $this->applyToSummaryRow = $applyToSummaryRow; + } - /** - * Filters the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $key => $row) - { - if (!$this->applyToSummaryRow && $key == Piwik_DataTable::ID_SUMMARY_ROW) - { - continue; - } - - $oldValue = $row->getColumn($this->columnToRead); - $parameters = array($oldValue); - if(!is_null($this->functionParameters)) - { - $parameters = array_merge($parameters, $this->functionParameters); - } - if(!is_null($this->functionToApply)) - { - $newValue = call_user_func_array( $this->functionToApply, $parameters); - } - else - { - $newValue = $oldValue; - } - if ($newValue !== false) - { - $row->addMetadata($this->metadataToAdd, $newValue); - } - } - } + /** + * Filters the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $key => $row) { + if (!$this->applyToSummaryRow && $key == Piwik_DataTable::ID_SUMMARY_ROW) { + continue; + } + + $oldValue = $row->getColumn($this->columnToRead); + $parameters = array($oldValue); + if (!is_null($this->functionParameters)) { + $parameters = array_merge($parameters, $this->functionParameters); + } + if (!is_null($this->functionToApply)) { + $newValue = call_user_func_array($this->functionToApply, $parameters); + } else { + $newValue = $oldValue; + } + if ($newValue !== false) { + $row->addMetadata($this->metadataToAdd, $newValue); + } + } + } } diff --git a/core/DataTable/Filter/ColumnCallbackDeleteRow.php b/core/DataTable/Filter/ColumnCallbackDeleteRow.php index da5c773184..dbe0684cf8 100644 --- a/core/DataTable/Filter/ColumnCallbackDeleteRow.php +++ b/core/DataTable/Filter/ColumnCallbackDeleteRow.php @@ -1,61 +1,58 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Delete all rows for which a given function returns false for a given column. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_ColumnCallbackDeleteRow extends Piwik_DataTable_Filter { - private $columnToFilter; - private $function; - private $functionParams; + private $columnToFilter; + private $function; + private $functionParams; - /** - * @param Piwik_DataTable $table - * @param string $columnToFilter - * @param callback $function - * @param array $functionParams - */ - public function __construct( $table, $columnToFilter, $function, $functionParams = array() ) - { - parent::__construct($table); - - if (!is_array($functionParams)) - { - $functionParams = array($functionParams); - } - - $this->function = $function; - $this->columnToFilter = $columnToFilter; - $this->functionParams = $functionParams; - } + /** + * @param Piwik_DataTable $table + * @param string $columnToFilter + * @param callback $function + * @param array $functionParams + */ + public function __construct($table, $columnToFilter, $function, $functionParams = array()) + { + parent::__construct($table); - /** - * Filters the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $key => $row) - { - $columnValue = $row->getColumn($this->columnToFilter); - if( !call_user_func_array( $this->function, array_merge(array($columnValue), $this->functionParams))) - { - $table->deleteRow($key); - } - $this->filterSubTable($row); - } - } + if (!is_array($functionParams)) { + $functionParams = array($functionParams); + } + + $this->function = $function; + $this->columnToFilter = $columnToFilter; + $this->functionParams = $functionParams; + } + + /** + * Filters the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $key => $row) { + $columnValue = $row->getColumn($this->columnToFilter); + if (!call_user_func_array($this->function, array_merge(array($columnValue), $this->functionParams))) { + $table->deleteRow($key); + } + $this->filterSubTable($row); + } + } } diff --git a/core/DataTable/Filter/ColumnCallbackReplace.php b/core/DataTable/Filter/ColumnCallbackReplace.php index 1ab5a4931a..f1c2d65f3a 100644 --- a/core/DataTable/Filter/ColumnCallbackReplace.php +++ b/core/DataTable/Filter/ColumnCallbackReplace.php @@ -1,108 +1,102 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * Replace a column value with a new value resulting + * Replace a column value with a new value resulting * from the function called with the column's value - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_ColumnCallbackReplace extends Piwik_DataTable_Filter { - private $columnsToFilter; - private $functionToApply; - private $functionParameters; - private $extraColumnParameters; + private $columnsToFilter; + private $functionToApply; + private $functionParameters; + private $extraColumnParameters; - /** - * @param Piwik_DataTable $table - * @param array|string $columnsToFilter - * @param callback $functionToApply - * @param array|null $functionParameters - * @param array $extraColumnParameters - */ - public function __construct( $table, $columnsToFilter, $functionToApply, $functionParameters = null, - $extraColumnParameters = array() ) - { - parent::__construct($table); - $this->functionToApply = $functionToApply; - $this->functionParameters = $functionParameters; - - if (!is_array($columnsToFilter)) - { - $columnsToFilter = array($columnsToFilter); - } - - $this->columnsToFilter = $columnsToFilter; - $this->extraColumnParameters = $extraColumnParameters; - } + /** + * @param Piwik_DataTable $table + * @param array|string $columnsToFilter + * @param callback $functionToApply + * @param array|null $functionParameters + * @param array $extraColumnParameters + */ + public function __construct($table, $columnsToFilter, $functionToApply, $functionParameters = null, + $extraColumnParameters = array()) + { + parent::__construct($table); + $this->functionToApply = $functionToApply; + $this->functionParameters = $functionParameters; - /** - * Filters the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $key => $row) - { - $extraColumnParameters = array(); - foreach ($this->extraColumnParameters as $columnName) - { - $extraColumnParameters[] = $row->getColumn($columnName); - } - - foreach ($this->columnsToFilter as $column) - { - // when a value is not defined, we set it to zero by default (rather than displaying '-') - $value = $this->getElementToReplace($row, $column); - if($value === false) - { - $value = 0; - } + if (!is_array($columnsToFilter)) { + $columnsToFilter = array($columnsToFilter); + } - $parameters = array_merge(array($value), $extraColumnParameters); - if(!is_null($this->functionParameters)) - { - $parameters = array_merge($parameters, $this->functionParameters); - } - $newValue = call_user_func_array( $this->functionToApply, $parameters); - $this->setElementToReplace($row, $column, $newValue); - $this->filterSubTable($row); - } - } - } + $this->columnsToFilter = $columnsToFilter; + $this->extraColumnParameters = $extraColumnParameters; + } - /** - * Replaces the given column within given row with the given value - * - * @param Piwik_DataTable_Row $row - * @param string $columnToFilter - * @param mixed $newValue - */ - protected function setElementToReplace($row, $columnToFilter, $newValue) - { - $row->setColumn($columnToFilter, $newValue); - } + /** + * Filters the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $key => $row) { + $extraColumnParameters = array(); + foreach ($this->extraColumnParameters as $columnName) { + $extraColumnParameters[] = $row->getColumn($columnName); + } - /** - * Returns the element that should be replaced - * - * @param Piwik_DataTable_Row $row - * @param string $columnToFilter - * @return mixed - */ - protected function getElementToReplace($row, $columnToFilter) - { - return $row->getColumn($columnToFilter); - } + foreach ($this->columnsToFilter as $column) { + // when a value is not defined, we set it to zero by default (rather than displaying '-') + $value = $this->getElementToReplace($row, $column); + if ($value === false) { + $value = 0; + } + + $parameters = array_merge(array($value), $extraColumnParameters); + if (!is_null($this->functionParameters)) { + $parameters = array_merge($parameters, $this->functionParameters); + } + $newValue = call_user_func_array($this->functionToApply, $parameters); + $this->setElementToReplace($row, $column, $newValue); + $this->filterSubTable($row); + } + } + } + + /** + * Replaces the given column within given row with the given value + * + * @param Piwik_DataTable_Row $row + * @param string $columnToFilter + * @param mixed $newValue + */ + protected function setElementToReplace($row, $columnToFilter, $newValue) + { + $row->setColumn($columnToFilter, $newValue); + } + + /** + * Returns the element that should be replaced + * + * @param Piwik_DataTable_Row $row + * @param string $columnToFilter + * @return mixed + */ + protected function getElementToReplace($row, $columnToFilter) + { + return $row->getColumn($columnToFilter); + } } diff --git a/core/DataTable/Filter/ColumnDelete.php b/core/DataTable/Filter/ColumnDelete.php index 930f9969f8..471315dd4a 100644 --- a/core/DataTable/Filter/ColumnDelete.php +++ b/core/DataTable/Filter/ColumnDelete.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,122 +12,109 @@ /** * Filter that will remove columns from a DataTable using either a blacklist, * whitelist or both. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_ColumnDelete extends Piwik_DataTable_Filter { - /** - * The columns that should be removed from DataTable rows. - * - * @var array - */ - private $columnsToRemove; - - /** - * The columns that should be kept in DataTable rows. All other columns will be - * removed. If a column is in $columnsToRemove and this variable, it will NOT be kept. - * - * @var array - */ - private $columnsToKeep; + /** + * The columns that should be removed from DataTable rows. + * + * @var array + */ + private $columnsToRemove; - /** - * Delete the column, only if the value was zero - * - * @var bool - */ - private $deleteIfZeroOnly; - - /** - * Constructor. - * - * @param Piwik_DataTable $table - * @param array|string $columnsToRemove An array of column names or a comma-separated list of - * column names. These columns will be removed. - * @param array|string $columnsToKeep An array of column names that should be kept or a - * comma-separated list of column names. Columns not in - * this list will be removed. - */ - public function __construct( $table, $columnsToRemove, $columnsToKeep = array(), $deleteIfZeroOnly = false ) - { - parent::__construct($table); - - if (is_string($columnsToRemove)) - { - $columnsToRemove = $columnsToRemove == '' ? array() : explode(',', $columnsToRemove); - } - - if (is_string($columnsToKeep)) - { - $columnsToKeep = $columnsToKeep == '' ? array() : explode(',', $columnsToKeep); - } - - $this->columnsToRemove = $columnsToRemove; - $this->columnsToKeep = array_flip($columnsToKeep); // flip so we can use isset instead of in_array - $this->deleteIfZeroOnly = $deleteIfZeroOnly; - } + /** + * The columns that should be kept in DataTable rows. All other columns will be + * removed. If a column is in $columnsToRemove and this variable, it will NOT be kept. + * + * @var array + */ + private $columnsToKeep; - /** - * Filters the given DataTable. Removes columns that are not desired from - * each DataTable row. - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - // always do recursive filter - $this->enableRecursive(true); - $recurse = false; // only recurse if there are columns to remove/keep + /** + * Delete the column, only if the value was zero + * + * @var bool + */ + private $deleteIfZeroOnly; - // remove columns specified in $this->columnsToRemove - if (!empty($this->columnsToRemove)) - { - foreach ($table->getRows() as $row) - { - foreach ($this->columnsToRemove as $column) - { - if($this->deleteIfZeroOnly) - { - $value = $row->getColumn($column); - if($value === false || !empty($value)) - { - continue; - } - } - $row->deleteColumn($column); - } - } - - $recurse = true; - } - - // remove columns not specified in $columnsToKeep - if (!empty($this->columnsToKeep)) - { - foreach ($table->getRows() as $row) - { - foreach ($row->getColumns() as $name => $value) - { - // label cannot be removed via whitelisting - if ($name != 'label' && !isset($this->columnsToKeep[$name])) - { - $row->deleteColumn($name); - } - } - } - - $recurse = true; - } - - // recurse - if ($recurse) - { - foreach ($table->getRows() as $row) - { - $this->filterSubTable($row); - } - } - } + /** + * Constructor. + * + * @param Piwik_DataTable $table + * @param array|string $columnsToRemove An array of column names or a comma-separated list of + * column names. These columns will be removed. + * @param array|string $columnsToKeep An array of column names that should be kept or a + * comma-separated list of column names. Columns not in + * this list will be removed. + */ + public function __construct($table, $columnsToRemove, $columnsToKeep = array(), $deleteIfZeroOnly = false) + { + parent::__construct($table); + + if (is_string($columnsToRemove)) { + $columnsToRemove = $columnsToRemove == '' ? array() : explode(',', $columnsToRemove); + } + + if (is_string($columnsToKeep)) { + $columnsToKeep = $columnsToKeep == '' ? array() : explode(',', $columnsToKeep); + } + + $this->columnsToRemove = $columnsToRemove; + $this->columnsToKeep = array_flip($columnsToKeep); // flip so we can use isset instead of in_array + $this->deleteIfZeroOnly = $deleteIfZeroOnly; + } + + /** + * Filters the given DataTable. Removes columns that are not desired from + * each DataTable row. + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + // always do recursive filter + $this->enableRecursive(true); + $recurse = false; // only recurse if there are columns to remove/keep + + // remove columns specified in $this->columnsToRemove + if (!empty($this->columnsToRemove)) { + foreach ($table->getRows() as $row) { + foreach ($this->columnsToRemove as $column) { + if ($this->deleteIfZeroOnly) { + $value = $row->getColumn($column); + if ($value === false || !empty($value)) { + continue; + } + } + $row->deleteColumn($column); + } + } + + $recurse = true; + } + + // remove columns not specified in $columnsToKeep + if (!empty($this->columnsToKeep)) { + foreach ($table->getRows() as $row) { + foreach ($row->getColumns() as $name => $value) { + // label cannot be removed via whitelisting + if ($name != 'label' && !isset($this->columnsToKeep[$name])) { + $row->deleteColumn($name); + } + } + } + + $recurse = true; + } + + // recurse + if ($recurse) { + foreach ($table->getRows() as $row) { + $this->filterSubTable($row); + } + } + } } diff --git a/core/DataTable/Filter/ExcludeLowPopulation.php b/core/DataTable/Filter/ExcludeLowPopulation.php index aa3dc82f14..c5d6369d6c 100644 --- a/core/DataTable/Filter/ExcludeLowPopulation.php +++ b/core/DataTable/Filter/ExcludeLowPopulation.php @@ -1,77 +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 - * + * * @category Piwik * @package Piwik */ /** - * Delete all rows that have a $columnToFilter value less than the $minimumValue - * + * Delete all rows that have a $columnToFilter value less than the $minimumValue + * * For example we delete from the countries report table all countries that have less than 3 visits. * It is very useful to exclude noise from the reports. * You can obviously apply this filter on a percentaged column, eg. remove all countries with the column 'percent_visits' less than 0.05 - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_ExcludeLowPopulation extends Piwik_DataTable_Filter { - static public $minimumValue; - const MINIMUM_SIGNIFICANT_PERCENTAGE_THRESHOLD = 0.02; + static public $minimumValue; + const MINIMUM_SIGNIFICANT_PERCENTAGE_THRESHOLD = 0.02; - /** - * Constructor - * - * @param Piwik_DataTable $table - * @param string $columnToFilter column to filter - * @param number $minimumValue minimum value - * @param bool $minimumPercentageThreshold - */ - public function __construct( $table, $columnToFilter, $minimumValue, $minimumPercentageThreshold = false ) - { - parent::__construct($table); - $this->columnToFilter = $columnToFilter; - - if($minimumValue == 0) - { - if($minimumPercentageThreshold === false) - { - $minimumPercentageThreshold = self::MINIMUM_SIGNIFICANT_PERCENTAGE_THRESHOLD; - } - $allValues = $table->getColumn($this->columnToFilter); - $sumValues = array_sum($allValues); - $minimumValue = $sumValues * $minimumPercentageThreshold; - } - self::$minimumValue = $minimumValue; - } + /** + * Constructor + * + * @param Piwik_DataTable $table + * @param string $columnToFilter column to filter + * @param number $minimumValue minimum value + * @param bool $minimumPercentageThreshold + */ + public function __construct($table, $columnToFilter, $minimumValue, $minimumPercentageThreshold = false) + { + parent::__construct($table); + $this->columnToFilter = $columnToFilter; - /** - * Executes filter and removes all rows below the defined minimum - * - * @param Piwik_DataTable $table - */ - function filter($table) - { - $table->filter('ColumnCallbackDeleteRow', - array($this->columnToFilter, - array("Piwik_DataTable_Filter_ExcludeLowPopulation", "excludeLowPopulation") - ) - ); - } + if ($minimumValue == 0) { + if ($minimumPercentageThreshold === false) { + $minimumPercentageThreshold = self::MINIMUM_SIGNIFICANT_PERCENTAGE_THRESHOLD; + } + $allValues = $table->getColumn($this->columnToFilter); + $sumValues = array_sum($allValues); + $minimumValue = $sumValues * $minimumPercentageThreshold; + } + self::$minimumValue = $minimumValue; + } - /** - * Checks whether the given value is below the defined minimum - * - * @param number $value value to check - * @return bool - */ - static public function excludeLowPopulation($value) - { - return $value >= self::$minimumValue; - } + /** + * Executes filter and removes all rows below the defined minimum + * + * @param Piwik_DataTable $table + */ + function filter($table) + { + $table->filter('ColumnCallbackDeleteRow', + array($this->columnToFilter, + array("Piwik_DataTable_Filter_ExcludeLowPopulation", "excludeLowPopulation") + ) + ); + } + + /** + * Checks whether the given value is below the defined minimum + * + * @param number $value value to check + * @return bool + */ + static public function excludeLowPopulation($value) + { + return $value >= self::$minimumValue; + } } diff --git a/core/DataTable/Filter/GroupBy.php b/core/DataTable/Filter/GroupBy.php index 5c521e7ca2..4b42e8d2ba 100755 --- a/core/DataTable/Filter/GroupBy.php +++ b/core/DataTable/Filter/GroupBy.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,88 +12,83 @@ /** * DataTable filter that will group DataTable rows together based on the results * of a reduce function. Rows with the same reduce result will be summed and merged. - * + * * NOTE: This filter should never be queued, it must be applied directly on a DataTable. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_GroupBy extends Piwik_DataTable_Filter { - /** - * The name of the columns to reduce. - * @var string - */ - private $groupByColumn; - - /** - * A callback that modifies the $groupByColumn of each row in some way. Rows with - * the same reduction result will be added together. - */ - private $reduceFunction; - - /** - * Extra parameters to pass to the reduce function. - */ - private $parameters; - - /** - * Constructor. - * - * @param Piwik_DataTable $table The DataTable to filter. - * @param string $groupByColumn The column name to reduce. - * @param mixed $reduceFunction The reduce function. This must alter the $groupByColumn in some way. - * @param array $parameters Extra parameters to supply to the reduce function. - */ - public function __construct( $table, $groupByColumn, $reduceFunction, $parameters = array() ) - { - parent::__construct($table); - - $this->groupByColumn = $groupByColumn; - $this->reduceFunction = $reduceFunction; - $this->parameters = $parameters; - } - - /** - * Applies the reduce function to each row and merges rows w/ the same reduce result. - * - * @param Piwik_DataTable $table - */ - public function filter( $table ) - { - $groupByRows = array(); - $nonGroupByRowIds = array(); - - foreach ($table->getRows() as $rowId => $row) - { - // skip the summary row - if ($rowId == Piwik_DataTable::ID_SUMMARY_ROW) - { - continue; - } - - // reduce the group by column of this row - $groupByColumnValue = $row->getColumn($this->groupByColumn); - $parameters = array_merge(array($groupByColumnValue), $this->parameters); - $groupByValue = call_user_func_array($this->reduceFunction, $parameters); - - if (!isset($groupByRows[$groupByValue])) - { - // if we haven't encountered this group by value before, we mark this row as a - // row to keep, and change the group by column to the reduced value. - $groupByRows[$groupByValue] = $row; - $row->setColumn($this->groupByColumn, $groupByValue); - } - else - { - // if we have already encountered this group by value, we add this row to the - // row that will be kept, and mark this one for deletion - $groupByRows[$groupByValue]->sumRow($row); - $nonGroupByRowIds[] = $rowId; - } - } - - // delete the unneeded rows. - $table->deleteRows($nonGroupByRowIds); - } + /** + * The name of the columns to reduce. + * @var string + */ + private $groupByColumn; + + /** + * A callback that modifies the $groupByColumn of each row in some way. Rows with + * the same reduction result will be added together. + */ + private $reduceFunction; + + /** + * Extra parameters to pass to the reduce function. + */ + private $parameters; + + /** + * Constructor. + * + * @param Piwik_DataTable $table The DataTable to filter. + * @param string $groupByColumn The column name to reduce. + * @param mixed $reduceFunction The reduce function. This must alter the $groupByColumn in some way. + * @param array $parameters Extra parameters to supply to the reduce function. + */ + public function __construct($table, $groupByColumn, $reduceFunction, $parameters = array()) + { + parent::__construct($table); + + $this->groupByColumn = $groupByColumn; + $this->reduceFunction = $reduceFunction; + $this->parameters = $parameters; + } + + /** + * Applies the reduce function to each row and merges rows w/ the same reduce result. + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + $groupByRows = array(); + $nonGroupByRowIds = array(); + + foreach ($table->getRows() as $rowId => $row) { + // skip the summary row + if ($rowId == Piwik_DataTable::ID_SUMMARY_ROW) { + continue; + } + + // reduce the group by column of this row + $groupByColumnValue = $row->getColumn($this->groupByColumn); + $parameters = array_merge(array($groupByColumnValue), $this->parameters); + $groupByValue = call_user_func_array($this->reduceFunction, $parameters); + + if (!isset($groupByRows[$groupByValue])) { + // if we haven't encountered this group by value before, we mark this row as a + // row to keep, and change the group by column to the reduced value. + $groupByRows[$groupByValue] = $row; + $row->setColumn($this->groupByColumn, $groupByValue); + } else { + // if we have already encountered this group by value, we add this row to the + // row that will be kept, and mark this one for deletion + $groupByRows[$groupByValue]->sumRow($row); + $nonGroupByRowIds[] = $rowId; + } + } + + // delete the unneeded rows. + $table->deleteRows($nonGroupByRowIds); + } } diff --git a/core/DataTable/Filter/Limit.php b/core/DataTable/Filter/Limit.php index 8e2e7e3272..5241b466e2 100644 --- a/core/DataTable/Filter/Limit.php +++ b/core/DataTable/Filter/Limit.php @@ -1,71 +1,66 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Delete all rows from the table that are not in the offset,offset+limit range - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_Limit extends Piwik_DataTable_Filter { - /** - * Filter constructor. - * - * @param Piwik_DataTable $table - * @param int $offset Starting row (indexed from 0) - * @param int $limit Number of rows to keep (specify -1 to keep all rows) - * @param bool $keepSummaryRow Whether to keep the summary row or not. - */ - public function __construct( $table, $offset, $limit = null, $keepSummaryRow = false ) - { - parent::__construct($table); - $this->offset = $offset; - - if(is_null($limit)) - { - $limit = -1; - } - $this->limit = $limit; - $this->keepSummaryRow = $keepSummaryRow; - } + /** + * Filter constructor. + * + * @param Piwik_DataTable $table + * @param int $offset Starting row (indexed from 0) + * @param int $limit Number of rows to keep (specify -1 to keep all rows) + * @param bool $keepSummaryRow Whether to keep the summary row or not. + */ + public function __construct($table, $offset, $limit = null, $keepSummaryRow = false) + { + parent::__construct($table); + $this->offset = $offset; - /** - * Limits the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - $table->setRowsCountBeforeLimitFilter(); - - if ($this->keepSummaryRow) - { - $summaryRow = $table->getRowFromId(Piwik_DataTable::ID_SUMMARY_ROW); - } - - // we delete from 0 to offset - if($this->offset > 0) - { - $table->deleteRowsOffset( 0, $this->offset ); - } - // at this point the array has offset less elements. We delete from limit to the end - if( $this->limit >= 0 ) - { - $table->deleteRowsOffset( $this->limit ); - } - - if ($this->keepSummaryRow && $summaryRow) - { - $table->addSummaryRow($summaryRow); - } - } + if (is_null($limit)) { + $limit = -1; + } + $this->limit = $limit; + $this->keepSummaryRow = $keepSummaryRow; + } + + /** + * Limits the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + $table->setRowsCountBeforeLimitFilter(); + + if ($this->keepSummaryRow) { + $summaryRow = $table->getRowFromId(Piwik_DataTable::ID_SUMMARY_ROW); + } + + // we delete from 0 to offset + if ($this->offset > 0) { + $table->deleteRowsOffset(0, $this->offset); + } + // at this point the array has offset less elements. We delete from limit to the end + if ($this->limit >= 0) { + $table->deleteRowsOffset($this->limit); + } + + if ($this->keepSummaryRow && $summaryRow) { + $table->addSummaryRow($summaryRow); + } + } } diff --git a/core/DataTable/Filter/MetadataCallbackAddMetadata.php b/core/DataTable/Filter/MetadataCallbackAddMetadata.php index 3d70916bfd..dbfc1728ee 100644 --- a/core/DataTable/Filter/MetadataCallbackAddMetadata.php +++ b/core/DataTable/Filter/MetadataCallbackAddMetadata.php @@ -1,78 +1,73 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * Add a new metadata to the table based on the value resulting + * Add a new metadata to the table based on the value resulting * from a callback function with the parameter being another metadata value - * - * For example for the searchEngine we have a "metadata" information that gives - * the URL of the search engine. We use this URL to add a new "metadata" that gives - * the path of the logo for this search engine URL (which has the format URL.png). - * + * + * For example for the searchEngine we have a "metadata" information that gives + * the URL of the search engine. We use this URL to add a new "metadata" that gives + * the path of the logo for this search engine URL (which has the format URL.png). + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_MetadataCallbackAddMetadata extends Piwik_DataTable_Filter { - private $metadataToRead; - private $functionToApply; - private $metadataToAdd; - private $applyToSummaryRow; + private $metadataToRead; + private $functionToApply; + private $metadataToAdd; + private $applyToSummaryRow; - /** - * @param Piwik_DataTable $table - * @param string|array $metadataToRead - * @param string $metadataToAdd - * @param callback $functionToApply - * @param bool $applyToSummaryRow - */ - public function __construct( $table, $metadataToRead, $metadataToAdd, $functionToApply, - $applyToSummaryRow = true ) - { - parent::__construct($table); - $this->functionToApply = $functionToApply; - - if (!is_array($metadataToRead)) - { - $metadataToRead = array($metadataToRead); - } - - $this->metadataToRead = $metadataToRead; - $this->metadataToAdd = $metadataToAdd; - $this->applyToSummaryRow = $applyToSummaryRow; - } + /** + * @param Piwik_DataTable $table + * @param string|array $metadataToRead + * @param string $metadataToAdd + * @param callback $functionToApply + * @param bool $applyToSummaryRow + */ + public function __construct($table, $metadataToRead, $metadataToAdd, $functionToApply, + $applyToSummaryRow = true) + { + parent::__construct($table); + $this->functionToApply = $functionToApply; - /** - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $key => $row) - { - if (!$this->applyToSummaryRow && $key == Piwik_DataTable::ID_SUMMARY_ROW) - { - continue; - } - - $params = array(); - foreach ($this->metadataToRead as $name) - { - $params[] = $row->getMetadata($name); - } - - $newValue = call_user_func_array($this->functionToApply, $params); - if($newValue !== false) - { - $row->addMetadata($this->metadataToAdd, $newValue); - } - } - } + if (!is_array($metadataToRead)) { + $metadataToRead = array($metadataToRead); + } + + $this->metadataToRead = $metadataToRead; + $this->metadataToAdd = $metadataToAdd; + $this->applyToSummaryRow = $applyToSummaryRow; + } + + /** + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $key => $row) { + if (!$this->applyToSummaryRow && $key == Piwik_DataTable::ID_SUMMARY_ROW) { + continue; + } + + $params = array(); + foreach ($this->metadataToRead as $name) { + $params[] = $row->getMetadata($name); + } + + $newValue = call_user_func_array($this->functionToApply, $params); + if ($newValue !== false) { + $row->addMetadata($this->metadataToAdd, $newValue); + } + } + } } diff --git a/core/DataTable/Filter/MetadataCallbackReplace.php b/core/DataTable/Filter/MetadataCallbackReplace.php index 9fc49f0403..4e6e5990de 100644 --- a/core/DataTable/Filter/MetadataCallbackReplace.php +++ b/core/DataTable/Filter/MetadataCallbackReplace.php @@ -1,53 +1,53 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * Replace a metadata value with a new value resulting + * Replace a metadata value with a new value resulting * from the function called with the metadata's value - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_MetadataCallbackReplace extends Piwik_DataTable_Filter_ColumnCallbackReplace { - /** - * @param Piwik_DataTable $table - * @param array|string $metadataToFilter - * @param callback $functionToApply - * @param null|array $functionParameters - * @param array $extraColumnParameters - */ - public function __construct( $table, $metadataToFilter, $functionToApply, $functionParameters = null, - $extraColumnParameters = array() ) - { - parent::__construct($table, $metadataToFilter, $functionToApply, $functionParameters, $extraColumnParameters); - } + /** + * @param Piwik_DataTable $table + * @param array|string $metadataToFilter + * @param callback $functionToApply + * @param null|array $functionParameters + * @param array $extraColumnParameters + */ + public function __construct($table, $metadataToFilter, $functionToApply, $functionParameters = null, + $extraColumnParameters = array()) + { + parent::__construct($table, $metadataToFilter, $functionToApply, $functionParameters, $extraColumnParameters); + } - /** - * @param Piwik_DataTable_Row $row - * @param string $metadataToFilter - * @param mixed $newValue - */ - protected function setElementToReplace($row, $metadataToFilter, $newValue) - { - $row->setMetadata($metadataToFilter, $newValue); - } + /** + * @param Piwik_DataTable_Row $row + * @param string $metadataToFilter + * @param mixed $newValue + */ + protected function setElementToReplace($row, $metadataToFilter, $newValue) + { + $row->setMetadata($metadataToFilter, $newValue); + } - /** - * @param Piwik_DataTable_Row $row - * @param string $metadataToFilter - * @return array|false|mixed - */ - protected function getElementToReplace($row, $metadataToFilter) - { - return $row->getMetadata($metadataToFilter); - } + /** + * @param Piwik_DataTable_Row $row + * @param string $metadataToFilter + * @return array|false|mixed + */ + protected function getElementToReplace($row, $metadataToFilter) + { + return $row->getMetadata($metadataToFilter); + } } diff --git a/core/DataTable/Filter/Null.php b/core/DataTable/Filter/Null.php index 0d954532e1..240dc53c41 100644 --- a/core/DataTable/Filter/Null.php +++ b/core/DataTable/Filter/Null.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,27 +12,26 @@ /** * Filter template. * You can use it if you want to create a new filter. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_Null extends Piwik_DataTable_Filter { - /** - * @param Piwik_DataTable $table - */ - public function __construct( $table ) - { - parent::__construct($table); - } + /** + * @param Piwik_DataTable $table + */ + public function __construct($table) + { + parent::__construct($table); + } - /** - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $key => $row) - { - } - } + /** + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $key => $row) { + } + } } diff --git a/core/DataTable/Filter/Pattern.php b/core/DataTable/Filter/Pattern.php index 92f8b2b3d5..4a1ef8fd99 100644 --- a/core/DataTable/Filter/Pattern.php +++ b/core/DataTable/Filter/Pattern.php @@ -1,89 +1,86 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Delete all rows for which the given $columnToFilter do not contain the $patternToSearch - * This filter is to be used on columns containing strings. + * This filter is to be used on columns containing strings. * Example: from the keyword report, keep only the rows for which the label contains "piwik" - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_Pattern extends Piwik_DataTable_Filter { - private $columnToFilter; - private $patternToSearch; - private $patternToSearchQuoted; + private $columnToFilter; + private $patternToSearch; + private $patternToSearchQuoted; private $invertedMatch; - /** - * @param Piwik_DataTable $table - * @param string $columnToFilter - * @param string $patternToSearch - * @param bool $invertedMatch - */ - public function __construct( $table, $columnToFilter, $patternToSearch, $invertedMatch = false ) - { - parent::__construct($table); - $this->patternToSearch = $patternToSearch; - $this->patternToSearchQuoted = self::getPatternQuoted($patternToSearch); - $this->columnToFilter = $columnToFilter; + /** + * @param Piwik_DataTable $table + * @param string $columnToFilter + * @param string $patternToSearch + * @param bool $invertedMatch + */ + public function __construct($table, $columnToFilter, $patternToSearch, $invertedMatch = false) + { + parent::__construct($table); + $this->patternToSearch = $patternToSearch; + $this->patternToSearchQuoted = self::getPatternQuoted($patternToSearch); + $this->columnToFilter = $columnToFilter; $this->invertedMatch = $invertedMatch; - } + } - /** - * Helper method to return the given pattern quoted - * - * @param string $pattern - * @return string - */ - static public function getPatternQuoted( $pattern ) - { - return '/'. str_replace('/', '\/', $pattern) .'/'; - } + /** + * Helper method to return the given pattern quoted + * + * @param string $pattern + * @return string + */ + static public function getPatternQuoted($pattern) + { + return '/' . str_replace('/', '\/', $pattern) . '/'; + } - /** - * Performs case insensitive match - * - * @param string $pattern - * @param string $patternQuoted - * @param string $string - * @param bool $invertedMatch - * @return int - */ - static public function match($pattern, $patternQuoted, $string, $invertedMatch) - { - return @preg_match($patternQuoted . "i", $string) == 1 ^ $invertedMatch; - } + /** + * Performs case insensitive match + * + * @param string $pattern + * @param string $patternQuoted + * @param string $string + * @param bool $invertedMatch + * @return int + */ + static public function match($pattern, $patternQuoted, $string, $invertedMatch) + { + return @preg_match($patternQuoted . "i", $string) == 1 ^ $invertedMatch; + } - /** - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $key => $row) - { - //instead search must handle - // - negative search with -piwik - // - exact match with "" - // see (?!pattern) A subexpression that performs a negative lookahead search, which matches the search string at any point where a string not matching pattern begins. - $value = $row->getColumn($this->columnToFilter); - if($value === false) - { - $value = $row->getMetadata($this->columnToFilter); - } - if( !self::match($this->patternToSearch, $this->patternToSearchQuoted, $value, $this->invertedMatch)) - { - $table->deleteRow($key); - } - } - } + /** + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $key => $row) { + //instead search must handle + // - negative search with -piwik + // - exact match with "" + // see (?!pattern) A subexpression that performs a negative lookahead search, which matches the search string at any point where a string not matching pattern begins. + $value = $row->getColumn($this->columnToFilter); + if ($value === false) { + $value = $row->getMetadata($this->columnToFilter); + } + if (!self::match($this->patternToSearch, $this->patternToSearchQuoted, $value, $this->invertedMatch)) { + $table->deleteRow($key); + } + } + } } diff --git a/core/DataTable/Filter/PatternRecursive.php b/core/DataTable/Filter/PatternRecursive.php index 4d73b4dc75..088546fee5 100644 --- a/core/DataTable/Filter/PatternRecursive.php +++ b/core/DataTable/Filter/PatternRecursive.php @@ -1,83 +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 - * + * * @category Piwik * @package Piwik */ /** - * Delete all rows for which - * - the given $columnToFilter do not contain the $patternToSearch + * Delete all rows for which + * - the given $columnToFilter do not contain the $patternToSearch * - AND all the subTables associated to this row do not contain the $patternToSearch - * - * This filter is to be used on columns containing strings. + * + * This filter is to be used on columns containing strings. * Example: from the pages viewed report, keep only the rows that contain "piwik" or for which a subpage contains "piwik". - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_PatternRecursive extends Piwik_DataTable_Filter { - private $columnToFilter; - private $patternToSearch; - private $patternToSearchQuoted; + private $columnToFilter; + private $patternToSearch; + private $patternToSearchQuoted; - /** - * @param Piwik_DataTable $table - * @param string $columnToFilter - * @param string $patternToSearch - */ - public function __construct( $table, $columnToFilter, $patternToSearch ) - { - parent::__construct($table); - $this->patternToSearch = $patternToSearch; - $this->patternToSearchQuoted = Piwik_DataTable_Filter_Pattern::getPatternQuoted($patternToSearch); - $this->patternToSearch = $patternToSearch;//preg_quote($patternToSearch); - $this->columnToFilter = $columnToFilter; - } + /** + * @param Piwik_DataTable $table + * @param string $columnToFilter + * @param string $patternToSearch + */ + public function __construct($table, $columnToFilter, $patternToSearch) + { + parent::__construct($table); + $this->patternToSearch = $patternToSearch; + $this->patternToSearchQuoted = Piwik_DataTable_Filter_Pattern::getPatternQuoted($patternToSearch); + $this->patternToSearch = $patternToSearch; //preg_quote($patternToSearch); + $this->columnToFilter = $columnToFilter; + } - /** - * @param Piwik_DataTable $table - * @return int - */ - public function filter( $table ) - { - $rows = $table->getRows(); - - foreach($rows as $key => $row) - { - // A row is deleted if - // 1 - its label doesnt contain the pattern - // AND 2 - the label is not found in the children - $patternNotFoundInChildren = false; - - try{ - $idSubTable = $row->getIdSubDataTable(); - $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); - - // we delete the row if we couldn't find the pattern in any row in the - // children hierarchy - if( $this->filter($subTable) == 0 ) - { - $patternNotFoundInChildren = true; - } - } catch(Exception $e) { - // there is no subtable loaded for example - $patternNotFoundInChildren = true; - } + /** + * @param Piwik_DataTable $table + * @return int + */ + public function filter($table) + { + $rows = $table->getRows(); - if( $patternNotFoundInChildren - && !Piwik_DataTable_Filter_Pattern::match($this->patternToSearch, $this->patternToSearchQuoted, $row->getColumn($this->columnToFilter), $invertedMatch = false) - ) - { - $table->deleteRow($key); - } - } - - return $table->getRowsCount(); - } + foreach ($rows as $key => $row) { + // A row is deleted if + // 1 - its label doesnt contain the pattern + // AND 2 - the label is not found in the children + $patternNotFoundInChildren = false; + + try { + $idSubTable = $row->getIdSubDataTable(); + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + + // we delete the row if we couldn't find the pattern in any row in the + // children hierarchy + if ($this->filter($subTable) == 0) { + $patternNotFoundInChildren = true; + } + } catch (Exception $e) { + // there is no subtable loaded for example + $patternNotFoundInChildren = true; + } + + if ($patternNotFoundInChildren + && !Piwik_DataTable_Filter_Pattern::match($this->patternToSearch, $this->patternToSearchQuoted, $row->getColumn($this->columnToFilter), $invertedMatch = false) + ) { + $table->deleteRow($key); + } + } + + return $table->getRowsCount(); + } } diff --git a/core/DataTable/Filter/RangeCheck.php b/core/DataTable/Filter/RangeCheck.php index 105f22f395..ebfea03d7f 100644 --- a/core/DataTable/Filter/RangeCheck.php +++ b/core/DataTable/Filter/RangeCheck.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -17,48 +17,43 @@ */ class Piwik_DataTable_Filter_RangeCheck extends Piwik_DataTable_Filter { - static public $minimumValue = 0.00; - static public $maximumValue = 100.0; + static public $minimumValue = 0.00; + static public $maximumValue = 100.0; - /** - * @param Piwik_DataTable $table - * @param string $columnToFilter name of the column to filter - * @param float $minimumValue minimum value for range - * @param float $maximumValue maximum value for range - */ - public function __construct( $table, $columnToFilter, $minimumValue = 0.00, $maximumValue = 100.0 ) - { - parent::__construct($table); + /** + * @param Piwik_DataTable $table + * @param string $columnToFilter name of the column to filter + * @param float $minimumValue minimum value for range + * @param float $maximumValue maximum value for range + */ + public function __construct($table, $columnToFilter, $minimumValue = 0.00, $maximumValue = 100.0) + { + parent::__construct($table); - $this->columnToFilter = $columnToFilter; + $this->columnToFilter = $columnToFilter; - if ($minimumValue < $maximumValue) { - self::$minimumValue = $minimumValue; - self::$maximumValue = $maximumValue; - } - } + if ($minimumValue < $maximumValue) { + self::$minimumValue = $minimumValue; + self::$maximumValue = $maximumValue; + } + } - /** - * Executes the filter an adjusts all columns to fit the defined range - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $row) - { - $value = $row->getColumn($this->columnToFilter); - if($value !== false) - { - if ($value < self::$minimumValue) - { - $row->setColumn($this->columnToFilter, self::$minimumValue); - } - elseif ($value > self::$maximumValue) - { - $row->setColumn($this->columnToFilter, self::$maximumValue); - } - } - } - } + /** + * Executes the filter an adjusts all columns to fit the defined range + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $row) { + $value = $row->getColumn($this->columnToFilter); + if ($value !== false) { + if ($value < self::$minimumValue) { + $row->setColumn($this->columnToFilter, self::$minimumValue); + } elseif ($value > self::$maximumValue) { + $row->setColumn($this->columnToFilter, self::$maximumValue); + } + } + } + } } diff --git a/core/DataTable/Filter/ReplaceColumnNames.php b/core/DataTable/Filter/ReplaceColumnNames.php index a4ff0d9baf..18942f70c1 100644 --- a/core/DataTable/Filter/ReplaceColumnNames.php +++ b/core/DataTable/Filter/ReplaceColumnNames.php @@ -1,115 +1,104 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * This filter replaces column names using a mapping table that maps from the old name to the new name. - * + * * Why this filter? * For saving bytes in the database, you can change all the columns labels by an integer value. * Exemple instead of saving 10000 rows with the column name 'nb_uniq_visitors' which would cost a lot of memory, * we map it to the integer 1 before saving in the DB. * After selecting the DataTable from the DB though, you need to restore back the real names so that * it shows nicely in the report (XML for example). - * + * * You can specify the mapping array to apply in the constructor. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_ReplaceColumnNames extends Piwik_DataTable_Filter { - protected $mappingToApply; - - /** - * @param Piwik_DataTable $table Table - * @param array $mappingToApply Mapping to apply. Must have the format - * array( OLD_COLUMN_NAME => NEW_COLUMN NAME, - * OLD_COLUMN_NAME2 => NEW_COLUMN NAME2, - * ) - */ - public function __construct( $table, $mappingToApply = null ) - { - parent::__construct($table); - $this->mappingToApply = Piwik_Archive::$mappingFromIdToName; - if(!is_null($mappingToApply)) - { - $this->mappingToApply = $mappingToApply; - } - } + protected $mappingToApply; - /** - * Executes the filter and renames the defined columns - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $key => $row) - { - $oldColumns = $row->getColumns(); - $newColumns = $this->getRenamedColumns($oldColumns); - $row->setColumns( $newColumns ); - $this->filterSubTable($row); - } - } + /** + * @param Piwik_DataTable $table Table + * @param array $mappingToApply Mapping to apply. Must have the format + * array( OLD_COLUMN_NAME => NEW_COLUMN NAME, + * OLD_COLUMN_NAME2 => NEW_COLUMN NAME2, + * ) + */ + public function __construct($table, $mappingToApply = null) + { + parent::__construct($table); + $this->mappingToApply = Piwik_Archive::$mappingFromIdToName; + if (!is_null($mappingToApply)) { + $this->mappingToApply = $mappingToApply; + } + } - /** - * Checks the given columns and renames them if required - * - * @param array $columns - * @return array - */ - protected function getRenamedColumns($columns) - { - $newColumns = array(); - foreach($columns as $columnName => $columnValue) - { - if(isset($this->mappingToApply[$columnName])) - { - $columnName = $this->mappingToApply[$columnName]; - - if($columnName == 'goals') - { - $newSubColumns = array(); - foreach($columnValue as $idGoal => $goalValues) - { - $mapping = Piwik_Archive::$mappingFromIdToNameGoal; - if($idGoal == Piwik_Tracker_GoalManager::IDGOAL_CART) - { - $idGoal = Piwik_Archive::LABEL_ECOMMERCE_CART; - } - elseif($idGoal == Piwik_Tracker_GoalManager::IDGOAL_ORDER) - { - $idGoal = Piwik_Archive::LABEL_ECOMMERCE_ORDER; - } - foreach($goalValues as $id => $goalValue) - { - $subColumnName = $mapping[$id]; - $newSubColumns['idgoal='.$idGoal][$subColumnName] = $goalValue; - } - } - $columnValue = $newSubColumns; - } - // If we happen to rename a column to a name that already exists, - // sum both values in the column. This should really not happen, but - // we introduced in 1.1 a new dataTable indexing scheme for Actions table, and - // could end up with both strings and their int indexes counterpart in a monthly/yearly dataTable - // built from DataTable with both formats - if(isset($newColumns[$columnName])) - { - $columnValue += $newColumns[$columnName]; - } - } - $newColumns[$columnName] = $columnValue; - } - return $newColumns; - } + /** + * Executes the filter and renames the defined columns + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $key => $row) { + $oldColumns = $row->getColumns(); + $newColumns = $this->getRenamedColumns($oldColumns); + $row->setColumns($newColumns); + $this->filterSubTable($row); + } + } + + /** + * Checks the given columns and renames them if required + * + * @param array $columns + * @return array + */ + protected function getRenamedColumns($columns) + { + $newColumns = array(); + foreach ($columns as $columnName => $columnValue) { + if (isset($this->mappingToApply[$columnName])) { + $columnName = $this->mappingToApply[$columnName]; + + if ($columnName == 'goals') { + $newSubColumns = array(); + foreach ($columnValue as $idGoal => $goalValues) { + $mapping = Piwik_Archive::$mappingFromIdToNameGoal; + if ($idGoal == Piwik_Tracker_GoalManager::IDGOAL_CART) { + $idGoal = Piwik_Archive::LABEL_ECOMMERCE_CART; + } elseif ($idGoal == Piwik_Tracker_GoalManager::IDGOAL_ORDER) { + $idGoal = Piwik_Archive::LABEL_ECOMMERCE_ORDER; + } + foreach ($goalValues as $id => $goalValue) { + $subColumnName = $mapping[$id]; + $newSubColumns['idgoal=' . $idGoal][$subColumnName] = $goalValue; + } + } + $columnValue = $newSubColumns; + } + // If we happen to rename a column to a name that already exists, + // sum both values in the column. This should really not happen, but + // we introduced in 1.1 a new dataTable indexing scheme for Actions table, and + // could end up with both strings and their int indexes counterpart in a monthly/yearly dataTable + // built from DataTable with both formats + if (isset($newColumns[$columnName])) { + $columnValue += $newColumns[$columnName]; + } + } + $newColumns[$columnName] = $columnValue; + } + return $newColumns; + } } diff --git a/core/DataTable/Filter/ReplaceSummaryRowLabel.php b/core/DataTable/Filter/ReplaceSummaryRowLabel.php index ff70d3c4a9..b89f05871c 100644 --- a/core/DataTable/Filter/ReplaceSummaryRowLabel.php +++ b/core/DataTable/Filter/ReplaceSummaryRowLabel.php @@ -1,60 +1,55 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Filter_ReplaceSummaryRowLabel extends Piwik_DataTable_Filter { - /** - * @param Piwik_DataTable $table - * @param string|null $newLabel new label for summary row - */ - public function __construct( $table, $newLabel = null) - { - parent::__construct($table); - if(is_null($newLabel)) - { - $newLabel = Piwik_Translate('General_Others'); - } - $this->newLabel = $newLabel; - } + /** + * @param Piwik_DataTable $table + * @param string|null $newLabel new label for summary row + */ + public function __construct($table, $newLabel = null) + { + parent::__construct($table); + if (is_null($newLabel)) { + $newLabel = Piwik_Translate('General_Others'); + } + $this->newLabel = $newLabel; + } - /** - * Updates the summary row label - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - $rows = $table->getRows(); - foreach($rows as $row) - { - if($row->getColumn('label') == Piwik_DataTable::LABEL_SUMMARY_ROW) - { - $row->setColumn('label', $this->newLabel); - break; - } - } - - // recurse - foreach ($rows as $row) - { - if ($row->isSubtableLoaded()) - { - $subTable = Piwik_DataTable_Manager::getInstance()->getTable($row->getIdSubDataTable()); - $this->filter($subTable); - } - } - } + /** + * Updates the summary row label + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + $rows = $table->getRows(); + foreach ($rows as $row) { + if ($row->getColumn('label') == Piwik_DataTable::LABEL_SUMMARY_ROW) { + $row->setColumn('label', $this->newLabel); + break; + } + } + + // recurse + foreach ($rows as $row) { + if ($row->isSubtableLoaded()) { + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($row->getIdSubDataTable()); + $this->filter($subTable); + } + } + } } diff --git a/core/DataTable/Filter/SafeDecodeLabel.php b/core/DataTable/Filter/SafeDecodeLabel.php index 035b67072a..ef8643b77f 100644 --- a/core/DataTable/Filter/SafeDecodeLabel.php +++ b/core/DataTable/Filter/SafeDecodeLabel.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,64 +15,61 @@ */ class Piwik_DataTable_Filter_SafeDecodeLabel extends Piwik_DataTable_Filter { - private $columnToDecode; - static private $outputHtml = true; + private $columnToDecode; + static private $outputHtml = true; - /** - * @param Piwik_DataTable $table - */ - public function __construct( $table ) - { - parent::__construct($table); - $this->columnToDecode = 'label'; - } + /** + * @param Piwik_DataTable $table + */ + public function __construct($table) + { + parent::__construct($table); + $this->columnToDecode = 'label'; + } - /** - * Decodes the given value - * - * @param string $value - * @return mixed|string - */ - static public function safeDecodeLabel($value) - { - if(empty($value)) { - return $value; - } - $raw = urldecode($value); - $value = htmlspecialchars_decode($raw , ENT_QUOTES); - if(self::$outputHtml) - { - // Pre 5.3 - if(!defined('ENT_IGNORE')) { - $style = ENT_QUOTES; - } else { - $style = ENT_QUOTES | ENT_IGNORE; - } - // See changes in 5.4: http://nikic.github.com/2012/01/28/htmlspecialchars-improvements-in-PHP-5-4.html - // Note: at some point we should change ENT_IGNORE to ENT_SUBSTITUTE - $value = htmlspecialchars($value, $style, 'UTF-8'); - } - return $value; - } + /** + * Decodes the given value + * + * @param string $value + * @return mixed|string + */ + static public function safeDecodeLabel($value) + { + if (empty($value)) { + return $value; + } + $raw = urldecode($value); + $value = htmlspecialchars_decode($raw, ENT_QUOTES); + if (self::$outputHtml) { + // Pre 5.3 + if (!defined('ENT_IGNORE')) { + $style = ENT_QUOTES; + } else { + $style = ENT_QUOTES | ENT_IGNORE; + } + // See changes in 5.4: http://nikic.github.com/2012/01/28/htmlspecialchars-improvements-in-PHP-5-4.html + // Note: at some point we should change ENT_IGNORE to ENT_SUBSTITUTE + $value = htmlspecialchars($value, $style, 'UTF-8'); + } + return $value; + } + + /** + * Decodes all columns of the given data table + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + foreach ($table->getRows() as $row) { + $value = $row->getColumn($this->columnToDecode); + if ($value !== false) { + $value = self::safeDecodeLabel($value); + $row->setColumn($this->columnToDecode, $value); + + $this->filterSubTable($row); + } + } + } - /** - * Decodes all columns of the given data table - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - foreach($table->getRows() as $row) - { - $value = $row->getColumn($this->columnToDecode); - if($value !== false) - { - $value = self::safeDecodeLabel($value); - $row->setColumn($this->columnToDecode,$value); - - $this->filterSubTable($row); - } - } - } - } diff --git a/core/DataTable/Filter/Sort.php b/core/DataTable/Filter/Sort.php index 89d5ee9e4c..ef56f70cad 100644 --- a/core/DataTable/Filter/Sort.php +++ b/core/DataTable/Filter/Sort.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -18,217 +18,196 @@ */ class Piwik_DataTable_Filter_Sort extends Piwik_DataTable_Filter { - protected $columnToSort; - protected $order; + protected $columnToSort; + protected $order; + + /** + * @param Piwik_DataTable $table + * @param string $columnToSort name of the column to sort by + * @param string $order order (asc|desc) + * @param bool $naturalSort use natural sort? + * @param bool $recursiveSort sort recursively? + */ + public function __construct($table, $columnToSort, $order = 'desc', $naturalSort = true, $recursiveSort = false) + { + parent::__construct($table); + if ($recursiveSort) { + $table->enableRecursiveSort(); + } + $this->columnToSort = $columnToSort; + $this->naturalSort = $naturalSort; + $this->setOrder($order); + } + + /** + * Updates the order + * + * @param string $order asc|desc + */ + public function setOrder($order) + { + if ($order == 'asc') { + $this->order = 'asc'; + $this->sign = 1; + } else { + $this->order = 'desc'; + $this->sign = -1; + } + } + + /** + * Sorting method used for sorting numbers + * + * @param number $a + * @param number $b + * @return int + */ + public function sort($a, $b) + { + return !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + && !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + + ? 0 + : ( + !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + ? 1 + : ( + !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + ? -1 + : (($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] != $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] + || !isset($a->c[Piwik_DataTable_Row::COLUMNS]['label'])) + ? ($this->sign * ( + $a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] + < $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] + ? -1 + : 1) + ) + : -1 * $this->sign * strnatcasecmp( + $a->c[Piwik_DataTable_Row::COLUMNS]['label'], + $b->c[Piwik_DataTable_Row::COLUMNS]['label']) + ) + ) + ); + } + + /** + * Sorting method used for sorting values natural + * + * @param mixed $a + * @param mixed $b + * @return int + */ + function naturalSort($a, $b) + { + return !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + && !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + ? 0 + : (!isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + ? 1 + : (!isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + ? -1 + : $this->sign * strnatcasecmp( + $a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort], + $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] + ) + ) + ); + } + + /** + * Sorting method used for sorting values + * + * @param mixed $a + * @param mixed $b + * @return int + */ + function sortString($a, $b) + { + return !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + && !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + ? 0 + : (!isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + ? 1 + : (!isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) + ? -1 + : $this->sign * + strcasecmp($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort], + $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] + ) + ) + ); + } - /** - * @param Piwik_DataTable $table - * @param string $columnToSort name of the column to sort by - * @param string $order order (asc|desc) - * @param bool $naturalSort use natural sort? - * @param bool $recursiveSort sort recursively? - */ - public function __construct( $table, $columnToSort, $order = 'desc', $naturalSort = true, $recursiveSort = false ) - { - parent::__construct($table); - if($recursiveSort) - { - $table->enableRecursiveSort(); - } - $this->columnToSort = $columnToSort; - $this->naturalSort = $naturalSort; - $this->setOrder($order); - } + /** + * Sets the column to be used for sorting + * + * @param Piwik_DataTable_Row $row + * @return int + */ + protected function selectColumnToSort($row) + { + $value = $row->getColumn($this->columnToSort); + if ($value !== false) { + return $this->columnToSort; + } - /** - * Updates the order - * - * @param string $order asc|desc - */ - public function setOrder($order) - { - if($order == 'asc') - { - $this->order = 'asc'; - $this->sign = 1; - } - else - { - $this->order = 'desc'; - $this->sign = -1; - } - } + // sorting by "nb_visits" but the index is Piwik_Archive::INDEX_NB_VISITS in the table + if (isset(Piwik_Archive::$mappingFromNameToId[$this->columnToSort])) { + $column = Piwik_Archive::$mappingFromNameToId[$this->columnToSort]; + $value = $row->getColumn($column); - /** - * Sorting method used for sorting numbers - * - * @param number $a - * @param number $b - * @return int - */ - public function sort($a, $b) - { - return !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) - && !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) - - ? 0 - : ( - !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) - ? 1 - : ( - !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) - ? -1 - : ( ($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] != $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] - || !isset($a->c[Piwik_DataTable_Row::COLUMNS]['label'])) - ? ( $this->sign * ( - $a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] - < $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] - ? -1 - : 1) - ) - : -1 * $this->sign * strnatcasecmp( - $a->c[Piwik_DataTable_Row::COLUMNS]['label'], - $b->c[Piwik_DataTable_Row::COLUMNS]['label']) - ) - ) - ) - ; - } + if ($value !== false) { + return $column; + } + } - /** - * Sorting method used for sorting values natural - * - * @param mixed $a - * @param mixed $b - * @return int - */ - function naturalSort($a, $b) - { - return !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) - && !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] ) - ? 0 - : (!isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) - ? 1 - : (!isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] ) - ? -1 - : $this->sign * strnatcasecmp( - $a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort], - $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] - ) - ) - ) - ; - } + // eg. was previously sorted by revenue_per_visit, but this table + // doesn't have this column; defaults with nb_visits + $column = Piwik_Archive::INDEX_NB_VISITS; + $value = $row->getColumn($column); + if ($value !== false) { + return $column; + } - /** - * Sorting method used for sorting values - * - * @param mixed $a - * @param mixed $b - * @return int - */ - function sortString($a, $b) - { - return !isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) - && !isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] ) - ? 0 - : (!isset($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort]) - ? 1 - : (!isset($b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] ) - ? -1 - : $this->sign * - strcasecmp($a->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort], - $b->c[Piwik_DataTable_Row::COLUMNS][$this->columnToSort] - ) - ) - ) - ; - } - - /** - * Sets the column to be used for sorting - * - * @param Piwik_DataTable_Row $row - * @return int - */ - protected function selectColumnToSort($row) - { - $value = $row->getColumn($this->columnToSort); - if($value !== false) - { - return $this->columnToSort; - } - - // sorting by "nb_visits" but the index is Piwik_Archive::INDEX_NB_VISITS in the table - if(isset(Piwik_Archive::$mappingFromNameToId[$this->columnToSort])) - { - $column = Piwik_Archive::$mappingFromNameToId[$this->columnToSort]; - $value = $row->getColumn($column); + // even though this column is not set properly in the table, + // we select it for the sort, so that the table's internal state is set properly + return $this->columnToSort; + } - if($value !== false) - { - return $column; - } - } - - // eg. was previously sorted by revenue_per_visit, but this table - // doesn't have this column; defaults with nb_visits - $column = Piwik_Archive::INDEX_NB_VISITS; - $value = $row->getColumn($column); - if($value !== false) - { - return $column; - } - - // even though this column is not set properly in the table, - // we select it for the sort, so that the table's internal state is set properly - return $this->columnToSort; - } + /** + * Sorts the given data table by defined column and sorting method + * + * @param Piwik_DataTable $table + * @return mixed + */ + public function filter($table) + { + if ($table instanceof Piwik_DataTable_Simple) { + return; + } + if (empty($this->columnToSort)) { + return; + } + $rows = $table->getRows(); + if (count($rows) == 0) { + return; + } + $row = current($rows); + if ($row === false) { + return; + } + $this->columnToSort = $this->selectColumnToSort($row); - /** - * Sorts the given data table by defined column and sorting method - * - * @param Piwik_DataTable $table - * @return mixed - */ - public function filter($table) - { - if($table instanceof Piwik_DataTable_Simple) - { - return; - } - if(empty($this->columnToSort)) - { - return; - } - $rows = $table->getRows(); - if(count($rows) == 0) - { - return; - } - $row = current($rows); - if($row === false) - { - return; - } - $this->columnToSort = $this->selectColumnToSort($row); - - $value = $row->getColumn($this->columnToSort); - if( is_numeric($value)) - { - $methodToUse = "sort"; - } - else - { - if($this->naturalSort) - { - $methodToUse = "naturalSort"; - } - else - { - $methodToUse = "sortString"; - } - } - $table->sort( array($this,$methodToUse), $this->columnToSort ); - } + $value = $row->getColumn($this->columnToSort); + if (is_numeric($value)) { + $methodToUse = "sort"; + } else { + if ($this->naturalSort) { + $methodToUse = "naturalSort"; + } else { + $methodToUse = "sortString"; + } + } + $table->sort(array($this, $methodToUse), $this->columnToSort); + } } diff --git a/core/DataTable/Filter/Truncate.php b/core/DataTable/Filter/Truncate.php index d66bc80405..7c917034f9 100644 --- a/core/DataTable/Filter/Truncate.php +++ b/core/DataTable/Filter/Truncate.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,34 +15,32 @@ */ class Piwik_DataTable_Filter_Truncate extends Piwik_DataTable_Filter { - /** - * @param Piwik_DataTable $table - * @param int $truncateAfter - */ - public function __construct( $table, $truncateAfter) - { - parent::__construct($table); - $this->truncateAfter = $truncateAfter; - } + /** + * @param Piwik_DataTable $table + * @param int $truncateAfter + */ + public function __construct($table, $truncateAfter) + { + parent::__construct($table); + $this->truncateAfter = $truncateAfter; + } - /** - * Truncates the table after X rows and adds a summary row - * - * @param Piwik_DataTable $table - */ - public function filter($table) - { - $table->filter('AddSummaryRow', array($this->truncateAfter)); - $table->filter('ReplaceSummaryRowLabel'); - - foreach($table->getRows() as $row) - { - if($row->isSubtableLoaded()) - { - $idSubTable = $row->getIdSubDataTable(); - $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); - $subTable->filter('Truncate', array($this->truncateAfter)); - } - } - } + /** + * Truncates the table after X rows and adds a summary row + * + * @param Piwik_DataTable $table + */ + public function filter($table) + { + $table->filter('AddSummaryRow', array($this->truncateAfter)); + $table->filter('ReplaceSummaryRowLabel'); + + foreach ($table->getRows() as $row) { + if ($row->isSubtableLoaded()) { + $idSubTable = $row->getIdSubDataTable(); + $subTable = Piwik_DataTable_Manager::getInstance()->getTable($idSubTable); + $subTable->filter('Truncate', array($this->truncateAfter)); + } + } + } } diff --git a/core/DataTable/Manager.php b/core/DataTable/Manager.php index c7062ebdaa..3fff1bcc2d 100644 --- a/core/DataTable/Manager.php +++ b/core/DataTable/Manager.php @@ -1,158 +1,149 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * The DataTable_Manager registers all the instanciated DataTable and provides an + * The DataTable_Manager registers all the instanciated DataTable and provides an * easy way to access them. This is used to store all the DataTable during the archiving process. * At the end of archiving, the ArchiveProcessing will read the stored datatable and record them in the DB. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Manager { - static private $instance = null; - /** - * Returns instance - * - * @return Piwik_DataTable_Manager - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Array used to store the DataTable - * - * @var array - */ - protected $tables = array(); - - /** - * Id of the next inserted table id in the Manager - * @var int - */ - protected $nextTableId = 1; - - /** - * Add a DataTable to the registry - * - * @param Piwik_DataTable $table - * @return int Index of the table in the manager array - */ - public function addTable( $table ) - { - $this->tables[$this->nextTableId] = $table; - $this->nextTableId++; - return $this->nextTableId - 1; - } + static private $instance = null; - /** - * Returns the DataTable associated to the ID $idTable. - * NB: The datatable has to have been instanciated before! - * This method will not fetch the DataTable from the DB. - * - * @param int $idTable - * @throws Exception If the table can't be found - * @return Piwik_DataTable The table - */ - public function getTable( $idTable ) - { - if(!isset($this->tables[$idTable])) - { - throw new Exception(sprintf("This report has been reprocessed since your last click. To see this error less often, please increase the timeout value in seconds in Settings > General Settings. (error: id %s not found).", $idTable)); - } - return $this->tables[$idTable]; - } - - /** - * Returns the latest used table ID - * - * @return int - */ - public function getMostRecentTableId() - { - return $this->nextTableId - 1; - } - - /** - * Delete all the registered DataTables from the manager - */ - public function deleteAll( $deleteWhenIdTableGreaterThan = 0) - { - foreach($this->tables as $id => $table) - { - if($id > $deleteWhenIdTableGreaterThan) - { - $this->deleteTable($id); - } - } - if($deleteWhenIdTableGreaterThan == 0) - { - $this->tables = array(); - $this->nextTableId = 1; - } - } - - /** - * Deletes (unsets) the datatable given its id and removes it from the manager - * Subsequent get for this table will fail - * - * @param int $id - */ - public function deleteTable( $id ) - { - if(isset($this->tables[$id])) - { - destroy($this->tables[$id]); - $this->setTableDeleted($id); - } - } - - /** - * Remove the table from the manager (table has already been unset) - * - * @param int $id - */ - public function setTableDeleted($id) - { - $this->tables[$id] = null; - } - - /** - * Debug only. Dumps all tables currently registered in the Manager - */ - public function dumpAllTables() - { - 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 />"; - var_dump($table); - } - else - { - echo "<hr />"; - echo "Table (index=$id) TableId = ". $table->getId() . "<br />"; - echo $table; - echo "<br />"; - } - } - echo "<br />-- End Piwik_DataTable_Manager->dumpAllTables()<hr />"; - } + /** + * Returns instance + * + * @return Piwik_DataTable_Manager + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Array used to store the DataTable + * + * @var array + */ + protected $tables = array(); + + /** + * Id of the next inserted table id in the Manager + * @var int + */ + protected $nextTableId = 1; + + /** + * Add a DataTable to the registry + * + * @param Piwik_DataTable $table + * @return int Index of the table in the manager array + */ + public function addTable($table) + { + $this->tables[$this->nextTableId] = $table; + $this->nextTableId++; + return $this->nextTableId - 1; + } + + /** + * Returns the DataTable associated to the ID $idTable. + * NB: The datatable has to have been instanciated before! + * This method will not fetch the DataTable from the DB. + * + * @param int $idTable + * @throws Exception If the table can't be found + * @return Piwik_DataTable The table + */ + public function getTable($idTable) + { + if (!isset($this->tables[$idTable])) { + throw new Exception(sprintf("This report has been reprocessed since your last click. To see this error less often, please increase the timeout value in seconds in Settings > General Settings. (error: id %s not found).", $idTable)); + } + return $this->tables[$idTable]; + } + + /** + * Returns the latest used table ID + * + * @return int + */ + public function getMostRecentTableId() + { + return $this->nextTableId - 1; + } + + /** + * Delete all the registered DataTables from the manager + */ + public function deleteAll($deleteWhenIdTableGreaterThan = 0) + { + foreach ($this->tables as $id => $table) { + if ($id > $deleteWhenIdTableGreaterThan) { + $this->deleteTable($id); + } + } + if ($deleteWhenIdTableGreaterThan == 0) { + $this->tables = array(); + $this->nextTableId = 1; + } + } + + /** + * Deletes (unsets) the datatable given its id and removes it from the manager + * Subsequent get for this table will fail + * + * @param int $id + */ + public function deleteTable($id) + { + if (isset($this->tables[$id])) { + destroy($this->tables[$id]); + $this->setTableDeleted($id); + } + } + + /** + * Remove the table from the manager (table has already been unset) + * + * @param int $id + */ + public function setTableDeleted($id) + { + $this->tables[$id] = null; + } + + /** + * Debug only. Dumps all tables currently registered in the Manager + */ + public function dumpAllTables() + { + 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 />"; + var_dump($table); + } else { + echo "<hr />"; + echo "Table (index=$id) TableId = " . $table->getId() . "<br />"; + echo $table; + echo "<br />"; + } + } + echo "<br />-- End Piwik_DataTable_Manager->dumpAllTables()<hr />"; + } } diff --git a/core/DataTable/Renderer.php b/core/DataTable/Renderer.php index fbc522f5e8..38855b65bc 100644 --- a/core/DataTable/Renderer.php +++ b/core/DataTable/Renderer.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -16,421 +16,399 @@ * $render = new Piwik_DataTable_Renderer_Xml(); * $render->setTable($dataTable); * echo $render; - * + * * @package Piwik * @subpackage Piwik_DataTable */ abstract class Piwik_DataTable_Renderer { - protected $table; - protected $exception; - protected $renderSubTables = false; - protected $hideIdSubDatatable = false; - - /** - * Whether to translate column names (i.e. metric names) or not - * @var bool - */ - public $translateColumnNames = false; - - /** - * Column translations - * @var array - */ - private $columnTranslations = false; - - /** - * The API method that has returned the data that should be rendered - * @var string - */ - public $apiMethod = false; - - /** - * API metadata for the current report - * @var array - */ - private $apiMetaData = null; - - /** - * The current idSite - * @var int - */ - public $idSite = 'all'; - - - public function __construct() - { - } - - /** - * Sets whether to render subtables or not - * - * @param bool $enableRenderSubTable - */ - public function setRenderSubTables($enableRenderSubTable) - { - $this->renderSubTables = (bool)$enableRenderSubTable; - } - - /** - * @param bool $bool - */ - public function setHideIdSubDatableFromResponse($bool) - { - $this->hideIdSubDatatable = (bool)$bool; - } - - /** - * Returns whether to render subtables or not - * - * @return bool - */ - protected function isRenderSubtables() - { - return $this->renderSubTables; - } - - /** - * Output HTTP Content-Type header - */ - protected function renderHeader() - { - @header('Content-Type: text/plain; charset=utf-8'); - } - - /** - * Computes the dataTable output and returns the string/binary - * - * @return string - */ - abstract public function render(); - - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - abstract public function renderException(); - - protected function getExceptionMessage() - { - $message = self::renderHtmlEntities($this->exception->getMessage()); - - // DEBUG + protected $table; + protected $exception; + protected $renderSubTables = false; + protected $hideIdSubDatatable = false; + + /** + * Whether to translate column names (i.e. metric names) or not + * @var bool + */ + public $translateColumnNames = false; + + /** + * Column translations + * @var array + */ + private $columnTranslations = false; + + /** + * The API method that has returned the data that should be rendered + * @var string + */ + public $apiMethod = false; + + /** + * API metadata for the current report + * @var array + */ + private $apiMetaData = null; + + /** + * The current idSite + * @var int + */ + public $idSite = 'all'; + + + public function __construct() + { + } + + /** + * Sets whether to render subtables or not + * + * @param bool $enableRenderSubTable + */ + public function setRenderSubTables($enableRenderSubTable) + { + $this->renderSubTables = (bool)$enableRenderSubTable; + } + + /** + * @param bool $bool + */ + public function setHideIdSubDatableFromResponse($bool) + { + $this->hideIdSubDatatable = (bool)$bool; + } + + /** + * Returns whether to render subtables or not + * + * @return bool + */ + protected function isRenderSubtables() + { + return $this->renderSubTables; + } + + /** + * Output HTTP Content-Type header + */ + protected function renderHeader() + { + @header('Content-Type: text/plain; charset=utf-8'); + } + + /** + * Computes the dataTable output and returns the string/binary + * + * @return string + */ + abstract public function render(); + + /** + * Computes the exception output and returns the string/binary + * + * @return string + */ + abstract public function renderException(); + + protected function getExceptionMessage() + { + $message = self::renderHtmlEntities($this->exception->getMessage()); + + // DEBUG // $message .= $this->exception->getTraceAsString(); - return $message; - } - - /** - * @see render() - * @return string - */ - public function __toString() - { - return $this->render(); - } - - /** - * Set the DataTable to be rendered - * - * @param Piwik_DataTable|Piwik_DataTable_Simple|Piwik_DataTable_Array $table table to be rendered - * @throws Exception - */ - public function setTable($table) - { - if (!is_array($table) - && !($table instanceof Piwik_DataTable) - && !($table instanceof Piwik_DataTable_Array)) - { - throw new Exception("DataTable renderers renderer accepts only Piwik_DataTable and Piwik_DataTable_Array instances, and array instances."); - } - $this->table = $table; - } - - /** - * Set the Exception to be rendered - * - * @param Exception $exception to be rendered - * @throws Exception - */ - public function setException($exception) - { - if(!($exception instanceof Exception)) - { - throw new Exception("The exception renderer accepts only an Exception object."); - } - $this->exception = $exception; - } - - - /** - * @var array - */ - static protected $availableRenderers = array( 'xml', - 'json', - 'csv', - 'tsv', - 'html', - 'php' - ); - - /** - * Returns available renderers - * - * @return array - */ - static public function getRenderers() - { - return self::$availableRenderers; - } - - /** - * Returns the DataTable associated to the output format $name - * - * @param string $name - * @throws Exception If the renderer is unknown - * @return Piwik_DataTable_Renderer - */ - static public function factory( $name ) - { - $name = ucfirst(strtolower($name)); - $className = 'Piwik_DataTable_Renderer_' . $name; - - try { - Piwik_Loader::loadClass($className); - return new $className; - } catch(Exception $e) { - $availableRenderers = implode(', ', self::getRenderers()); - @header('Content-Type: text/plain; charset=utf-8'); - 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 data to be converted - * @return String - */ - static protected function renderHtmlEntities( $rawData ) - { - return self::formatValueXml($rawData); - } - - /** - * Format a value to xml - * - * @param string|number|bool $value value to format - * @return int|string - */ - public static function formatValueXml($value) - { - if(is_string($value) - && !is_numeric($value)) - { - $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8'); - // make sure non-UTF-8 chars don't cause htmlspecialchars to choke - if (function_exists('mb_convert_encoding')) - { - $value = @mb_convert_encoding($value, 'UTF-8', 'UTF-8'); - } - $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); - $htmlentities = array( " ","¡","¢","£","¤","¥","¦","§","¨","©","ª","«","¬","­","®","¯","°","±","²","³","´","µ","¶","·","¸","¹","º","»","¼","½","¾","¿","À","Á","Â","Ã","Ä","Å","Æ","Ç","È","É","Ê","Ë","Ì","Í","Î","Ï","Ð","Ñ","Ò","Ó","Ô","Õ","Ö","×","Ø","Ù","Ú","Û","Ü","Ý","Þ","ß","à","á","â","ã","ä","å","æ","ç","è","é","ê","ë","ì","í","î","ï","ð","ñ","ò","ó","ô","õ","ö","÷","ø","ù","ú","û","ü","ý","þ","ÿ","€"); - $xmlentities = array( "¢","£","¤","¥","¦","§","¨","©","ª","«","¬","­","®","¯","°","±","²","³","´","µ","¶","·","¸","¹","º","»","¼","½","¾","¿","À","Á","Â","Ã","Ä","Å","Æ","Ç","È","É","Ê","Ë","Ì","Í","Î","Ï","Ð","Ñ","Ò","Ó","Ô","Õ","Ö","×","Ø","Ù","Ú","Û","Ü","Ý","Þ","ß","à","á","â","ã","ä","å","æ","ç","è","é","ê","ë","ì","í","î","ï","ð","ñ","ò","ó","ô","õ","ö","÷","ø","ù","ú","û","ü","ý","þ","ÿ","€" ); - $value = str_replace($htmlentities,$xmlentities,$value); - } - elseif($value===false) - { - $value = 0; - } - return $value; - } - - /** - * Translate column names to the current language. - * Used in subclasses. - * - * @param array $names - * @return array - */ - protected function translateColumnNames($names) - { - if (!$this->apiMethod) - { - return $names; - } - - // load the translations only once - // when multiple dates are requested (date=...,...&period=day), the meta data would - // be loaded lots of times otherwise - if ($this->columnTranslations === false) - { - $meta = $this->getApiMetaData(); - if ($meta === false) - { - return $names; - } - - $t = Piwik_API_API::getDefaultMetricTranslations(); - foreach (array('metrics', 'processedMetrics', 'metricsGoal', 'processedMetricsGoal') as $index) - { - if (isset($meta[$index]) && is_array($meta[$index])) - { - $t = array_merge($t, $meta[$index]); - } - } - - $this->columnTranslations = &$t; - } - - foreach ($names as &$name) - { - if (isset($this->columnTranslations[$name])) - { - $name = $this->columnTranslations[$name]; - } - } - - return $names; - } - - /** - * @return array|null - */ - protected function getApiMetaData() - { - if ($this->apiMetaData === null) - { - list($apiModule, $apiAction) = explode('.', $this->apiMethod); - - if(!$apiModule || !$apiAction) - { - $this->apiMetaData = false; - } - - $api = Piwik_API_API::getInstance(); - $meta = $api->getMetadata($this->idSite, $apiModule, $apiAction); - if (is_array($meta[0])) - { - $meta = $meta[0]; - } - - $this->apiMetaData = &$meta; - } - - return $this->apiMetaData; - } - - /** - * Translates the given column name - * - * @param string $column - * @return mixed - */ - protected function translateColumnName($column) - { - $columns = array($column); - $columns = $this->translateColumnNames($columns); - return $columns[0]; - } - - /** - * Enables column translating - * - * @param bool $bool - */ - public function setTranslateColumnNames($bool) - { - $this->translateColumnNames = $bool; - } - - /** - * Sets the api method - * - * @param $method - */ - public function setApiMethod($method) - { - $this->apiMethod = $method; - } - - /** - * Sets the site id - * - * @param int $idSite - */ - public function setIdSite($idSite) - { - $this->idSite = $idSite; - } - - /** - * Returns true if an array should be wrapped before rendering. This is used to - * mimic quirks in the old rendering logic (for backwards compatibility). The - * specific meaning of 'wrap' is left up to the Renderer. For XML, this means a - * new <row> node. For JSON, this means wrapping in an array. - * - * In the old code, arrays were added to new DataTable instances, and then rendered. - * This transformation wrapped associative arrays except under certain circumstances, - * including: - * - single element (ie, array('nb_visits' => 0)) (not wrapped for some renderers) - * - empty array (ie, array()) - * - array w/ arrays/DataTable instances as values (ie, - * array('name' => 'myreport', - * 'reportData' => new Piwik_DataTable()) - * OR array('name' => 'myreport', - * 'reportData' => array(...)) ) - * - * @param array $array - * @param bool $wrapSingleValues Whether to wrap array('key' => 'value') arrays. Some - * renderers wrap them and some don't. - * @param bool|null $isAssociativeArray Whether the array is associative or not. - * If null, it is determined. - * @return bool - */ - protected static function shouldWrapArrayBeforeRendering( - $array, $wrapSingleValues = true, $isAssociativeArray = null ) - { - if (empty($array)) - { - return false; - } - - if ($isAssociativeArray === null) - { - $isAssociativeArray = Piwik::isAssociativeArray($array); - } - - $wrap = true; - if ($isAssociativeArray) - { - // we don't wrap if the array has one element that is a value - $firstValue = reset($array); - if (!$wrapSingleValues - && count($array) === 1 - && (!is_array($firstValue) - && !is_object($firstValue))) - { - $wrap = false; - } - else - { - foreach ($array as $value) - { - if (is_array($value) - || is_object($value)) - { - $wrap = false; - break; - } - } - } - } - else - { - $wrap = false; - } - - return $wrap; - } + return $message; + } + + /** + * @see render() + * @return string + */ + public function __toString() + { + return $this->render(); + } + + /** + * Set the DataTable to be rendered + * + * @param Piwik_DataTable|Piwik_DataTable_Simple|Piwik_DataTable_Array $table table to be rendered + * @throws Exception + */ + public function setTable($table) + { + if (!is_array($table) + && !($table instanceof Piwik_DataTable) + && !($table instanceof Piwik_DataTable_Array) + ) { + throw new Exception("DataTable renderers renderer accepts only Piwik_DataTable and Piwik_DataTable_Array instances, and array instances."); + } + $this->table = $table; + } + + /** + * Set the Exception to be rendered + * + * @param Exception $exception to be rendered + * @throws Exception + */ + public function setException($exception) + { + if (!($exception instanceof Exception)) { + throw new Exception("The exception renderer accepts only an Exception object."); + } + $this->exception = $exception; + } + + + /** + * @var array + */ + static protected $availableRenderers = array('xml', + 'json', + 'csv', + 'tsv', + 'html', + 'php' + ); + + /** + * Returns available renderers + * + * @return array + */ + static public function getRenderers() + { + return self::$availableRenderers; + } + + /** + * Returns the DataTable associated to the output format $name + * + * @param string $name + * @throws Exception If the renderer is unknown + * @return Piwik_DataTable_Renderer + */ + static public function factory($name) + { + $name = ucfirst(strtolower($name)); + $className = 'Piwik_DataTable_Renderer_' . $name; + + try { + Piwik_Loader::loadClass($className); + return new $className; + } catch (Exception $e) { + $availableRenderers = implode(', ', self::getRenderers()); + @header('Content-Type: text/plain; charset=utf-8'); + 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 data to be converted + * @return String + */ + static protected function renderHtmlEntities($rawData) + { + return self::formatValueXml($rawData); + } + + /** + * Format a value to xml + * + * @param string|number|bool $value value to format + * @return int|string + */ + public static function formatValueXml($value) + { + if (is_string($value) + && !is_numeric($value) + ) { + $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8'); + // make sure non-UTF-8 chars don't cause htmlspecialchars to choke + if (function_exists('mb_convert_encoding')) { + $value = @mb_convert_encoding($value, 'UTF-8', 'UTF-8'); + } + $value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8'); + $htmlentities = array(" ", "¡", "¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "­", "®", "¯", "°", "±", "²", "³", "´", "µ", "¶", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à", "á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "€"); + $xmlentities = array("¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "­", "®", "¯", "°", "±", "²", "³", "´", "µ", "¶", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à", "á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "€"); + $value = str_replace($htmlentities, $xmlentities, $value); + } elseif ($value === false) { + $value = 0; + } + return $value; + } + + /** + * Translate column names to the current language. + * Used in subclasses. + * + * @param array $names + * @return array + */ + protected function translateColumnNames($names) + { + if (!$this->apiMethod) { + return $names; + } + + // load the translations only once + // when multiple dates are requested (date=...,...&period=day), the meta data would + // be loaded lots of times otherwise + if ($this->columnTranslations === false) { + $meta = $this->getApiMetaData(); + if ($meta === false) { + return $names; + } + + $t = Piwik_API_API::getDefaultMetricTranslations(); + foreach (array('metrics', 'processedMetrics', 'metricsGoal', 'processedMetricsGoal') as $index) { + if (isset($meta[$index]) && is_array($meta[$index])) { + $t = array_merge($t, $meta[$index]); + } + } + + $this->columnTranslations = & $t; + } + + foreach ($names as &$name) { + if (isset($this->columnTranslations[$name])) { + $name = $this->columnTranslations[$name]; + } + } + + return $names; + } + + /** + * @return array|null + */ + protected function getApiMetaData() + { + if ($this->apiMetaData === null) { + list($apiModule, $apiAction) = explode('.', $this->apiMethod); + + if (!$apiModule || !$apiAction) { + $this->apiMetaData = false; + } + + $api = Piwik_API_API::getInstance(); + $meta = $api->getMetadata($this->idSite, $apiModule, $apiAction); + if (is_array($meta[0])) { + $meta = $meta[0]; + } + + $this->apiMetaData = & $meta; + } + + return $this->apiMetaData; + } + + /** + * Translates the given column name + * + * @param string $column + * @return mixed + */ + protected function translateColumnName($column) + { + $columns = array($column); + $columns = $this->translateColumnNames($columns); + return $columns[0]; + } + + /** + * Enables column translating + * + * @param bool $bool + */ + public function setTranslateColumnNames($bool) + { + $this->translateColumnNames = $bool; + } + + /** + * Sets the api method + * + * @param $method + */ + public function setApiMethod($method) + { + $this->apiMethod = $method; + } + + /** + * Sets the site id + * + * @param int $idSite + */ + public function setIdSite($idSite) + { + $this->idSite = $idSite; + } + + /** + * Returns true if an array should be wrapped before rendering. This is used to + * mimic quirks in the old rendering logic (for backwards compatibility). The + * specific meaning of 'wrap' is left up to the Renderer. For XML, this means a + * new <row> node. For JSON, this means wrapping in an array. + * + * In the old code, arrays were added to new DataTable instances, and then rendered. + * This transformation wrapped associative arrays except under certain circumstances, + * including: + * - single element (ie, array('nb_visits' => 0)) (not wrapped for some renderers) + * - empty array (ie, array()) + * - array w/ arrays/DataTable instances as values (ie, + * array('name' => 'myreport', + * 'reportData' => new Piwik_DataTable()) + * OR array('name' => 'myreport', + * 'reportData' => array(...)) ) + * + * @param array $array + * @param bool $wrapSingleValues Whether to wrap array('key' => 'value') arrays. Some + * renderers wrap them and some don't. + * @param bool|null $isAssociativeArray Whether the array is associative or not. + * If null, it is determined. + * @return bool + */ + protected static function shouldWrapArrayBeforeRendering( + $array, $wrapSingleValues = true, $isAssociativeArray = null) + { + if (empty($array)) { + return false; + } + + if ($isAssociativeArray === null) { + $isAssociativeArray = Piwik::isAssociativeArray($array); + } + + $wrap = true; + if ($isAssociativeArray) { + // we don't wrap if the array has one element that is a value + $firstValue = reset($array); + if (!$wrapSingleValues + && count($array) === 1 + && (!is_array($firstValue) + && !is_object($firstValue)) + ) { + $wrap = false; + } else { + foreach ($array as $value) { + if (is_array($value) + || is_object($value) + ) { + $wrap = false; + break; + } + } + } + } else { + $wrap = false; + } + + return $wrap; + } } diff --git a/core/DataTable/Renderer/Console.php b/core/DataTable/Renderer/Console.php index 941471f91d..c78b4eda1f 100644 --- a/core/DataTable/Renderer/Console.php +++ b/core/DataTable/Renderer/Console.php @@ -1,179 +1,164 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Simple output - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Renderer_Console extends Piwik_DataTable_Renderer { - /** - * Prefix - * - * @var string - */ - protected $prefixRows = '#'; + /** + * Prefix + * + * @var string + */ + protected $prefixRows = '#'; - /** - * Computes the dataTable output and returns the string/binary - * - * @return string - */ - public function render() - { - $this->renderHeader(); - return $this->renderTable($this->table); - } + /** + * Computes the dataTable output and returns the string/binary + * + * @return string + */ + public function render() + { + $this->renderHeader(); + return $this->renderTable($this->table); + } - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - public function renderException() - { - $this->renderHeader(); - $exceptionMessage = $this->getExceptionMessage(); - return 'Error: '.$exceptionMessage; - } + /** + * Computes the exception output and returns the string/binary + * + * @return string + */ + public function renderException() + { + $this->renderHeader(); + $exceptionMessage = $this->getExceptionMessage(); + return 'Error: ' . $exceptionMessage; + } - /** - * Sets the prefix to be used - * - * @param string $str new prefix - */ - public function setPrefixRow($str) - { - $this->prefixRows = $str; - } + /** + * Sets the prefix to be used + * + * @param string $str new prefix + */ + public function setPrefixRow($str) + { + $this->prefixRows = $str; + } - /** - * Computes the output of the given array of data tables - * - * @param Piwik_DataTable_Array $tableArray data tables to render - * @param string $prefix prefix to output before table data - * @return string - */ - protected function renderDataTableArray(Piwik_DataTable_Array $tableArray, $prefix ) - { - $output = "Piwik_DataTable_Array<hr />"; - $prefix = $prefix . ' '; - foreach($tableArray->getArray() as $descTable => $table) - { - $output .= $prefix . "<b>". $descTable. "</b><br />"; - $output .= $prefix . $this->renderTable($table, $prefix . ' '); - $output .= "<hr />"; - } - return $output; - } + /** + * Computes the output of the given array of data tables + * + * @param Piwik_DataTable_Array $tableArray data tables to render + * @param string $prefix prefix to output before table data + * @return string + */ + protected function renderDataTableArray(Piwik_DataTable_Array $tableArray, $prefix) + { + $output = "Piwik_DataTable_Array<hr />"; + $prefix = $prefix . ' '; + foreach ($tableArray->getArray() as $descTable => $table) { + $output .= $prefix . "<b>" . $descTable . "</b><br />"; + $output .= $prefix . $this->renderTable($table, $prefix . ' '); + $output .= "<hr />"; + } + return $output; + } - /** - * Computes the given dataTable output and returns the string/binary - * - * @param Piwik_DataTable $table data table to render - * @param string $prefix prefix to output before table data - * @return string - */ - protected function renderTable($table, $prefix = "") - { - if (is_array($table)) // convert array to DataTable - { - $table = Piwik_DataTable::makeFromSimpleArray($table); - } - - if($table instanceof Piwik_DataTable_Array) - { - return $this->renderDataTableArray($table, $prefix); - } - - if($table->getRowsCount() == 0) - { - return "Empty table<br />\n"; - } - - static $depth=0; - $output = ''; - $i = 1; - foreach($table->getRows() as $row) - { - $dataTableArrayBreak = false; - $columns=array(); - foreach($row->getColumns() as $column => $value) - { - if($value instanceof Piwik_DataTable_Array ) - { - $output .= $this->renderDataTableArray($value, $prefix); - $dataTableArrayBreak = true; - break; - } - if(is_string($value)) $value = "'$value'"; - elseif(is_array($value)) $value = var_export($value, true); - - $columns[] = "'$column' => $value"; - } - if($dataTableArrayBreak === true) - { - continue; - } - $columns = implode(", ", $columns); - - $metadata = array(); - foreach($row->getMetadata() as $name => $value) - { - if(is_string($value)) $value = "'$value'"; - elseif(is_array($value)) $value = var_export($value, true); - $metadata[] = "'$name' => $value"; - } - $metadata = implode(", ", $metadata); - - $output.= str_repeat($this->prefixRows, $depth) - . "- $i [".$columns."] [".$metadata."] [idsubtable = " - . $row->getIdSubDataTable()."]<br />\n"; - - if(!is_null($row->getIdSubDataTable())) - { - if($row->isSubtableLoaded()) - { - $depth++; - $output.= $this->renderTable( - Piwik_DataTable_Manager::getInstance()->getTable( - $row->getIdSubDataTable() - ), - $prefix . ' ' - ); - $depth--; - } - else - { - $output.= "-- Sub DataTable not loaded<br />\n"; - } - } - $i++; - } - - if (!empty($table->metadata)) - { - $output .= "<hr />Metadata<br />"; - foreach($table->metadata as $id => $metadata) - { - $output .= "<br />"; - $output .= $prefix . " <b>$id</b><br />"; - foreach($metadata as $name => $value) - { - $output .= $prefix . $prefix . "$name => $value"; - } - } - } - return $output; - } + /** + * Computes the given dataTable output and returns the string/binary + * + * @param Piwik_DataTable $table data table to render + * @param string $prefix prefix to output before table data + * @return string + */ + protected function renderTable($table, $prefix = "") + { + if (is_array($table)) // convert array to DataTable + { + $table = Piwik_DataTable::makeFromSimpleArray($table); + } + + if ($table instanceof Piwik_DataTable_Array) { + return $this->renderDataTableArray($table, $prefix); + } + + if ($table->getRowsCount() == 0) { + return "Empty table<br />\n"; + } + + static $depth = 0; + $output = ''; + $i = 1; + foreach ($table->getRows() as $row) { + $dataTableArrayBreak = false; + $columns = array(); + foreach ($row->getColumns() as $column => $value) { + if ($value instanceof Piwik_DataTable_Array) { + $output .= $this->renderDataTableArray($value, $prefix); + $dataTableArrayBreak = true; + break; + } + if (is_string($value)) $value = "'$value'"; + elseif (is_array($value)) $value = var_export($value, true); + + $columns[] = "'$column' => $value"; + } + if ($dataTableArrayBreak === true) { + continue; + } + $columns = implode(", ", $columns); + + $metadata = array(); + foreach ($row->getMetadata() as $name => $value) { + if (is_string($value)) $value = "'$value'"; + elseif (is_array($value)) $value = var_export($value, true); + $metadata[] = "'$name' => $value"; + } + $metadata = implode(", ", $metadata); + + $output .= str_repeat($this->prefixRows, $depth) + . "- $i [" . $columns . "] [" . $metadata . "] [idsubtable = " + . $row->getIdSubDataTable() . "]<br />\n"; + + if (!is_null($row->getIdSubDataTable())) { + if ($row->isSubtableLoaded()) { + $depth++; + $output .= $this->renderTable( + Piwik_DataTable_Manager::getInstance()->getTable( + $row->getIdSubDataTable() + ), + $prefix . ' ' + ); + $depth--; + } else { + $output .= "-- Sub DataTable not loaded<br />\n"; + } + } + $i++; + } + + if (!empty($table->metadata)) { + $output .= "<hr />Metadata<br />"; + foreach ($table->metadata as $id => $metadata) { + $output .= "<br />"; + $output .= $prefix . " <b>$id</b><br />"; + foreach ($metadata as $name => $value) { + $output .= $prefix . $prefix . "$name => $value"; + } + } + } + return $output; + } } diff --git a/core/DataTable/Renderer/Csv.php b/core/DataTable/Renderer/Csv.php index 4e97840b05..1e61e62aef 100644 --- a/core/DataTable/Renderer/Csv.php +++ b/core/DataTable/Renderer/Csv.php @@ -1,415 +1,371 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * CSV export - * + * * When rendered using the default settings, a CSV report has the following characteristics: * The first record contains headers for all the columns in the report. * All rows have the same number of columns. * The default field delimiter string is a comma (,). * Formatting and layout are ignored. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Renderer_Csv extends Piwik_DataTable_Renderer { - /** - * Column separator - * - * @var string - */ - public $separator = ","; - - /** - * Line end - * - * @var string - */ - public $lineEnd = "\n"; - - /** - * 'metadata' columns will be exported, prefixed by 'metadata_' - * - * @var bool - */ - public $exportMetadata = true; - - /** - * Converts the content to unicode so that UTF8 characters (eg. chinese) can be imported in Excel - * - * @var bool - */ - public $convertToUnicode = true; - - /** - * idSubtable will be exported in a column called 'idsubdatatable' - * - * @var bool - */ - public $exportIdSubtable = true; - - /** - * Computes the dataTable output and returns the string/binary - * - * @return string - */ - public function render() - { - $str = $this->renderTable($this->table); - if(empty($str)) - { - return 'No data available'; - } - - $this->renderHeader(); - - if($this->convertToUnicode - && function_exists('mb_convert_encoding')) - { - $str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8'); - } - return $str; - } - - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - @header('Content-Type: text/html; charset=utf-8'); - $exceptionMessage = $this->getExceptionMessage(); - return 'Error: '.$exceptionMessage; - } - - /** - * Enables / Disables unicode converting - * - * @param $bool - */ - public function setConvertToUnicode($bool) - { - $this->convertToUnicode = $bool; - } - - /** - * Sets the column separator - * - * @param $separator - */ - public function setSeparator($separator) - { - $this->separator = $separator; - } - - /** - * Computes the output of the given data table - * - * @param Piwik_DataTable|array $table - * @param array $allColumns - * @return string - */ - protected function renderTable($table, &$allColumns = array() ) - { - if (is_array($table)) // convert array to DataTable - { - $table = Piwik_DataTable::makeFromSimpleArray($table); - } - - if($table instanceof Piwik_DataTable_Array) - { - $str = $this->renderDataTableArray($table, $allColumns); - } - else - { - $str = $this->renderDataTable($table, $allColumns); - } - return $str; - } - - /** - * Computes the output of the given data table array - * - * @param Piwik_DataTable_Array $table - * @param array $allColumns - * @return string - */ - protected function renderDataTableArray($table, &$allColumns = array()) - { - $str = ''; - foreach($table->getArray() as $currentLinePrefix => $dataTable) - { - $returned = explode("\n",$this->renderTable($dataTable, $allColumns)); - - // get rid of the columns names - $returned = array_slice($returned,1); - - // case empty datatable we dont print anything in the CSV export - // when in xml we would output <result date="2008-01-15" /> - if(!empty($returned)) - { - foreach($returned as &$row) - { - $row = $currentLinePrefix . $this->separator . $row; - } - $str .= "\n" . implode("\n", $returned); - } - } - - // prepend table key to column list - $allColumns = array_merge(array($table->getKeyName() => true), $allColumns); - - // add header to output string - $str = $this->getHeaderLine(array_keys($allColumns)).$str; - - return $str; - } - - /** - * Converts the output of the given simple data table - * - * @param Piwik_DataTable_Simple $table - * @param array $allColumns - * @return string - */ - protected function renderDataTable( $table, &$allColumns = array() ) - { - if($table instanceof Piwik_DataTable_Simple) - { - $row = $table->getFirstRow(); - if($row !== false) - { - $columnNameToValue = $row->getColumns(); - if(count($columnNameToValue) == 1) - { - // simple tables should only have one column, the value - $allColumns['value'] = true; - - $value = array_values($columnNameToValue); - $str = 'value' . $this->lineEnd . $this->formatValue($value[0]); - return $str; - } - } - } - $csv = array(); - foreach($table->getRows() as $row) - { - $csvRow = array(); - - $columns = $row->getColumns(); - foreach($columns as $name => $value) - { - //goals => array( 'idgoal=1' =>array(..), 'idgoal=2' => array(..)) - if(is_array($value)) - { - foreach($value as $key => $subValues) - { - if(is_array($subValues)) - { - foreach($subValues as $subKey => $subValue) - { - if ($this->translateColumnNames) - { - $subName = $name != 'goals' ? $name . ' ' . $key - : Piwik_Translate('Goals_GoalX', $key); - $columnName = $this->translateColumnName($subKey) - .' ('. $subName . ')'; - } - else - { - // goals_idgoal=1 - $columnName = $name . "_" . $key . "_" . $subKey; - } - $allColumns[$columnName] = true; - $csvRow[$columnName] = $subValue; - } - } - } - } - else - { - $allColumns[$name] = true; - $csvRow[$name] = $value; - } - } - - if($this->exportMetadata) - { - $metadata = $row->getMetadata(); - foreach($metadata as $name => $value) - { - if($name == 'idsubdatatable_in_db') { - continue; - } - //if a metadata and a column have the same name make sure they dont overwrite - if($this->translateColumnNames) - { - $name = Piwik_Translate('General_Metadata').': '.$name; - } - else - { - $name = 'metadata_'.$name; - } - - $allColumns[$name] = true; - $csvRow[$name] = $value; - } - } - - if($this->exportIdSubtable) - { - $idsubdatatable = $row->getIdSubDataTable(); - if($idsubdatatable !== false - && $this->hideIdSubDatatable === false) - { - $csvRow['idsubdatatable'] = $idsubdatatable; - } - } - - $csv[] = $csvRow; - } - - // now we make sure that all the rows in the CSV array have all the columns - foreach($csv as &$row) - { - foreach($allColumns as $columnName => $true) - { - if(!isset($row[$columnName])) - { - $row[$columnName] = ''; - } - } - } - - $str = ''; - - // specific case, we have only one column and this column wasn't named properly (indexed by a number) - // we don't print anything in the CSV file => an empty line - if(sizeof($allColumns) == 1 - && reset($allColumns) - && !is_string(key($allColumns))) - { - $str .= ''; - } - else - { - // render row names - $str .= $this->getHeaderLine(array_keys($allColumns)).$this->lineEnd; - } - - // we render the CSV - foreach($csv as $theRow) - { - $rowStr = ''; - foreach($allColumns as $columnName => $true) - { - $rowStr .= $this->formatValue($theRow[$columnName]) . $this->separator; - } - // remove the last separator - $rowStr = substr_replace($rowStr,"",-strlen($this->separator)); - $str .= $rowStr . $this->lineEnd; - } - $str = substr($str, 0, -strlen($this->lineEnd)); - return $str; - } - - /** - * Returns the CSV header line for a set of metrics. Will translate columns if desired. - * - * @param array $columnMetrics - * @return array - */ - private function getHeaderLine( $columnMetrics ) - { - if ($this->translateColumnNames) - { - $columnMetrics = $this->translateColumnNames($columnMetrics); - } - return implode($this->separator, $columnMetrics); - } - - /** - * Formats/Escapes the given value - * - * @param mixed $value - * @return string - */ - protected function formatValue($value) - { - if(is_string($value) - && !is_numeric($value)) - { - $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8'); - } - elseif($value === false) - { - $value = 0; - } - if(is_string($value) - && (strpos($value, '"') !== false - || strpos($value, $this->separator) !== false ) - ) - { - $value = '"'. str_replace('"', '""', $value). '"'; - } - - // in some number formats (e.g. German), the decimal separator is a comma - // we need to catch and replace this - if (is_numeric($value)) - { - $value = (string) $value; - $value = str_replace(',', '.', $value); - } - - return $value; - } - - /** - * Sends the http headers for csv file - */ - protected function renderHeader() - { - $fileName = 'Piwik '.Piwik_Translate('General_Export'); - - $period = Piwik_Common::getRequestVar('period', false); - $date = Piwik_Common::getRequestVar('date', false); - if ($period || $date) // in test cases, there are no request params set - { - if ($period == 'range') - { - $period = new Piwik_Period_Range($period, $date); - } - else if (strpos($date, ',') !== false) - { - $period = new Piwik_Period_Range('range', $date); - } - else - { - $period = Piwik_Period::factory($period, Piwik_Date::factory($date)); - } - - $prettyDate = $period->getLocalizedLongString(); - - $meta = $this->getApiMetaData(); - - $fileName .= ' _ '.$meta['name'] - .' _ '.$prettyDate.'.csv'; - } - - // silent fail otherwise unit tests fail - @header('Content-Type: application/vnd.ms-excel'); - @header('Content-Disposition: attachment; filename="'.$fileName.'"'); - Piwik::overrideCacheControlHeaders(); - } + /** + * Column separator + * + * @var string + */ + public $separator = ","; + + /** + * Line end + * + * @var string + */ + public $lineEnd = "\n"; + + /** + * 'metadata' columns will be exported, prefixed by 'metadata_' + * + * @var bool + */ + public $exportMetadata = true; + + /** + * Converts the content to unicode so that UTF8 characters (eg. chinese) can be imported in Excel + * + * @var bool + */ + public $convertToUnicode = true; + + /** + * idSubtable will be exported in a column called 'idsubdatatable' + * + * @var bool + */ + public $exportIdSubtable = true; + + /** + * Computes the dataTable output and returns the string/binary + * + * @return string + */ + public function render() + { + $str = $this->renderTable($this->table); + if (empty($str)) { + return 'No data available'; + } + + $this->renderHeader(); + + if ($this->convertToUnicode + && function_exists('mb_convert_encoding') + ) { + $str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8'); + } + return $str; + } + + /** + * Computes the exception output and returns the string/binary + * + * @return string + */ + function renderException() + { + @header('Content-Type: text/html; charset=utf-8'); + $exceptionMessage = $this->getExceptionMessage(); + return 'Error: ' . $exceptionMessage; + } + + /** + * Enables / Disables unicode converting + * + * @param $bool + */ + public function setConvertToUnicode($bool) + { + $this->convertToUnicode = $bool; + } + + /** + * Sets the column separator + * + * @param $separator + */ + public function setSeparator($separator) + { + $this->separator = $separator; + } + + /** + * Computes the output of the given data table + * + * @param Piwik_DataTable|array $table + * @param array $allColumns + * @return string + */ + protected function renderTable($table, &$allColumns = array()) + { + if (is_array($table)) // convert array to DataTable + { + $table = Piwik_DataTable::makeFromSimpleArray($table); + } + + if ($table instanceof Piwik_DataTable_Array) { + $str = $this->renderDataTableArray($table, $allColumns); + } else { + $str = $this->renderDataTable($table, $allColumns); + } + return $str; + } + + /** + * Computes the output of the given data table array + * + * @param Piwik_DataTable_Array $table + * @param array $allColumns + * @return string + */ + protected function renderDataTableArray($table, &$allColumns = array()) + { + $str = ''; + foreach ($table->getArray() as $currentLinePrefix => $dataTable) { + $returned = explode("\n", $this->renderTable($dataTable, $allColumns)); + + // get rid of the columns names + $returned = array_slice($returned, 1); + + // case empty datatable we dont print anything in the CSV export + // when in xml we would output <result date="2008-01-15" /> + if (!empty($returned)) { + foreach ($returned as &$row) { + $row = $currentLinePrefix . $this->separator . $row; + } + $str .= "\n" . implode("\n", $returned); + } + } + + // prepend table key to column list + $allColumns = array_merge(array($table->getKeyName() => true), $allColumns); + + // add header to output string + $str = $this->getHeaderLine(array_keys($allColumns)) . $str; + + return $str; + } + + /** + * Converts the output of the given simple data table + * + * @param Piwik_DataTable_Simple $table + * @param array $allColumns + * @return string + */ + protected function renderDataTable($table, &$allColumns = array()) + { + if ($table instanceof Piwik_DataTable_Simple) { + $row = $table->getFirstRow(); + if ($row !== false) { + $columnNameToValue = $row->getColumns(); + if (count($columnNameToValue) == 1) { + // simple tables should only have one column, the value + $allColumns['value'] = true; + + $value = array_values($columnNameToValue); + $str = 'value' . $this->lineEnd . $this->formatValue($value[0]); + return $str; + } + } + } + $csv = array(); + foreach ($table->getRows() as $row) { + $csvRow = array(); + + $columns = $row->getColumns(); + foreach ($columns as $name => $value) { + //goals => array( 'idgoal=1' =>array(..), 'idgoal=2' => array(..)) + if (is_array($value)) { + foreach ($value as $key => $subValues) { + if (is_array($subValues)) { + foreach ($subValues as $subKey => $subValue) { + if ($this->translateColumnNames) { + $subName = $name != 'goals' ? $name . ' ' . $key + : Piwik_Translate('Goals_GoalX', $key); + $columnName = $this->translateColumnName($subKey) + . ' (' . $subName . ')'; + } else { + // goals_idgoal=1 + $columnName = $name . "_" . $key . "_" . $subKey; + } + $allColumns[$columnName] = true; + $csvRow[$columnName] = $subValue; + } + } + } + } else { + $allColumns[$name] = true; + $csvRow[$name] = $value; + } + } + + if ($this->exportMetadata) { + $metadata = $row->getMetadata(); + foreach ($metadata as $name => $value) { + if ($name == 'idsubdatatable_in_db') { + continue; + } + //if a metadata and a column have the same name make sure they dont overwrite + if ($this->translateColumnNames) { + $name = Piwik_Translate('General_Metadata') . ': ' . $name; + } else { + $name = 'metadata_' . $name; + } + + $allColumns[$name] = true; + $csvRow[$name] = $value; + } + } + + if ($this->exportIdSubtable) { + $idsubdatatable = $row->getIdSubDataTable(); + if ($idsubdatatable !== false + && $this->hideIdSubDatatable === false + ) { + $csvRow['idsubdatatable'] = $idsubdatatable; + } + } + + $csv[] = $csvRow; + } + + // now we make sure that all the rows in the CSV array have all the columns + foreach ($csv as &$row) { + foreach ($allColumns as $columnName => $true) { + if (!isset($row[$columnName])) { + $row[$columnName] = ''; + } + } + } + + $str = ''; + + // specific case, we have only one column and this column wasn't named properly (indexed by a number) + // we don't print anything in the CSV file => an empty line + if (sizeof($allColumns) == 1 + && reset($allColumns) + && !is_string(key($allColumns)) + ) { + $str .= ''; + } else { + // render row names + $str .= $this->getHeaderLine(array_keys($allColumns)) . $this->lineEnd; + } + + // we render the CSV + foreach ($csv as $theRow) { + $rowStr = ''; + foreach ($allColumns as $columnName => $true) { + $rowStr .= $this->formatValue($theRow[$columnName]) . $this->separator; + } + // remove the last separator + $rowStr = substr_replace($rowStr, "", -strlen($this->separator)); + $str .= $rowStr . $this->lineEnd; + } + $str = substr($str, 0, -strlen($this->lineEnd)); + return $str; + } + + /** + * Returns the CSV header line for a set of metrics. Will translate columns if desired. + * + * @param array $columnMetrics + * @return array + */ + private function getHeaderLine($columnMetrics) + { + if ($this->translateColumnNames) { + $columnMetrics = $this->translateColumnNames($columnMetrics); + } + return implode($this->separator, $columnMetrics); + } + + /** + * Formats/Escapes the given value + * + * @param mixed $value + * @return string + */ + protected function formatValue($value) + { + if (is_string($value) + && !is_numeric($value) + ) { + $value = html_entity_decode($value, ENT_COMPAT, 'UTF-8'); + } elseif ($value === false) { + $value = 0; + } + if (is_string($value) + && (strpos($value, '"') !== false + || strpos($value, $this->separator) !== false) + ) { + $value = '"' . str_replace('"', '""', $value) . '"'; + } + + // in some number formats (e.g. German), the decimal separator is a comma + // we need to catch and replace this + if (is_numeric($value)) { + $value = (string)$value; + $value = str_replace(',', '.', $value); + } + + return $value; + } + + /** + * Sends the http headers for csv file + */ + protected function renderHeader() + { + $fileName = 'Piwik ' . Piwik_Translate('General_Export'); + + $period = Piwik_Common::getRequestVar('period', false); + $date = Piwik_Common::getRequestVar('date', false); + if ($period || $date) // in test cases, there are no request params set + { + if ($period == 'range') { + $period = new Piwik_Period_Range($period, $date); + } else if (strpos($date, ',') !== false) { + $period = new Piwik_Period_Range('range', $date); + } else { + $period = Piwik_Period::factory($period, Piwik_Date::factory($date)); + } + + $prettyDate = $period->getLocalizedLongString(); + + $meta = $this->getApiMetaData(); + + $fileName .= ' _ ' . $meta['name'] + . ' _ ' . $prettyDate . '.csv'; + } + + // silent fail otherwise unit tests fail + @header('Content-Type: application/vnd.ms-excel'); + @header('Content-Disposition: attachment; filename="' . $fileName . '"'); + Piwik::overrideCacheControlHeaders(); + } } diff --git a/core/DataTable/Renderer/Html.php b/core/DataTable/Renderer/Html.php index 11fbdbc1e3..1d620f74cc 100644 --- a/core/DataTable/Renderer/Html.php +++ b/core/DataTable/Renderer/Html.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,217 +12,195 @@ /** * Simple HTML output * Does not work with recursive DataTable (i.e., when a row can be associated with a subDataTable). - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Renderer_Html extends Piwik_DataTable_Renderer { - protected $tableId; - protected $allColumns; - protected $tableStructure; - protected $i; - - /** - * Sets the table id - * - * @param string $id - */ - function setTableId($id) - { - $this->tableId = str_replace('.', '_', $id); - } - - /** - * Output HTTP Content-Type header - */ - protected function renderHeader() - { - @header('Content-Type: text/html; charset=utf-8'); - } - - /** - * Computes the dataTable output and returns the string/binary - * - * @return string - */ - function render() - { - $this->renderHeader(); - $this->tableStructure = array(); - $this->allColumns = array(); - $this->i = 0; - - return $this->renderTable($this->table); - } - - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - $this->renderHeader(); - $exceptionMessage = $this->getExceptionMessage(); - return nl2br($exceptionMessage); - } - - /** - * Computes the output for the given data table - * - * @param Piwik_DataTable $table - * @return string - */ - protected function renderTable($table) - { - if (is_array($table)) // convert array to DataTable - { - $table = Piwik_DataTable::makeFromSimpleArray($table); - } - - if($table instanceof Piwik_DataTable_Array) - { - foreach($table->getArray() as $date => $subtable ) - { - if ($subtable->getRowsCount()) { - $this->buildTableStructure($subtable, '_'. $table->getKeyName(), $date); - } - } - } - else // Piwik_DataTable_Simple - { - if($table->getRowsCount()) - { - $this->buildTableStructure($table); - } - } - - $out = $this->renderDataTable(); - return $out; - } - - /** - * Adds the given data table to the table structure array - * - * @param Piwik_DataTable_Simple $table - * @param null|string $columnToAdd - * @param null|string $valueToAdd - * @throws Exception - */ - protected function buildTableStructure($table, $columnToAdd = null, $valueToAdd = null) - { - $i = $this->i; - $someMetadata = false; - $someIdSubTable = false; - - /* - * table = array - * ROW1 = col1 | col2 | col3 | metadata | idSubTable - * ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable - */ - if(!($table instanceof Piwik_DataTable)) - { - throw new Exception("HTML Renderer does not work with this combination of parameters"); - } - foreach($table->getRows() as $row) - { - if(isset($columnToAdd) && isset($valueToAdd)) - { - $this->allColumns[$columnToAdd] = true; - $this->tableStructure[$i][$columnToAdd] = $valueToAdd; - } - - foreach($row->getColumns() as $column => $value) - { - $this->allColumns[$column] = true; - $this->tableStructure[$i][$column] = $value; - } - - $metadata = array(); - foreach($row->getMetadata() as $name => $value) - { - if(is_string($value)) $value = "'$value'"; - $metadata[] = "'$name' => $value"; - } - - if(count($metadata) != 0) - { - $someMetadata = true; - $metadata = implode("<br />", $metadata); - $this->tableStructure[$i]['_metadata'] = $metadata; - } - - $idSubtable = $row->getIdSubDataTable(); - if(!is_null($idSubtable)) - { - $someIdSubTable = true; - $this->tableStructure[$i]['_idSubtable'] = $idSubtable; - } - - $i++; - } - $this->i = $i; - - $this->allColumns['_metadata'] = $someMetadata; - $this->allColumns['_idSubtable'] = $someIdSubTable; - } - - /** - * Computes the output for the table structure array - * - * @return string - */ - protected function renderDataTable() - { - $html = "<table ". ($this->tableId ? "id=\"{$this->tableId}\" " : "") ."border=\"1\">\n<thead>\n\t<tr>\n"; - - foreach($this->allColumns as $name => $toDisplay) - { - if($toDisplay !== false) - { - if($name === 0) - { - $name = 'value'; - } - if($this->translateColumnNames) - { - $name = $this->translateColumnName($name); - } - $html .= "\t\t<th>$name</th>\n"; - } - } - - $html .= "\t</tr>\n</thead>\n<tbody>\n"; - - foreach($this->tableStructure as $row) - { - $html .= "\t<tr>\n"; - foreach($this->allColumns as $name => $toDisplay) - { - if($toDisplay !== false) - { - $value = "-"; - if(isset($row[$name])) - { - if(is_array($row[$name])) - { - $value = "<pre>".self::formatValueXml(var_export($row[$name], true)) . "</pre>"; - } - else - { - $value = self::formatValueXml($row[$name]); - } - } - - $html .= "\t\t<td>$value</td>\n"; - } - } - $html .= "\t</tr>\n"; - } - - $html .= "</tbody>\n</table>\n"; - - return $html; - } + protected $tableId; + protected $allColumns; + protected $tableStructure; + protected $i; + + /** + * Sets the table id + * + * @param string $id + */ + function setTableId($id) + { + $this->tableId = str_replace('.', '_', $id); + } + + /** + * Output HTTP Content-Type header + */ + protected function renderHeader() + { + @header('Content-Type: text/html; charset=utf-8'); + } + + /** + * Computes the dataTable output and returns the string/binary + * + * @return string + */ + function render() + { + $this->renderHeader(); + $this->tableStructure = array(); + $this->allColumns = array(); + $this->i = 0; + + return $this->renderTable($this->table); + } + + /** + * Computes the exception output and returns the string/binary + * + * @return string + */ + function renderException() + { + $this->renderHeader(); + $exceptionMessage = $this->getExceptionMessage(); + return nl2br($exceptionMessage); + } + + /** + * Computes the output for the given data table + * + * @param Piwik_DataTable $table + * @return string + */ + protected function renderTable($table) + { + if (is_array($table)) // convert array to DataTable + { + $table = Piwik_DataTable::makeFromSimpleArray($table); + } + + if ($table instanceof Piwik_DataTable_Array) { + foreach ($table->getArray() as $date => $subtable) { + if ($subtable->getRowsCount()) { + $this->buildTableStructure($subtable, '_' . $table->getKeyName(), $date); + } + } + } else // Piwik_DataTable_Simple + { + if ($table->getRowsCount()) { + $this->buildTableStructure($table); + } + } + + $out = $this->renderDataTable(); + return $out; + } + + /** + * Adds the given data table to the table structure array + * + * @param Piwik_DataTable_Simple $table + * @param null|string $columnToAdd + * @param null|string $valueToAdd + * @throws Exception + */ + protected function buildTableStructure($table, $columnToAdd = null, $valueToAdd = null) + { + $i = $this->i; + $someMetadata = false; + $someIdSubTable = false; + + /* + * table = array + * ROW1 = col1 | col2 | col3 | metadata | idSubTable + * ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable + */ + if (!($table instanceof Piwik_DataTable)) { + throw new Exception("HTML Renderer does not work with this combination of parameters"); + } + foreach ($table->getRows() as $row) { + if (isset($columnToAdd) && isset($valueToAdd)) { + $this->allColumns[$columnToAdd] = true; + $this->tableStructure[$i][$columnToAdd] = $valueToAdd; + } + + foreach ($row->getColumns() as $column => $value) { + $this->allColumns[$column] = true; + $this->tableStructure[$i][$column] = $value; + } + + $metadata = array(); + foreach ($row->getMetadata() as $name => $value) { + if (is_string($value)) $value = "'$value'"; + $metadata[] = "'$name' => $value"; + } + + if (count($metadata) != 0) { + $someMetadata = true; + $metadata = implode("<br />", $metadata); + $this->tableStructure[$i]['_metadata'] = $metadata; + } + + $idSubtable = $row->getIdSubDataTable(); + if (!is_null($idSubtable)) { + $someIdSubTable = true; + $this->tableStructure[$i]['_idSubtable'] = $idSubtable; + } + + $i++; + } + $this->i = $i; + + $this->allColumns['_metadata'] = $someMetadata; + $this->allColumns['_idSubtable'] = $someIdSubTable; + } + + /** + * Computes the output for the table structure array + * + * @return string + */ + protected function renderDataTable() + { + $html = "<table " . ($this->tableId ? "id=\"{$this->tableId}\" " : "") . "border=\"1\">\n<thead>\n\t<tr>\n"; + + foreach ($this->allColumns as $name => $toDisplay) { + if ($toDisplay !== false) { + if ($name === 0) { + $name = 'value'; + } + if ($this->translateColumnNames) { + $name = $this->translateColumnName($name); + } + $html .= "\t\t<th>$name</th>\n"; + } + } + + $html .= "\t</tr>\n</thead>\n<tbody>\n"; + + foreach ($this->tableStructure as $row) { + $html .= "\t<tr>\n"; + foreach ($this->allColumns as $name => $toDisplay) { + if ($toDisplay !== false) { + $value = "-"; + if (isset($row[$name])) { + if (is_array($row[$name])) { + $value = "<pre>" . self::formatValueXml(var_export($row[$name], true)) . "</pre>"; + } else { + $value = self::formatValueXml($row[$name]); + } + } + + $html .= "\t\t<td>$value</td>\n"; + } + } + $html .= "\t</tr>\n"; + } + + $html .= "</tbody>\n</table>\n"; + + return $html; + } } diff --git a/core/DataTable/Renderer/Json.php b/core/DataTable/Renderer/Json.php index 87fb8035f6..286299acd6 100644 --- a/core/DataTable/Renderer/Json.php +++ b/core/DataTable/Renderer/Json.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,109 +12,102 @@ /** * JSON export. * Works with recursive DataTable (when a row can be associated with a subDataTable). - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Renderer_Json extends Piwik_DataTable_Renderer { - /** - * Computes the dataTable output and returns the string/binary - * - * @return string - */ - public function render() - { - $this->renderHeader(); - return $this->renderTable($this->table); - } + /** + * Computes the dataTable output and returns the string/binary + * + * @return string + */ + public function render() + { + $this->renderHeader(); + return $this->renderTable($this->table); + } - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - $this->renderHeader(); - - $exceptionMessage = $this->getExceptionMessage(); - $exceptionMessage = str_replace(array("\r\n","\n"), "", $exceptionMessage); - $exceptionMessage = '{"result":"error", "message":"'.$exceptionMessage.'"}'; - - return $this->jsonpWrap($exceptionMessage); - } + /** + * Computes the exception output and returns the string/binary + * + * @return string + */ + function renderException() + { + $this->renderHeader(); - /** - * Computes the output for the given data table - * - * @param Piwik_DataTable $table - * @return string - */ - protected function renderTable($table) - { - if (is_array($table)) - { - $array = $table; - if (self::shouldWrapArrayBeforeRendering($array, $wrapSingleValues = true)) - { - $array = array($array); - } - } - else - { - $renderer = new Piwik_DataTable_Renderer_Php(); - $renderer->setTable($table); - $renderer->setRenderSubTables($this->isRenderSubtables()); - $renderer->setSerialize(false); - $renderer->setHideIdSubDatableFromResponse($this->hideIdSubDatatable); - $array = $renderer->flatRender(); - } - - if(!is_array($array)) - { - $array = array('value' => $array); - } + $exceptionMessage = $this->getExceptionMessage(); + $exceptionMessage = str_replace(array("\r\n", "\n"), "", $exceptionMessage); + $exceptionMessage = '{"result":"error", "message":"' . $exceptionMessage . '"}'; - // decode all entities - $callback = create_function('&$value,$key', 'if(is_string($value)){$value = html_entity_decode($value, ENT_QUOTES, "UTF-8");}'); - array_walk_recursive($array, $callback); - - $str = Piwik_Common::json_encode($array); - - return $this->jsonpWrap($str); - } + return $this->jsonpWrap($exceptionMessage); + } - /** - * @param $str - * @return string - */ - protected function jsonpWrap($str) - { - if(($jsonCallback = Piwik_Common::getRequestVar('callback', false)) === false) - $jsonCallback = Piwik_Common::getRequestVar('jsoncallback', false); - if($jsonCallback !== false) - { - if(preg_match('/^[0-9a-zA-Z_]*$/D', $jsonCallback) > 0) - { - $str = $jsonCallback . "(" . $str . ")"; - } - } - - return $str; - } + /** + * Computes the output for the given data table + * + * @param Piwik_DataTable $table + * @return string + */ + protected function renderTable($table) + { + if (is_array($table)) { + $array = $table; + if (self::shouldWrapArrayBeforeRendering($array, $wrapSingleValues = true)) { + $array = array($array); + } + } else { + $renderer = new Piwik_DataTable_Renderer_Php(); + $renderer->setTable($table); + $renderer->setRenderSubTables($this->isRenderSubtables()); + $renderer->setSerialize(false); + $renderer->setHideIdSubDatableFromResponse($this->hideIdSubDatatable); + $array = $renderer->flatRender(); + } - /** - * Sends the http header for json file - */ - protected function renderHeader() - { - self::sendHeaderJSON(); - Piwik::overrideCacheControlHeaders(); - } + if (!is_array($array)) { + $array = array('value' => $array); + } - public static function sendHeaderJSON() - { - @header('Content-Type: application/json; charset=utf-8'); - } + // decode all entities + $callback = create_function('&$value,$key', 'if(is_string($value)){$value = html_entity_decode($value, ENT_QUOTES, "UTF-8");}'); + array_walk_recursive($array, $callback); + + $str = Piwik_Common::json_encode($array); + + return $this->jsonpWrap($str); + } + + /** + * @param $str + * @return string + */ + protected function jsonpWrap($str) + { + if (($jsonCallback = Piwik_Common::getRequestVar('callback', false)) === false) + $jsonCallback = Piwik_Common::getRequestVar('jsoncallback', false); + if ($jsonCallback !== false) { + if (preg_match('/^[0-9a-zA-Z_]*$/D', $jsonCallback) > 0) { + $str = $jsonCallback . "(" . $str . ")"; + } + } + + return $str; + } + + /** + * Sends the http header for json file + */ + protected function renderHeader() + { + self::sendHeaderJSON(); + Piwik::overrideCacheControlHeaders(); + } + + public static function sendHeaderJSON() + { + @header('Content-Type: application/json; charset=utf-8'); + } } diff --git a/core/DataTable/Renderer/Php.php b/core/DataTable/Renderer/Php.php index f0570ae3f6..9c671c1201 100644 --- a/core/DataTable/Renderer/Php.php +++ b/core/DataTable/Renderer/Php.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -14,283 +14,255 @@ * You can specify in the constructor if you want the serialized version. * Please note that by default it will produce a flat version of the array. * See the method flatRender() for details. @see flatRender(); - * + * * Works with recursive DataTable (when a row can be associated with a subDataTable). - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Renderer_Php extends Piwik_DataTable_Renderer { - protected $prettyDisplay = false; - protected $serialize = true; + protected $prettyDisplay = false; + protected $serialize = true; - /** - * Enables/Disables serialize - * - * @param bool $bool - */ - public function setSerialize( $bool ) - { - $this->serialize = (bool)$bool; - } + /** + * Enables/Disables serialize + * + * @param bool $bool + */ + public function setSerialize($bool) + { + $this->serialize = (bool)$bool; + } - /** - * Enables/Disables pretty display - * - * @param bool $bool - */ - public function setPrettyDisplay($bool) - { - $this->prettyDisplay = (bool)$bool; - } + /** + * Enables/Disables pretty display + * + * @param bool $bool + */ + public function setPrettyDisplay($bool) + { + $this->prettyDisplay = (bool)$bool; + } - /** - * Converts current data table to string - * - * @return string - */ - public function __toString() - { - $data = $this->render(); - if(!is_string($data)) - { - $data = serialize($data); - } - return $data; - } + /** + * Converts current data table to string + * + * @return string + */ + public function __toString() + { + $data = $this->render(); + if (!is_string($data)) { + $data = serialize($data); + } + return $data; + } - /** - * Computes the dataTable output and returns the string/binary - * - * @param null|Piwik_DataTable_Array|Piwik_DataTable_Simple $dataTable - * @return string - */ - public function render( $dataTable = null ) - { - $this->renderHeader(); + /** + * Computes the dataTable output and returns the string/binary + * + * @param null|Piwik_DataTable_Array|Piwik_DataTable_Simple $dataTable + * @return string + */ + public function render($dataTable = null) + { + $this->renderHeader(); - if(is_null($dataTable)) - { - $dataTable = $this->table; - } - $toReturn = $this->flatRender( $dataTable ); - - if( $this->prettyDisplay ) - { - if(!is_array($toReturn)) - { - $toReturn = unserialize($toReturn); - } - $toReturn = "<pre>" . var_export($toReturn, true ) . "</pre>"; - } - return $toReturn; - } + if (is_null($dataTable)) { + $dataTable = $this->table; + } + $toReturn = $this->flatRender($dataTable); - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - $this->renderHeader(); + if ($this->prettyDisplay) { + if (!is_array($toReturn)) { + $toReturn = unserialize($toReturn); + } + $toReturn = "<pre>" . var_export($toReturn, true) . "</pre>"; + } + return $toReturn; + } - $exceptionMessage = $this->getExceptionMessage(); - - $return = array('result' => 'error', 'message' => $exceptionMessage); - - if($this->serialize) - { - $return = serialize($return); - } - - return $return; - } + /** + * Computes the exception output and returns the string/binary + * + * @return string + */ + function renderException() + { + $this->renderHeader(); - /** - * Produces a flat php array from the DataTable, putting "columns" and "metadata" on the same level. - * - * For example, when a originalRender() would be - * array( 'columns' => array( 'col1_name' => value1, 'col2_name' => value2 ), - * 'metadata' => array( 'metadata1_name' => value_metadata) ) - * - * a flatRender() is - * array( 'col1_name' => value1, - * 'col2_name' => value2, - * 'metadata1_name' => value_metadata ) - * - * @param null|Piwik_DataTable_Array|Piwik_DataTable_Simple $dataTable - * @return array Php array representing the 'flat' version of the datatable - */ - public function flatRender( $dataTable = null ) - { - if(is_null($dataTable)) - { - $dataTable = $this->table; - } - - if (is_array($dataTable)) - { - $flatArray = $dataTable; - if (self::shouldWrapArrayBeforeRendering($flatArray)) - { - $flatArray = array($flatArray); - } - } - else if ($dataTable instanceof Piwik_DataTable_Array) - { - $flatArray = array(); - foreach($dataTable->getArray() as $keyName => $table) - { - $serializeSave = $this->serialize; - $this->serialize = false; - $flatArray[$keyName] = $this->flatRender($table); - $this->serialize = $serializeSave; - } - } - else if($dataTable instanceof Piwik_DataTable_Simple) - { - $flatArray = $this->renderSimpleTable($dataTable); - - // if we return only one numeric value then we print out the result in a simple <result> tag - // keep it simple! - if(count($flatArray) == 1) - { - $flatArray = current($flatArray); - } - - } - // A normal DataTable needs to be handled specifically - else - { - $array = $this->renderTable($dataTable); - $flatArray = $this->flattenArray($array); - } - - if($this->serialize) - { - $flatArray = serialize($flatArray); - } - - return $flatArray; - } + $exceptionMessage = $this->getExceptionMessage(); - /** - * - * @param array $array - * @return array - */ - protected function flattenArray($array) - { - $flatArray = array(); - foreach($array as $row) - { - $newRow = $row['columns'] + $row['metadata']; - if(isset($row['idsubdatatable']) - && $this->hideIdSubDatatable === false) - { - $newRow += array('idsubdatatable' => $row['idsubdatatable']); - } - if(isset($row['subtable'])) - { - $newRow += array('subtable' => $this->flattenArray($row['subtable']) ); - } - $flatArray[] = $newRow; - } - return $flatArray; - } + $return = array('result' => 'error', 'message' => $exceptionMessage); - /** - * Converts the current data table to an array - * - * @return array - * @throws Exception - */ - public function originalRender() - { - Piwik::checkObjectTypeIs($this->table, array('Piwik_DataTable_Simple', 'Piwik_DataTable')); - - if($this->table instanceof Piwik_DataTable_Simple) - { - $array = $this->renderSimpleTable($this->table); - } - elseif($this->table instanceof Piwik_DataTable) - { - $array = $this->renderTable($this->table); - } - - if($this->serialize) - { - $array = serialize($array); - } - return $array; - } + if ($this->serialize) { + $return = serialize($return); + } - /** - * Converts the given data table to an array - * - * @param Piwik_DataTable $table - * @return array - */ - protected function renderTable($table) - { - $array = array(); + return $return; + } - foreach($table->getRows() as $id => $row) - { - $newRow = array( - 'columns' => $row->getColumns(), - 'metadata' => $row->getMetadata(), - 'idsubdatatable' => $row->getIdSubDataTable(), - ); - - if ($id == Piwik_DataTable::ID_SUMMARY_ROW) - { - $newRow['issummaryrow'] = true; - } - - if($this->isRenderSubtables() - && $row->isSubtableLoaded() ) - { - $subTable = $this->renderTable( Piwik_DataTable_Manager::getInstance()->getTable($row->getIdSubDataTable())); - $newRow['subtable'] = $subTable; - if($this->hideIdSubDatatable === false - && isset($newRow['metadata']['idsubdatatable_in_db'])) - { - $newRow['columns']['idsubdatatable'] = $newRow['metadata']['idsubdatatable_in_db']; - } - unset($newRow['metadata']['idsubdatatable_in_db']); - } - if($this->hideIdSubDatatable !== false) - { - unset($newRow['idsubdatatable']); - } - - $array[] = $newRow; - } - return $array; - } + /** + * Produces a flat php array from the DataTable, putting "columns" and "metadata" on the same level. + * + * For example, when a originalRender() would be + * array( 'columns' => array( 'col1_name' => value1, 'col2_name' => value2 ), + * 'metadata' => array( 'metadata1_name' => value_metadata) ) + * + * a flatRender() is + * array( 'col1_name' => value1, + * 'col2_name' => value2, + * 'metadata1_name' => value_metadata ) + * + * @param null|Piwik_DataTable_Array|Piwik_DataTable_Simple $dataTable + * @return array Php array representing the 'flat' version of the datatable + */ + public function flatRender($dataTable = null) + { + if (is_null($dataTable)) { + $dataTable = $this->table; + } - /** - * Converts the simple data table to an array - * - * @param Piwik_DataTable_Simple $table - * @return array - */ - protected function renderSimpleTable($table) - { - $array = array(); + if (is_array($dataTable)) { + $flatArray = $dataTable; + if (self::shouldWrapArrayBeforeRendering($flatArray)) { + $flatArray = array($flatArray); + } + } else if ($dataTable instanceof Piwik_DataTable_Array) { + $flatArray = array(); + foreach ($dataTable->getArray() as $keyName => $table) { + $serializeSave = $this->serialize; + $this->serialize = false; + $flatArray[$keyName] = $this->flatRender($table); + $this->serialize = $serializeSave; + } + } else if ($dataTable instanceof Piwik_DataTable_Simple) { + $flatArray = $this->renderSimpleTable($dataTable); - $row = $table->getFirstRow(); - if($row === false) - { - return $array; - } - foreach($row->getColumns() as $columnName => $columnValue) - { - $array[$columnName] = $columnValue; - } - return $array; - } + // if we return only one numeric value then we print out the result in a simple <result> tag + // keep it simple! + if (count($flatArray) == 1) { + $flatArray = current($flatArray); + } + + } // A normal DataTable needs to be handled specifically + else { + $array = $this->renderTable($dataTable); + $flatArray = $this->flattenArray($array); + } + + if ($this->serialize) { + $flatArray = serialize($flatArray); + } + + return $flatArray; + } + + /** + * + * @param array $array + * @return array + */ + protected function flattenArray($array) + { + $flatArray = array(); + foreach ($array as $row) { + $newRow = $row['columns'] + $row['metadata']; + if (isset($row['idsubdatatable']) + && $this->hideIdSubDatatable === false + ) { + $newRow += array('idsubdatatable' => $row['idsubdatatable']); + } + if (isset($row['subtable'])) { + $newRow += array('subtable' => $this->flattenArray($row['subtable'])); + } + $flatArray[] = $newRow; + } + return $flatArray; + } + + /** + * Converts the current data table to an array + * + * @return array + * @throws Exception + */ + public function originalRender() + { + Piwik::checkObjectTypeIs($this->table, array('Piwik_DataTable_Simple', 'Piwik_DataTable')); + + if ($this->table instanceof Piwik_DataTable_Simple) { + $array = $this->renderSimpleTable($this->table); + } elseif ($this->table instanceof Piwik_DataTable) { + $array = $this->renderTable($this->table); + } + + if ($this->serialize) { + $array = serialize($array); + } + return $array; + } + + /** + * Converts the given data table to an array + * + * @param Piwik_DataTable $table + * @return array + */ + protected function renderTable($table) + { + $array = array(); + + foreach ($table->getRows() as $id => $row) { + $newRow = array( + 'columns' => $row->getColumns(), + 'metadata' => $row->getMetadata(), + 'idsubdatatable' => $row->getIdSubDataTable(), + ); + + if ($id == Piwik_DataTable::ID_SUMMARY_ROW) { + $newRow['issummaryrow'] = true; + } + + if ($this->isRenderSubtables() + && $row->isSubtableLoaded() + ) { + $subTable = $this->renderTable(Piwik_DataTable_Manager::getInstance()->getTable($row->getIdSubDataTable())); + $newRow['subtable'] = $subTable; + if ($this->hideIdSubDatatable === false + && isset($newRow['metadata']['idsubdatatable_in_db']) + ) { + $newRow['columns']['idsubdatatable'] = $newRow['metadata']['idsubdatatable_in_db']; + } + unset($newRow['metadata']['idsubdatatable_in_db']); + } + if ($this->hideIdSubDatatable !== false) { + unset($newRow['idsubdatatable']); + } + + $array[] = $newRow; + } + return $array; + } + + /** + * Converts the simple data table to an array + * + * @param Piwik_DataTable_Simple $table + * @return array + */ + protected function renderSimpleTable($table) + { + $array = array(); + + $row = $table->getFirstRow(); + if ($row === false) { + return $array; + } + foreach ($row->getColumns() as $columnName => $columnValue) { + $array[$columnName] = $columnValue; + } + return $array; + } } diff --git a/core/DataTable/Renderer/Rss.php b/core/DataTable/Renderer/Rss.php index 3bda7f3d8d..60f11ca13e 100644 --- a/core/DataTable/Renderer/Rss.php +++ b/core/DataTable/Renderer/Rss.php @@ -1,127 +1,126 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * RSS Feed. + * RSS Feed. * The RSS renderer can be used only on Piwik_DataTable_Array that are arrays of Piwik_DataTable. * A RSS feed contains one dataTable per element in the Piwik_DataTable_Array. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Renderer_Rss extends Piwik_DataTable_Renderer { - /** - * Computes the dataTable output and returns the string/binary - * - * @return string - */ - function render() - { - $this->renderHeader(); - return $this->renderTable($this->table); - } - - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - header('Content-type: text/plain'); - $exceptionMessage = $this->getExceptionMessage(); - return 'Error: '.$exceptionMessage; - } - - /** - * Computes the output for the given data table - * - * @param Piwik_DataTable $table - * @return string - * @throws Exception - */ - protected function renderTable($table) - { - if(!($table instanceof Piwik_DataTable_Array) - || $table->getKeyName() != 'date') - { - throw new Exception("RSS feeds can be generated for one specific website &idSite=X.". - "\nPlease specify only one idSite or consider using &format=XML instead."); - } - - $idSite = Piwik_Common::getRequestVar('idSite', 1, 'int'); - $period = Piwik_Common::getRequestVar('period'); - - $piwikUrl = Piwik_Url::getCurrentUrlWithoutFileName() - . "?module=CoreHome&action=index&idSite=" . $idSite . "&period=" . $period; - $out = ""; - $moreRecentFirst = array_reverse($table->getArray(), true); - foreach($moreRecentFirst as $date => $subtable ) - { - $timestamp = $subtable->getMetadata('timestamp'); - $site = $subtable->getMetadata('site'); - - $pudDate = date('r', $timestamp); - - $dateInSiteTimezone = Piwik_Date::factory($timestamp)->setTimezone($site->getTimezone())->toString('Y-m-d'); - $thisPiwikUrl = Piwik_Common::sanitizeInputValue($piwikUrl . "&date=$dateInSiteTimezone"); - $siteName = $site->getName(); - $title = $siteName . " on ". $date; - - $out .= "\t<item> + /** + * Computes the dataTable output and returns the string/binary + * + * @return string + */ + function render() + { + $this->renderHeader(); + return $this->renderTable($this->table); + } + + /** + * Computes the exception output and returns the string/binary + * + * @return string + */ + function renderException() + { + header('Content-type: text/plain'); + $exceptionMessage = $this->getExceptionMessage(); + return 'Error: ' . $exceptionMessage; + } + + /** + * Computes the output for the given data table + * + * @param Piwik_DataTable $table + * @return string + * @throws Exception + */ + protected function renderTable($table) + { + if (!($table instanceof Piwik_DataTable_Array) + || $table->getKeyName() != 'date' + ) { + throw new Exception("RSS feeds can be generated for one specific website &idSite=X." . + "\nPlease specify only one idSite or consider using &format=XML instead."); + } + + $idSite = Piwik_Common::getRequestVar('idSite', 1, 'int'); + $period = Piwik_Common::getRequestVar('period'); + + $piwikUrl = Piwik_Url::getCurrentUrlWithoutFileName() + . "?module=CoreHome&action=index&idSite=" . $idSite . "&period=" . $period; + $out = ""; + $moreRecentFirst = array_reverse($table->getArray(), true); + foreach ($moreRecentFirst as $date => $subtable) { + $timestamp = $subtable->getMetadata('timestamp'); + $site = $subtable->getMetadata('site'); + + $pudDate = date('r', $timestamp); + + $dateInSiteTimezone = Piwik_Date::factory($timestamp)->setTimezone($site->getTimezone())->toString('Y-m-d'); + $thisPiwikUrl = Piwik_Common::sanitizeInputValue($piwikUrl . "&date=$dateInSiteTimezone"); + $siteName = $site->getName(); + $title = $siteName . " on " . $date; + + $out .= "\t<item> <pubDate>$pudDate</pubDate> <guid>$thisPiwikUrl</guid> <link>$thisPiwikUrl</link> <title>$title</title> <author>http://piwik.org</author> - <description>"; - - $out .= Piwik_Common::sanitizeInputValue( $this->renderDataTable($subtable) ); - $out .= "</description>\n\t</item>\n"; - } - - $header = $this->getRssHeader(); - $footer = $this->getRssFooter(); - - return $header . $out . $footer; - } - - /** - * Sends the xml file http header - */ - protected function renderHeader() - { - @header('Content-Type: text/xml; charset=utf-8'); - } - - /** - * Returns the RSS file footer - * - * @return string - */ - protected function getRssFooter() - { - return "\t</channel>\n</rss>"; - } - - /** - * Returns the RSS file header - * - * @return string - */ - protected function getRssHeader() - { - $generationDate = date('r'); - $header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> + <description>"; + + $out .= Piwik_Common::sanitizeInputValue($this->renderDataTable($subtable)); + $out .= "</description>\n\t</item>\n"; + } + + $header = $this->getRssHeader(); + $footer = $this->getRssFooter(); + + return $header . $out . $footer; + } + + /** + * Sends the xml file http header + */ + protected function renderHeader() + { + @header('Content-Type: text/xml; charset=utf-8'); + } + + /** + * Returns the RSS file footer + * + * @return string + */ + protected function getRssFooter() + { + return "\t</channel>\n</rss>"; + } + + /** + * Returns the RSS file header + * + * @return string + */ + protected function getRssHeader() + { + $generationDate = date('r'); + $header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?> <rss version=\"2.0\"> <channel> <title>piwik statistics - RSS</title> @@ -131,78 +130,67 @@ class Piwik_DataTable_Renderer_Rss extends Piwik_DataTable_Renderer <generator>piwik</generator> <language>en</language> <lastBuildDate>$generationDate</lastBuildDate>"; - return $header; - } - - protected function renderDataTable($table) - { - if($table->getRowsCount() == 0) - { - return "<b><i>Empty table</i></b><br />\n"; - } - - $i = 1; - $tableStructure = array(); - - /* - * table = array - * ROW1 = col1 | col2 | col3 | metadata | idSubTable - * ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable - * subtable here - */ - $allColumns = array(); - foreach($table->getRows() as $row) - { - foreach($row->getColumns() as $column => $value) - { - // for example, goals data is array: not supported in export RSS - // in the future we shall reuse ViewDataTable for html exports in RSS anyway - if(is_array($value)) - { - continue; - } - $allColumns[$column] = true; - $tableStructure[$i][$column] = $value; - } - $i++; - } - $html = "\n"; - $html .= "<table border=1 width=70%>"; - $html .= "\n<tr>"; - foreach($allColumns as $name => $toDisplay) - { - if($toDisplay !== false) - { - if($this->translateColumnNames) - { - $name = $this->translateColumnName($name); - } - $html .= "\n\t<td><b>$name</b></td>"; - } - } - $html .= "\n</tr>"; - $colspan = count($allColumns); - - foreach($tableStructure as $row) - { - $html .= "\n\n<tr>"; - foreach($allColumns as $columnName => $toDisplay) - { - if($toDisplay !== false) - { - $value = "-"; - if(isset($row[$columnName])) - { - $value = urldecode($row[$columnName]); - } - - $html .= "\n\t<td>$value</td>"; - } - } - $html .= "</tr>"; - - } - $html .= "\n\n</table>"; - return $html; - } + return $header; + } + + protected function renderDataTable($table) + { + if ($table->getRowsCount() == 0) { + return "<b><i>Empty table</i></b><br />\n"; + } + + $i = 1; + $tableStructure = array(); + + /* + * table = array + * ROW1 = col1 | col2 | col3 | metadata | idSubTable + * ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable + * subtable here + */ + $allColumns = array(); + foreach ($table->getRows() as $row) { + foreach ($row->getColumns() as $column => $value) { + // for example, goals data is array: not supported in export RSS + // in the future we shall reuse ViewDataTable for html exports in RSS anyway + if (is_array($value)) { + continue; + } + $allColumns[$column] = true; + $tableStructure[$i][$column] = $value; + } + $i++; + } + $html = "\n"; + $html .= "<table border=1 width=70%>"; + $html .= "\n<tr>"; + foreach ($allColumns as $name => $toDisplay) { + if ($toDisplay !== false) { + if ($this->translateColumnNames) { + $name = $this->translateColumnName($name); + } + $html .= "\n\t<td><b>$name</b></td>"; + } + } + $html .= "\n</tr>"; + $colspan = count($allColumns); + + foreach ($tableStructure as $row) { + $html .= "\n\n<tr>"; + foreach ($allColumns as $columnName => $toDisplay) { + if ($toDisplay !== false) { + $value = "-"; + if (isset($row[$columnName])) { + $value = urldecode($row[$columnName]); + } + + $html .= "\n\t<td>$value</td>"; + } + } + $html .= "</tr>"; + + } + $html .= "\n\n</table>"; + return $html; + } } diff --git a/core/DataTable/Renderer/Tsv.php b/core/DataTable/Renderer/Tsv.php index db0132913b..4835853143 100644 --- a/core/DataTable/Renderer/Tsv.php +++ b/core/DataTable/Renderer/Tsv.php @@ -1,41 +1,41 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @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 { - /** - * Constructor - */ - function __construct() - { - parent::__construct(); - $this->setSeparator("\t"); - } + /** + * Constructor + */ + function __construct() + { + parent::__construct(); + $this->setSeparator("\t"); + } - /** - * Computes the dataTable output and returns the string/binary - * - * @return string - */ - function render() - { - return parent::render(); - } + /** + * Computes the dataTable output and returns the string/binary + * + * @return string + */ + function render() + { + return parent::render(); + } } diff --git a/core/DataTable/Renderer/Xml.php b/core/DataTable/Renderer/Xml.php index 4e60e043f8..e04392bb02 100644 --- a/core/DataTable/Renderer/Xml.php +++ b/core/DataTable/Renderer/Xml.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -13,497 +13,414 @@ * XML export of a given DataTable. * See the tests cases for more information about the XML format (/tests/core/DataTable/Renderer.test.php) * Or have a look at the API calls examples. - * + * * Works with recursive DataTable (when a row can be associated with a subDataTable). - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Renderer_Xml extends Piwik_DataTable_Renderer { - /** - * Computes the dataTable output and returns the string/binary - * - * @return string - */ - function render() - { - $this->renderHeader(); - return '<?xml version="1.0" encoding="utf-8" ?>' . "\n" . $this->renderTable($this->table); - } + /** + * Computes the dataTable output and returns the string/binary + * + * @return string + */ + function render() + { + $this->renderHeader(); + return '<?xml version="1.0" encoding="utf-8" ?>' . "\n" . $this->renderTable($this->table); + } - /** - * Computes the exception output and returns the string/binary - * - * @return string - */ - function renderException() - { - $this->renderHeader(); + /** + * Computes the exception output and returns the string/binary + * + * @return string + */ + function renderException() + { + $this->renderHeader(); - $exceptionMessage = $this->getExceptionMessage(); - - $return = '<?xml version="1.0" encoding="utf-8" ?>' . "\n" . - "<result>\n". - "\t<error message=\"".$exceptionMessage."\" />\n". - "</result>"; - - return $return; - } + $exceptionMessage = $this->getExceptionMessage(); - /** - * Converts the given data table to an array - * - * @param Piwik_DataTable $table data table to convert - * @return array - */ - protected function getArrayFromDataTable($table) - { - if (is_array($table)) - { - return $table; - } - - $renderer = new Piwik_DataTable_Renderer_Php(); - $renderer->setRenderSubTables($this->isRenderSubtables()); - $renderer->setSerialize(false); - $renderer->setTable($table); - $renderer->setHideIdSubDatableFromResponse($this->hideIdSubDatatable); - return $renderer->flatRender(); - } + $return = '<?xml version="1.0" encoding="utf-8" ?>' . "\n" . + "<result>\n" . + "\t<error message=\"" . $exceptionMessage . "\" />\n" . + "</result>"; - /** - * Computes the output for the given data table - * - * @param Piwik_DataTable $table - * @param bool $returnOnlyDataTableXml - * @param string $prefixLines - * @return array|string - * @throws Exception - */ - protected function renderTable($table, $returnOnlyDataTableXml = false, $prefixLines = '') - { - $array = $this->getArrayFromDataTable($table); - if($table instanceof Piwik_DataTable_Array) - { - $out = $this->renderDataTableArray($table, $array, $prefixLines); - - if($returnOnlyDataTableXml) - { - return $out; - } - $out = "<results>\n$out</results>"; - return $out; - } - - // integer value of ZERO is a value we want to display - if($array != 0 && empty($array)) - { - if($returnOnlyDataTableXml) - { - throw new Exception("Illegal state, what xml shall we return?"); - } - $out = "<result />"; - return $out; - } - if($table instanceof Piwik_DataTable_Simple) - { - if(is_array($array)) - { - $out = $this->renderDataTableSimple($array); - } - else - { - $out = $array; - } - if($returnOnlyDataTableXml) - { - return $out; - } - - if(is_array($array)) - { - $out = "<result>\n".$out."</result>"; - } - else - { - $value = self::formatValueXml($out); - if($value === '') - { - $out = "<result />"; - } - else - { - $out = "<result>".$value."</result>"; - } - } - return $out; - } - - if ($table instanceof Piwik_DataTable) - { - $out = $this->renderDataTable($array); - if($returnOnlyDataTableXml) - { - return $out; - } - $out = "<result>\n$out</result>"; - return $out; - } - - if (is_array($array)) - { - $out = $this->renderArray($array, $prefixLines."\t"); - if ($returnOnlyDataTableXml) - { - return $out; - } - return "<result>\n$out</result>"; - } - } - - /** - * Renders an array as XML. - * - * @param array $array The array to render. - * @param string $prefixLines The string to prefix each line in the output. - * @return string - */ - private function renderArray( $array, $prefixLines ) - { - $isAssociativeArray = Piwik::isAssociativeArray($array); - - // check if array contains arrays, and if not wrap the result in an extra <row> element - // (only check if this is the root renderArray call) - // NOTE: this is for backwards compatibility. before, array's were added to a new DataTable. - // if the array had arrays, they were added as multiple rows, otherwise it was treated as - // one row. removing will change API output. - $wrapInRow = $prefixLines === "\t" - && self::shouldWrapArrayBeforeRendering($array, $wrapSingleValues = false, $isAssociativeArray); - - // render the array - $result = ""; - if ($wrapInRow) - { - $result .= "$prefixLines<row>\n"; - $prefixLines .= "\t"; - } - foreach ($array as $key => $value) - { - // based on the type of array & the key, determine how this node will look - if ($isAssociativeArray) - { - if (is_numeric($key)) - { - $prefix = "<row key=\"$key\">"; - $suffix = "</row>"; - $emptyNode = "<row key=\"$key\"/>"; - } - else - { - $prefix = "<$key>"; - $suffix = "</$key>"; - $emptyNode = "<$key />"; - } - } - else - { - $prefix = "<row>"; - $suffix = "</row>"; - $emptyNode = "<row/>"; - } - - // render the array item - if (is_array($value)) - { - $result .= $prefixLines.$prefix."\n"; - $result .= $this->renderArray($value, $prefixLines."\t"); - $result .= $prefixLines.$suffix."\n"; - } - else if ($value instanceof Piwik_DataTable - || $value instanceof Piwik_DataTable_Array) - { - if ($value->getRowsCount() == 0) - { - $result .= $prefixLines.$emptyNode."\n"; - } - else - { - $result .= $prefixLines.$prefix."\n"; - if ($value instanceof Piwik_DataTable_Array) - { - $result .= $this->renderDataTableArray($value, $this->getArrayFromDataTable($value), $prefixLines); - } - else if ($value instanceof Piwik_DataTable_Simple) - { - $result .= $this->renderDataTableSimple($this->getArrayFromDataTable($value), $prefixLines); - } - else - { - $result .= $this->renderDataTable($this->getArrayFromDataTable($value), $prefixLines); - } - $result .= $prefixLines.$suffix."\n"; - } - } - else - { - $xmlValue = self::formatValueXml($value); - if (strlen($xmlValue) != 0) - { - $result .= $prefixLines.$prefix.$xmlValue.$suffix."\n"; - } - else - { - $result .= $prefixLines.$emptyNode."\n"; - } - } - } - if ($wrapInRow) - { - $result .= substr($prefixLines, 0, strlen($prefixLines) - 1)."</row>\n"; - } - return $result; - } + return $return; + } - /** - * Computes the output for the given data table array - * - * @param Piwik_DataTable_Array $table - * @param array $array - * @param string $prefixLines - * @return string - */ - protected function renderDataTableArray($table, $array, $prefixLines = "") - { - // CASE 1 - //array - // 'day1' => string '14' (length=2) - // 'day2' => string '6' (length=1) - $firstTable = current($array); - if(!is_array( $firstTable )) - { - $xml = ''; - $nameDescriptionAttribute = $table->getKeyName(); - foreach($array as $valueAttribute => $value) - { - if(empty($value)) - { - $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\" />\n"; - } - elseif($value instanceof Piwik_DataTable_Array ) - { - $out = $this->renderTable($value, true); - //TODO somehow this code is not tested, cover this case - $xml .= "\t<result $nameDescriptionAttribute=\"$valueAttribute\">\n$out</result>\n"; - } - else - { - $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">".self::formatValueXml($value)."</result>\n"; - } - } - return $xml; - } - - $subTables = $table->getArray(); - $firstTable = current($subTables); - - // CASE 2 - //array - // 'day1' => - // array - // 'nb_uniq_visitors' => string '18' - // 'nb_visits' => string '101' - // 'day2' => - // array - // 'nb_uniq_visitors' => string '28' - // 'nb_visits' => string '11' - if( $firstTable instanceof Piwik_DataTable_Simple) - { - $xml = ''; - $nameDescriptionAttribute = $table->getKeyName(); - foreach($array as $valueAttribute => $dataTableSimple) - { - if(count($dataTableSimple) == 0) - { - $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\" />\n"; - } - else - { - if(is_array($dataTableSimple)) - { - $dataTableSimple = "\n" . $this->renderDataTableSimple($dataTableSimple, $prefixLines . "\t") . $prefixLines . "\t"; - } - $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">".$dataTableSimple. "</result>\n"; - } - } - return $xml; - } - - // CASE 3 - //array - // 'day1' => - // array - // 0 => - // array - // 'label' => string 'phpmyvisites' - // 'nb_uniq_visitors' => int 11 - // 'nb_visits' => int 13 - // 1 => - // array - // 'label' => string 'phpmyvisits' - // 'nb_uniq_visitors' => int 2 - // 'nb_visits' => int 2 - // 'day2' => - // array - // 0 => - // array - // 'label' => string 'piwik' - // 'nb_uniq_visitors' => int 121 - // 'nb_visits' => int 130 - // 1 => - // array - // 'label' => string 'piwik bis' - // 'nb_uniq_visitors' => int 20 - // 'nb_visits' => int 120 - if($firstTable instanceof Piwik_DataTable) - { - $xml = ''; - $nameDescriptionAttribute = $table->getKeyName(); - foreach($array as $keyName => $arrayForSingleDate) - { - $dataTableOut = $this->renderDataTable( $arrayForSingleDate, $prefixLines . "\t" ); - if(empty($dataTableOut)) - { - $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$keyName\" />\n"; - } - else - { - $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$keyName\">\n"; - $xml .= $dataTableOut; - $xml .= $prefixLines . "\t</result>\n"; - } - } - return $xml; - } - - if($firstTable instanceof Piwik_DataTable_Array) - { - $xml = ''; - $tables = $table->getArray(); - $nameDescriptionAttribute = $table->getKeyName(); - foreach( $tables as $valueAttribute => $tableInArray) - { - $out = $this->renderTable($tableInArray, true, $prefixLines . "\t"); - $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">\n".$out.$prefixLines."\t</result>\n"; - - } - return $xml; - } - } + /** + * Converts the given data table to an array + * + * @param Piwik_DataTable $table data table to convert + * @return array + */ + protected function getArrayFromDataTable($table) + { + if (is_array($table)) { + return $table; + } - /** - * Computes the output for the given data array - * - * @param array $array - * @param string $prefixLine - * @return string - */ - protected function renderDataTable( $array, $prefixLine = "" ) - { - $out = ''; - foreach($array as $rowId => $row) - { - if(!is_array($row)) - { - $value = self::formatValueXml($row); - if(strlen($value) == 0) - { - $out .= $prefixLine."\t\t<$rowId />\n"; - } - else - { - $out .= $prefixLine."\t\t<$rowId>".$value."</$rowId>\n"; - } - continue; - } + $renderer = new Piwik_DataTable_Renderer_Php(); + $renderer->setRenderSubTables($this->isRenderSubtables()); + $renderer->setSerialize(false); + $renderer->setTable($table); + $renderer->setHideIdSubDatableFromResponse($this->hideIdSubDatatable); + return $renderer->flatRender(); + } - // Handing case idgoal=7, creating a new array for that one - $rowAttribute = ''; - if(($equalFound = strstr($rowId, '=')) !== false) - { - $rowAttribute = explode('=', $rowId); - $rowAttribute = " " . $rowAttribute[0] . "='" . $rowAttribute[1] . "'"; - } - $out .= $prefixLine."\t<row$rowAttribute>"; - - if(count($row) === 1 - && key($row) === 0) - { - $value = self::formatValueXml(current($row)); - $out .= $prefixLine . $value; - } - else - { - $out .= "\n"; - foreach($row as $name => $value) - { - // handle the recursive dataTable case by XML outputting the recursive table - if(is_array($value)) - { - $value = "\n".$this->renderDataTable($value, $prefixLine."\t\t"); - $value .= $prefixLine."\t\t"; - } - else - { - $value = self::formatValueXml($value); - } - if(strlen($value) == 0) - { - $out .= $prefixLine."\t\t<$name />\n"; - } - else - { - $out .= $prefixLine."\t\t<$name>".$value."</$name>\n"; - } - } - $out .= "\t"; - } - $out .= $prefixLine."</row>\n"; - } - return $out; - } + /** + * Computes the output for the given data table + * + * @param Piwik_DataTable $table + * @param bool $returnOnlyDataTableXml + * @param string $prefixLines + * @return array|string + * @throws Exception + */ + protected function renderTable($table, $returnOnlyDataTableXml = false, $prefixLines = '') + { + $array = $this->getArrayFromDataTable($table); + if ($table instanceof Piwik_DataTable_Array) { + $out = $this->renderDataTableArray($table, $array, $prefixLines); - /** - * Computes the output for the given data array (representing a simple data table) - * - * @param $array - * @param string $prefixLine - * @return string - */ - protected function renderDataTableSimple( $array, $prefixLine = "") - { - $out = ''; - foreach($array as $keyName => $value) - { - $xmlValue = self::formatValueXml($value); - if(strlen($xmlValue) == 0) - { - $out .= $prefixLine."\t<$keyName />\n"; - } - else - { - $out .= $prefixLine."\t<$keyName>".$xmlValue."</$keyName>\n"; - } - } - return $out; - } + if ($returnOnlyDataTableXml) { + return $out; + } + $out = "<results>\n$out</results>"; + return $out; + } - /** - * Sends the XML headers - */ - protected function renderHeader() - { - // silent fail because otherwise it throws an exception in the unit tests - @header('Content-Type: text/xml; charset=utf-8'); - } + // integer value of ZERO is a value we want to display + if ($array != 0 && empty($array)) { + if ($returnOnlyDataTableXml) { + throw new Exception("Illegal state, what xml shall we return?"); + } + $out = "<result />"; + return $out; + } + if ($table instanceof Piwik_DataTable_Simple) { + if (is_array($array)) { + $out = $this->renderDataTableSimple($array); + } else { + $out = $array; + } + if ($returnOnlyDataTableXml) { + return $out; + } + + if (is_array($array)) { + $out = "<result>\n" . $out . "</result>"; + } else { + $value = self::formatValueXml($out); + if ($value === '') { + $out = "<result />"; + } else { + $out = "<result>" . $value . "</result>"; + } + } + return $out; + } + + if ($table instanceof Piwik_DataTable) { + $out = $this->renderDataTable($array); + if ($returnOnlyDataTableXml) { + return $out; + } + $out = "<result>\n$out</result>"; + return $out; + } + + if (is_array($array)) { + $out = $this->renderArray($array, $prefixLines . "\t"); + if ($returnOnlyDataTableXml) { + return $out; + } + return "<result>\n$out</result>"; + } + } + + /** + * Renders an array as XML. + * + * @param array $array The array to render. + * @param string $prefixLines The string to prefix each line in the output. + * @return string + */ + private function renderArray($array, $prefixLines) + { + $isAssociativeArray = Piwik::isAssociativeArray($array); + + // check if array contains arrays, and if not wrap the result in an extra <row> element + // (only check if this is the root renderArray call) + // NOTE: this is for backwards compatibility. before, array's were added to a new DataTable. + // if the array had arrays, they were added as multiple rows, otherwise it was treated as + // one row. removing will change API output. + $wrapInRow = $prefixLines === "\t" + && self::shouldWrapArrayBeforeRendering($array, $wrapSingleValues = false, $isAssociativeArray); + + // render the array + $result = ""; + if ($wrapInRow) { + $result .= "$prefixLines<row>\n"; + $prefixLines .= "\t"; + } + foreach ($array as $key => $value) { + // based on the type of array & the key, determine how this node will look + if ($isAssociativeArray) { + if (is_numeric($key)) { + $prefix = "<row key=\"$key\">"; + $suffix = "</row>"; + $emptyNode = "<row key=\"$key\"/>"; + } else { + $prefix = "<$key>"; + $suffix = "</$key>"; + $emptyNode = "<$key />"; + } + } else { + $prefix = "<row>"; + $suffix = "</row>"; + $emptyNode = "<row/>"; + } + + // render the array item + if (is_array($value)) { + $result .= $prefixLines . $prefix . "\n"; + $result .= $this->renderArray($value, $prefixLines . "\t"); + $result .= $prefixLines . $suffix . "\n"; + } else if ($value instanceof Piwik_DataTable + || $value instanceof Piwik_DataTable_Array + ) { + if ($value->getRowsCount() == 0) { + $result .= $prefixLines . $emptyNode . "\n"; + } else { + $result .= $prefixLines . $prefix . "\n"; + if ($value instanceof Piwik_DataTable_Array) { + $result .= $this->renderDataTableArray($value, $this->getArrayFromDataTable($value), $prefixLines); + } else if ($value instanceof Piwik_DataTable_Simple) { + $result .= $this->renderDataTableSimple($this->getArrayFromDataTable($value), $prefixLines); + } else { + $result .= $this->renderDataTable($this->getArrayFromDataTable($value), $prefixLines); + } + $result .= $prefixLines . $suffix . "\n"; + } + } else { + $xmlValue = self::formatValueXml($value); + if (strlen($xmlValue) != 0) { + $result .= $prefixLines . $prefix . $xmlValue . $suffix . "\n"; + } else { + $result .= $prefixLines . $emptyNode . "\n"; + } + } + } + if ($wrapInRow) { + $result .= substr($prefixLines, 0, strlen($prefixLines) - 1) . "</row>\n"; + } + return $result; + } + + /** + * Computes the output for the given data table array + * + * @param Piwik_DataTable_Array $table + * @param array $array + * @param string $prefixLines + * @return string + */ + protected function renderDataTableArray($table, $array, $prefixLines = "") + { + // CASE 1 + //array + // 'day1' => string '14' (length=2) + // 'day2' => string '6' (length=1) + $firstTable = current($array); + if (!is_array($firstTable)) { + $xml = ''; + $nameDescriptionAttribute = $table->getKeyName(); + foreach ($array as $valueAttribute => $value) { + if (empty($value)) { + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\" />\n"; + } elseif ($value instanceof Piwik_DataTable_Array) { + $out = $this->renderTable($value, true); + //TODO somehow this code is not tested, cover this case + $xml .= "\t<result $nameDescriptionAttribute=\"$valueAttribute\">\n$out</result>\n"; + } else { + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">" . self::formatValueXml($value) . "</result>\n"; + } + } + return $xml; + } + + $subTables = $table->getArray(); + $firstTable = current($subTables); + + // CASE 2 + //array + // 'day1' => + // array + // 'nb_uniq_visitors' => string '18' + // 'nb_visits' => string '101' + // 'day2' => + // array + // 'nb_uniq_visitors' => string '28' + // 'nb_visits' => string '11' + if ($firstTable instanceof Piwik_DataTable_Simple) { + $xml = ''; + $nameDescriptionAttribute = $table->getKeyName(); + foreach ($array as $valueAttribute => $dataTableSimple) { + if (count($dataTableSimple) == 0) { + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\" />\n"; + } else { + if (is_array($dataTableSimple)) { + $dataTableSimple = "\n" . $this->renderDataTableSimple($dataTableSimple, $prefixLines . "\t") . $prefixLines . "\t"; + } + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">" . $dataTableSimple . "</result>\n"; + } + } + return $xml; + } + + // CASE 3 + //array + // 'day1' => + // array + // 0 => + // array + // 'label' => string 'phpmyvisites' + // 'nb_uniq_visitors' => int 11 + // 'nb_visits' => int 13 + // 1 => + // array + // 'label' => string 'phpmyvisits' + // 'nb_uniq_visitors' => int 2 + // 'nb_visits' => int 2 + // 'day2' => + // array + // 0 => + // array + // 'label' => string 'piwik' + // 'nb_uniq_visitors' => int 121 + // 'nb_visits' => int 130 + // 1 => + // array + // 'label' => string 'piwik bis' + // 'nb_uniq_visitors' => int 20 + // 'nb_visits' => int 120 + if ($firstTable instanceof Piwik_DataTable) { + $xml = ''; + $nameDescriptionAttribute = $table->getKeyName(); + foreach ($array as $keyName => $arrayForSingleDate) { + $dataTableOut = $this->renderDataTable($arrayForSingleDate, $prefixLines . "\t"); + if (empty($dataTableOut)) { + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$keyName\" />\n"; + } else { + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$keyName\">\n"; + $xml .= $dataTableOut; + $xml .= $prefixLines . "\t</result>\n"; + } + } + return $xml; + } + + if ($firstTable instanceof Piwik_DataTable_Array) { + $xml = ''; + $tables = $table->getArray(); + $nameDescriptionAttribute = $table->getKeyName(); + foreach ($tables as $valueAttribute => $tableInArray) { + $out = $this->renderTable($tableInArray, true, $prefixLines . "\t"); + $xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">\n" . $out . $prefixLines . "\t</result>\n"; + + } + return $xml; + } + } + + /** + * Computes the output for the given data array + * + * @param array $array + * @param string $prefixLine + * @return string + */ + protected function renderDataTable($array, $prefixLine = "") + { + $out = ''; + foreach ($array as $rowId => $row) { + if (!is_array($row)) { + $value = self::formatValueXml($row); + if (strlen($value) == 0) { + $out .= $prefixLine . "\t\t<$rowId />\n"; + } else { + $out .= $prefixLine . "\t\t<$rowId>" . $value . "</$rowId>\n"; + } + continue; + } + + // Handing case idgoal=7, creating a new array for that one + $rowAttribute = ''; + if (($equalFound = strstr($rowId, '=')) !== false) { + $rowAttribute = explode('=', $rowId); + $rowAttribute = " " . $rowAttribute[0] . "='" . $rowAttribute[1] . "'"; + } + $out .= $prefixLine . "\t<row$rowAttribute>"; + + if (count($row) === 1 + && key($row) === 0 + ) { + $value = self::formatValueXml(current($row)); + $out .= $prefixLine . $value; + } else { + $out .= "\n"; + foreach ($row as $name => $value) { + // handle the recursive dataTable case by XML outputting the recursive table + if (is_array($value)) { + $value = "\n" . $this->renderDataTable($value, $prefixLine . "\t\t"); + $value .= $prefixLine . "\t\t"; + } else { + $value = self::formatValueXml($value); + } + if (strlen($value) == 0) { + $out .= $prefixLine . "\t\t<$name />\n"; + } else { + $out .= $prefixLine . "\t\t<$name>" . $value . "</$name>\n"; + } + } + $out .= "\t"; + } + $out .= $prefixLine . "</row>\n"; + } + return $out; + } + + /** + * Computes the output for the given data array (representing a simple data table) + * + * @param $array + * @param string $prefixLine + * @return string + */ + protected function renderDataTableSimple($array, $prefixLine = "") + { + $out = ''; + foreach ($array as $keyName => $value) { + $xmlValue = self::formatValueXml($value); + if (strlen($xmlValue) == 0) { + $out .= $prefixLine . "\t<$keyName />\n"; + } else { + $out .= $prefixLine . "\t<$keyName>" . $xmlValue . "</$keyName>\n"; + } + } + return $out; + } + + /** + * Sends the XML headers + */ + protected function renderHeader() + { + // silent fail because otherwise it throws an exception in the unit tests + @header('Content-Type: text/xml; charset=utf-8'); + } } diff --git a/core/DataTable/Row.php b/core/DataTable/Row.php index a0682541c9..8fb60b0f29 100644 --- a/core/DataTable/Row.php +++ b/core/DataTable/Row.php @@ -1,662 +1,619 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * A DataTable is composed of rows. - * + * * A row is composed of: - * - columns often at least a 'label' column containing the description - * of the row, and some numeric values ('nb_visits', etc.) + * - columns often at least a 'label' column containing the description + * of the row, and some numeric values ('nb_visits', etc.) * - metadata: other information never to be shown as 'columns' * - idSubtable: a row can be linked to a SubTable - * + * * IMPORTANT: Make sure that the column named 'label' contains at least one non-numeric character. * Otherwise the method addDataTable() or sumRow() would fail because they would consider * the 'label' as being a numeric column to sum. - * - * PERFORMANCE: Do *not* add new fields except if necessary in this object. New fields will be + * + * PERFORMANCE: Do *not* add new fields except if necessary in this object. New fields will be * serialized and recorded in the DB millions of times. This object size is critical and must be under control. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Row { - /** - * List of columns that cannot be summed. An associative array for speed. - * - * @var array - */ - private static $unsummableColumns = array( - 'label' => true, - 'full_url' => true // column used w/ old Piwik versions - ); - - /** - * This array contains the row information: - * - array indexed by self::COLUMNS contains the columns, pairs of (column names, value) - * - (optional) array indexed by self::METADATA contains the metadata, pairs of (metadata name, value) - * - (optional) integer indexed by self::DATATABLE_ASSOCIATED contains the ID of the Piwik_DataTable associated to this row. - * This ID can be used to read the DataTable from the DataTable_Manager. - * - * @var array - * @see constructor for more information - */ - public $c = array(); - private $subtableIdWasNegativeBeforeSerialize = false; - - // @see sumRow - implementation detail - public $maxVisitsSummed = 0; - - const COLUMNS = 0; - const METADATA = 1; - const DATATABLE_ASSOCIATED = 3; - - - /** - * Efficient load of the Row structure from a well structured php array - * - * @param array $row The row array has the structure - * array( - * Piwik_DataTable_Row::COLUMNS => array( - * 'label' => 'Piwik', - * 'column1' => 42, - * 'visits' => 657, - * 'time_spent' => 155744, - * ), - * Piwik_DataTable_Row::METADATA => array( - * 'logo' => 'test.png' - * ), - * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #Piwik_DataTable object - * (but in the row only the ID will be stored) - * ) - */ - public function __construct( $row = array() ) - { - $this->c[self::COLUMNS] = array(); - $this->c[self::METADATA] = array(); - $this->c[self::DATATABLE_ASSOCIATED] = null; - - if(isset($row[self::COLUMNS])) - { - $this->c[self::COLUMNS] = $row[self::COLUMNS]; - } - if(isset($row[self::METADATA])) - { - $this->c[self::METADATA] = $row[self::METADATA]; - } - if(isset($row[self::DATATABLE_ASSOCIATED]) - && $row[self::DATATABLE_ASSOCIATED] instanceof Piwik_DataTable) - { - $this->setSubtable($row[self::DATATABLE_ASSOCIATED]); - } - } - - /** - * Because $this->c[self::DATATABLE_ASSOCIATED] is negative when the table is in memory, - * we must prior to serialize() call, make sure the ID is saved as positive integer - * - * Only serialize the "c" member - */ - public function __sleep() - { - if(!empty($this->c[self::DATATABLE_ASSOCIATED]) - && $this->c[self::DATATABLE_ASSOCIATED] < 0) - { - $this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED]; - $this->subtableIdWasNegativeBeforeSerialize = true; - } - return array('c'); - } - - /** - * Must be called after the row was serialized and __sleep was called - * - */ - public function cleanPostSerialize() - { - if($this->subtableIdWasNegativeBeforeSerialize) - { - $this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED]; - $this->subtableIdWasNegativeBeforeSerialize = false; - } - } - - /** - * When destroyed, a row destroys its associated subTable if there is one - */ - public function __destruct() - { - if($this->isSubtableLoaded()) - { - Piwik_DataTable_Manager::getInstance()->deleteTable( $this->getIdSubDataTable() ); - $this->c[self::DATATABLE_ASSOCIATED] = null; - } - } - - /** - * Applies a basic rendering to the Row and returns the output - * - * @return string characterizing the row. Example: - 1 ['label' => 'piwik', 'nb_uniq_visitors' => 1685, 'nb_visits' => 1861, 'nb_actions' => 2271, 'max_actions' => 13, 'sum_visit_length' => 920131, 'bounce_count' => 1599] [] [idsubtable = 1375] - */ - public function __toString() - { - $columns = array(); - foreach($this->getColumns() as $column => $value) - { - if(is_string($value)) $value = "'$value'"; - elseif(is_array($value)) $value = var_export($value, true); - $columns[] = "'$column' => $value"; - } - $columns = implode(", ", $columns); - $metadata = array(); - foreach($this->getMetadata() as $name => $value) - { - if(is_string($value)) $value = "'$value'"; - elseif(is_array($value)) $value = var_export($value, true); - $metadata[] = "'$name' => $value"; - } - $metadata = implode(", ", $metadata); - $output = "# [".$columns."] [".$metadata."] [idsubtable = " . $this->getIdSubDataTable()."]<br />\n"; - return $output; - } - - /** - * Deletes the given column - * - * @param string $name Column name - * @return bool True on success, false if the column didn't exist - */ - public function deleteColumn( $name ) - { - if(!isset($this->c[self::COLUMNS][$name])) - { - return false; - } - unset($this->c[self::COLUMNS][$name]); - return true; - } - - /** - * Renames the given column - * - * @param string $oldName - * @param string $newName - */ - public function renameColumn($oldName, $newName) - { - if(isset($this->c[self::COLUMNS][$oldName])) - { - $this->c[self::COLUMNS][$newName] = $this->c[self::COLUMNS][$oldName]; - } - // outside the if() since we want to delete nulled columns - unset($this->c[self::COLUMNS][$oldName]); - } - - /** - * Returns the given column - * - * @param string $name Column name - * @return mixed|false The column value - */ - public function getColumn( $name ) - { - if(!isset($this->c[self::COLUMNS][$name])) - { - return false; - } - return $this->c[self::COLUMNS][$name]; - } - - /** - * Returns the array of all metadata, - * or the specified metadata - * - * @param string $name Metadata name - * @return mixed|array|false - */ - public function getMetadata( $name = null ) - { - if(is_null($name)) - { - return $this->c[self::METADATA]; - } - if(!isset($this->c[self::METADATA][$name])) - { - return false; - } - return $this->c[self::METADATA][$name]; - } - - /** - * Returns the array containing all the columns - * - * @return array Example: array( - * 'column1' => VALUE, - * 'label' => 'www.php.net' - * 'nb_visits' => 15894, - * ) - */ - public function getColumns() - { - return $this->c[self::COLUMNS]; - } - - /** - * Returns the ID of the subDataTable. - * If there is no such a table, returns null. - * - * @return int|null - */ - public function getIdSubDataTable() - { - return !is_null($this->c[self::DATATABLE_ASSOCIATED]) - // abs() is to ensure we return a positive int, @see isSubtableLoaded() - ? abs($this->c[self::DATATABLE_ASSOCIATED]) - : null; - } - - /** - * Returns the associated subtable, if one exists. - * - * @return Piwik_DataTable|false - */ - public function getSubtable() - { - if ($this->isSubtableLoaded()) - { - return Piwik_DataTable_Manager::getInstance()->getTable($this->getIdSubDataTable()); - } - return false; - } - - /** - * Sums a DataTable to this row subDataTable. - * If this row doesn't have a SubDataTable yet, we create a new one. - * Then we add the values of the given DataTable to this row's DataTable. - * - * @param Piwik_DataTable $subTable Table to sum to this row's subDatatable - * @see Piwik_DataTable::addDataTable() for the algorithm used for the sum - */ - public function sumSubtable(Piwik_DataTable $subTable) - { - if($this->isSubtableLoaded()) - { - $thisSubTable = $this->getSubtable(); - } - else - { - $thisSubTable = new Piwik_DataTable(); - $this->addSubtable($thisSubTable); - } - $thisSubTable->addDataTable($subTable); - } - - - /** - * Set a DataTable to be associated to this row. - * If the row already has a DataTable associated to it, throws an Exception. - * - * @param Piwik_DataTable $subTable DataTable to associate to this row - * @return Piwik_DataTable Returns $subTable. - * @throws Exception - */ - public function addSubtable(Piwik_DataTable $subTable) - { - if(!is_null($this->c[self::DATATABLE_ASSOCIATED])) - { - throw new Exception("Adding a subtable to the row, but it already has a subtable associated."); - } - return $this->setSubtable($subTable); - } - - /** - * Set a DataTable to this row. If there is already - * a DataTable associated, it is simply overwritten. - * - * @param Piwik_DataTable $subTable DataTable to associate to this row - * @return Piwik_DataTable Returns $subTable. - */ - public function setSubtable(Piwik_DataTable $subTable) - { - // Hacking -1 to ensure value is negative, so we know the table was loaded - // @see isSubtableLoaded() - $this->c[self::DATATABLE_ASSOCIATED] = -1 * $subTable->getId(); - return $subTable; - } - - /** - * Returns true if the subtable is currently loaded in memory via DataTable_Manager - * - * - * @return bool - */ - public function isSubtableLoaded() - { - // self::DATATABLE_ASSOCIATED are set as negative values, - // as a flag to signify that the subtable is loaded in memory - return !is_null($this->c[self::DATATABLE_ASSOCIATED]) - && $this->c[self::DATATABLE_ASSOCIATED] < 0; - } - - /** - * Remove the sub table reference - */ - public function removeSubtable() - { - $this->c[self::DATATABLE_ASSOCIATED] = null; - } - - /** - * Set all the columns at once. Overwrites previously set columns. - * - * @param array array( - * 'label' => 'www.php.net' - * 'nb_visits' => 15894, - * ) - */ - public function setColumns( $columns ) - { - $this->c[self::COLUMNS] = $columns; - } - - /** - * Set the value $value to the column called $name. - * - * @param string $name name of the column to set - * @param mixed $value value of the column to set - */ - public function setColumn($name, $value) - { - $this->c[self::COLUMNS][$name] = $value; - } - - /** - * Set the value $value to the metadata called $name. - * - * @param string $name name of the metadata to set - * @param mixed $value value of the metadata to set - */ - public function setMetadata($name, $value) - { - $this->c[self::METADATA][$name] = $value; - } - - /** - * Deletes the given metadata - * - * @param bool|string $name Meta column name (omit to delete entire metadata) - * @return bool True on success, false if the column didn't exist - */ - public function deleteMetadata($name = false) - { - if($name === false) - { - $this->c[self::METADATA] = array(); - return true; - } - if(!isset($this->c[self::METADATA][$name])) - { - return false; - } - unset($this->c[self::METADATA][$name]); - return true; - } - - /** - * Add a new column to the row. If the column already exists, throws an exception - * - * @param string $name name of the column to add - * @param mixed $value value of the column to set - * @throws Exception - */ - public function addColumn($name, $value) - { - if(isset($this->c[self::COLUMNS][$name])) - { + /** + * List of columns that cannot be summed. An associative array for speed. + * + * @var array + */ + private static $unsummableColumns = array( + 'label' => true, + 'full_url' => true // column used w/ old Piwik versions + ); + + /** + * This array contains the row information: + * - array indexed by self::COLUMNS contains the columns, pairs of (column names, value) + * - (optional) array indexed by self::METADATA contains the metadata, pairs of (metadata name, value) + * - (optional) integer indexed by self::DATATABLE_ASSOCIATED contains the ID of the Piwik_DataTable associated to this row. + * This ID can be used to read the DataTable from the DataTable_Manager. + * + * @var array + * @see constructor for more information + */ + public $c = array(); + private $subtableIdWasNegativeBeforeSerialize = false; + + // @see sumRow - implementation detail + public $maxVisitsSummed = 0; + + const COLUMNS = 0; + const METADATA = 1; + const DATATABLE_ASSOCIATED = 3; + + + /** + * Efficient load of the Row structure from a well structured php array + * + * @param array $row The row array has the structure + * array( + * Piwik_DataTable_Row::COLUMNS => array( + * 'label' => 'Piwik', + * 'column1' => 42, + * 'visits' => 657, + * 'time_spent' => 155744, + * ), + * Piwik_DataTable_Row::METADATA => array( + * 'logo' => 'test.png' + * ), + * Piwik_DataTable_Row::DATATABLE_ASSOCIATED => #Piwik_DataTable object + * (but in the row only the ID will be stored) + * ) + */ + public function __construct($row = array()) + { + $this->c[self::COLUMNS] = array(); + $this->c[self::METADATA] = array(); + $this->c[self::DATATABLE_ASSOCIATED] = null; + + if (isset($row[self::COLUMNS])) { + $this->c[self::COLUMNS] = $row[self::COLUMNS]; + } + if (isset($row[self::METADATA])) { + $this->c[self::METADATA] = $row[self::METADATA]; + } + if (isset($row[self::DATATABLE_ASSOCIATED]) + && $row[self::DATATABLE_ASSOCIATED] instanceof Piwik_DataTable + ) { + $this->setSubtable($row[self::DATATABLE_ASSOCIATED]); + } + } + + /** + * Because $this->c[self::DATATABLE_ASSOCIATED] is negative when the table is in memory, + * we must prior to serialize() call, make sure the ID is saved as positive integer + * + * Only serialize the "c" member + */ + public function __sleep() + { + if (!empty($this->c[self::DATATABLE_ASSOCIATED]) + && $this->c[self::DATATABLE_ASSOCIATED] < 0 + ) { + $this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED]; + $this->subtableIdWasNegativeBeforeSerialize = true; + } + return array('c'); + } + + /** + * Must be called after the row was serialized and __sleep was called + * + */ + public function cleanPostSerialize() + { + if ($this->subtableIdWasNegativeBeforeSerialize) { + $this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED]; + $this->subtableIdWasNegativeBeforeSerialize = false; + } + } + + /** + * When destroyed, a row destroys its associated subTable if there is one + */ + public function __destruct() + { + if ($this->isSubtableLoaded()) { + Piwik_DataTable_Manager::getInstance()->deleteTable($this->getIdSubDataTable()); + $this->c[self::DATATABLE_ASSOCIATED] = null; + } + } + + /** + * Applies a basic rendering to the Row and returns the output + * + * @return string characterizing the row. Example: - 1 ['label' => 'piwik', 'nb_uniq_visitors' => 1685, 'nb_visits' => 1861, 'nb_actions' => 2271, 'max_actions' => 13, 'sum_visit_length' => 920131, 'bounce_count' => 1599] [] [idsubtable = 1375] + */ + public function __toString() + { + $columns = array(); + foreach ($this->getColumns() as $column => $value) { + if (is_string($value)) $value = "'$value'"; + elseif (is_array($value)) $value = var_export($value, true); + $columns[] = "'$column' => $value"; + } + $columns = implode(", ", $columns); + $metadata = array(); + foreach ($this->getMetadata() as $name => $value) { + if (is_string($value)) $value = "'$value'"; + elseif (is_array($value)) $value = var_export($value, true); + $metadata[] = "'$name' => $value"; + } + $metadata = implode(", ", $metadata); + $output = "# [" . $columns . "] [" . $metadata . "] [idsubtable = " . $this->getIdSubDataTable() . "]<br />\n"; + return $output; + } + + /** + * Deletes the given column + * + * @param string $name Column name + * @return bool True on success, false if the column didn't exist + */ + public function deleteColumn($name) + { + if (!isset($this->c[self::COLUMNS][$name])) { + return false; + } + unset($this->c[self::COLUMNS][$name]); + return true; + } + + /** + * Renames the given column + * + * @param string $oldName + * @param string $newName + */ + public function renameColumn($oldName, $newName) + { + if (isset($this->c[self::COLUMNS][$oldName])) { + $this->c[self::COLUMNS][$newName] = $this->c[self::COLUMNS][$oldName]; + } + // outside the if() since we want to delete nulled columns + unset($this->c[self::COLUMNS][$oldName]); + } + + /** + * Returns the given column + * + * @param string $name Column name + * @return mixed|false The column value + */ + public function getColumn($name) + { + if (!isset($this->c[self::COLUMNS][$name])) { + return false; + } + return $this->c[self::COLUMNS][$name]; + } + + /** + * Returns the array of all metadata, + * or the specified metadata + * + * @param string $name Metadata name + * @return mixed|array|false + */ + public function getMetadata($name = null) + { + if (is_null($name)) { + return $this->c[self::METADATA]; + } + if (!isset($this->c[self::METADATA][$name])) { + return false; + } + return $this->c[self::METADATA][$name]; + } + + /** + * Returns the array containing all the columns + * + * @return array Example: array( + * 'column1' => VALUE, + * 'label' => 'www.php.net' + * 'nb_visits' => 15894, + * ) + */ + public function getColumns() + { + return $this->c[self::COLUMNS]; + } + + /** + * Returns the ID of the subDataTable. + * If there is no such a table, returns null. + * + * @return int|null + */ + public function getIdSubDataTable() + { + return !is_null($this->c[self::DATATABLE_ASSOCIATED]) + // abs() is to ensure we return a positive int, @see isSubtableLoaded() + ? abs($this->c[self::DATATABLE_ASSOCIATED]) + : null; + } + + /** + * Returns the associated subtable, if one exists. + * + * @return Piwik_DataTable|false + */ + public function getSubtable() + { + if ($this->isSubtableLoaded()) { + return Piwik_DataTable_Manager::getInstance()->getTable($this->getIdSubDataTable()); + } + return false; + } + + /** + * Sums a DataTable to this row subDataTable. + * If this row doesn't have a SubDataTable yet, we create a new one. + * Then we add the values of the given DataTable to this row's DataTable. + * + * @param Piwik_DataTable $subTable Table to sum to this row's subDatatable + * @see Piwik_DataTable::addDataTable() for the algorithm used for the sum + */ + public function sumSubtable(Piwik_DataTable $subTable) + { + if ($this->isSubtableLoaded()) { + $thisSubTable = $this->getSubtable(); + } else { + $thisSubTable = new Piwik_DataTable(); + $this->addSubtable($thisSubTable); + } + $thisSubTable->addDataTable($subTable); + } + + + /** + * Set a DataTable to be associated to this row. + * If the row already has a DataTable associated to it, throws an Exception. + * + * @param Piwik_DataTable $subTable DataTable to associate to this row + * @return Piwik_DataTable Returns $subTable. + * @throws Exception + */ + public function addSubtable(Piwik_DataTable $subTable) + { + if (!is_null($this->c[self::DATATABLE_ASSOCIATED])) { + throw new Exception("Adding a subtable to the row, but it already has a subtable associated."); + } + return $this->setSubtable($subTable); + } + + /** + * Set a DataTable to this row. If there is already + * a DataTable associated, it is simply overwritten. + * + * @param Piwik_DataTable $subTable DataTable to associate to this row + * @return Piwik_DataTable Returns $subTable. + */ + public function setSubtable(Piwik_DataTable $subTable) + { + // Hacking -1 to ensure value is negative, so we know the table was loaded + // @see isSubtableLoaded() + $this->c[self::DATATABLE_ASSOCIATED] = -1 * $subTable->getId(); + return $subTable; + } + + /** + * Returns true if the subtable is currently loaded in memory via DataTable_Manager + * + * + * @return bool + */ + public function isSubtableLoaded() + { + // self::DATATABLE_ASSOCIATED are set as negative values, + // as a flag to signify that the subtable is loaded in memory + return !is_null($this->c[self::DATATABLE_ASSOCIATED]) + && $this->c[self::DATATABLE_ASSOCIATED] < 0; + } + + /** + * Remove the sub table reference + */ + public function removeSubtable() + { + $this->c[self::DATATABLE_ASSOCIATED] = null; + } + + /** + * Set all the columns at once. Overwrites previously set columns. + * + * @param array array( + * 'label' => 'www.php.net' + * 'nb_visits' => 15894, + * ) + */ + public function setColumns($columns) + { + $this->c[self::COLUMNS] = $columns; + } + + /** + * Set the value $value to the column called $name. + * + * @param string $name name of the column to set + * @param mixed $value value of the column to set + */ + public function setColumn($name, $value) + { + $this->c[self::COLUMNS][$name] = $value; + } + + /** + * Set the value $value to the metadata called $name. + * + * @param string $name name of the metadata to set + * @param mixed $value value of the metadata to set + */ + public function setMetadata($name, $value) + { + $this->c[self::METADATA][$name] = $value; + } + + /** + * Deletes the given metadata + * + * @param bool|string $name Meta column name (omit to delete entire metadata) + * @return bool True on success, false if the column didn't exist + */ + public function deleteMetadata($name = false) + { + if ($name === false) { + $this->c[self::METADATA] = array(); + return true; + } + if (!isset($this->c[self::METADATA][$name])) { + return false; + } + unset($this->c[self::METADATA][$name]); + return true; + } + + /** + * Add a new column to the row. If the column already exists, throws an exception + * + * @param string $name name of the column to add + * @param mixed $value value of the column to set + * @throws Exception + */ + public function addColumn($name, $value) + { + if (isset($this->c[self::COLUMNS][$name])) { // debug_print_backtrace(); - throw new Exception("Column $name already in the array!"); - } - $this->c[self::COLUMNS][$name] = $value; - } - - /** - * Add columns to the row - * - * @param array $columns Name/Value pairs, e.g., array( name => value , ...) - * @return void - */ - public function addColumns($columns) - { - foreach($columns as $name => $value) - { - try { - $this->addColumn($name, $value); - } catch(Exception $e) { - } - } - - if(!empty($e)) { - throw $e; - } - } - - /** - * Add a new metadata to the row. If the column already exists, throws an exception. - * - * @param string $name name of the metadata to add - * @param mixed $value value of the metadata to set - * @throws Exception - */ - public function addMetadata($name, $value) - { - if(isset($this->c[self::METADATA][$name])) - { - throw new Exception("Metadata $name already in the array!"); - } - $this->c[self::METADATA][$name] = $value; - } - - /** - * Sums the given $row columns values to the existing row' columns values. - * It will sum only the int or float values of $row. - * It will not sum the column 'label' even if it has a numeric value. - * - * If a given column doesn't exist in $this then it is added with the value of $row. - * If the column already exists in $this then we have - * this.columns[idThisCol] += $row.columns[idThisCol] - * - * @param Piwik_DataTable_Row $rowToSum - */ - public function sumRow( Piwik_DataTable_Row $rowToSum, $enableCopyMetadata = true ) - { - foreach($rowToSum->getColumns() as $columnToSumName => $columnToSumValue) - { - if (!isset(self::$unsummableColumns[$columnToSumName])) // make sure we can add this column - { - $thisColumnValue = $this->getColumn($columnToSumName); - - // Max operation - if($columnToSumName == Piwik_Archive::INDEX_MAX_ACTIONS ) - { - $newValue = max($thisColumnValue, $columnToSumValue); - } - else - { - $newValue = $this->sumRowArray($thisColumnValue, $columnToSumValue); - } - $this->setColumn( $columnToSumName, $newValue); - } - } - - if($enableCopyMetadata) - { - $this->sumRowMetadata($rowToSum); - } - } - - public function sumRowMetadata($rowToSum) - { - if (!empty($rowToSum->c[self::METADATA]) - && !$this->isSummaryRow()) - { - // We shall update metadata, and keep the metadata with the _most visits or pageviews_, rather than first or last seen - $visits = max($rowToSum->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS) || $rowToSum->getColumn(Piwik_Archive::INDEX_NB_VISITS), - // Old format pre-1.2, @see also method updateInterestStats() - $rowToSum->getColumn('nb_actions') || $rowToSum->getColumn('nb_visits')); - if (($visits && $visits > $this->maxVisitsSummed) - || empty($this->c[self::METADATA]) - ) { - $this->maxVisitsSummed = $visits; - $this->c[self::METADATA] = $rowToSum->c[self::METADATA]; - } - } - } - - public function isSummaryRow() - { - return $this->getColumn('label') === Piwik_DataTable::LABEL_SUMMARY_ROW; - } - - /** - * Helper function: sums 2 values - * - * @param number|bool $thisColumnValue - * @param number|array $columnToSumValue - * @return array|int - */ - protected function sumRowArray( $thisColumnValue, $columnToSumValue ) - { - if(is_numeric($columnToSumValue)) - { - if($thisColumnValue === false) - { - $thisColumnValue = 0; - } - return $thisColumnValue + $columnToSumValue; - } - - if(is_array($columnToSumValue)) - { - if($thisColumnValue == false) - { - return $columnToSumValue; - } - $newValue = $thisColumnValue; - foreach($columnToSumValue as $arrayIndex => $arrayValue) - { - if(!isset($newValue[$arrayIndex])) - { - $newValue[$arrayIndex] = false; - } - $newValue[$arrayIndex] = $this->sumRowArray($newValue[$arrayIndex], $arrayValue); - } - return $newValue; - } - - if (is_string($columnToSumValue)) - { - if ($thisColumnValue === false) - { - return $columnToSumValue; - } - else if ($columnToSumValue === false) - { - return $thisColumnValue; - } - else - { - throw new Exception("Trying to add two strings values in DataTable_Row::sumRowArray: " - . "'$thisColumnValue' + '$columnToSumValue'"); - } - } - - return 0; - } - - /** - * Helper function to compare array elements - * - * @param mixed $elem1 - * @param mixed $elem2 - * @return bool - */ - static public function compareElements($elem1, $elem2) - { - if (is_array($elem1)) { - if (is_array($elem2)) - { - return strcmp(serialize($elem1), serialize($elem2)); - } - return 1; - } - if (is_array($elem2)) - return -1; - - if ((string)$elem1 === (string)$elem2) - return 0; - - return ((string)$elem1 > (string)$elem2) ? 1 : -1; - } - - /** - * Helper function to test if two rows are equal. - * - * Two rows are equal - * - if they have exactly the same columns / metadata - * - if they have a subDataTable associated, then we check that both of them are the same. - * - * @param Piwik_DataTable_Row $row1 first to compare - * @param Piwik_DataTable_Row $row2 second to compare - * @return bool - */ - static public function isEqual( Piwik_DataTable_Row $row1, Piwik_DataTable_Row $row2 ) - { - //same columns - $cols1 = $row1->getColumns(); - $cols2 = $row2->getColumns(); - - $diff1 = array_udiff($cols1, $cols2, array(__CLASS__, 'compareElements')); - $diff2 = array_udiff($cols2, $cols1, array(__CLASS__, 'compareElements')); - - if($diff1 != $diff2) - { - return false; - } - - $dets1 = $row1->getMetadata(); - $dets2 = $row2->getMetadata(); - - ksort($dets1); - ksort($dets2); - - if($dets1 != $dets2) - { - return false; - } - - // either both are null - // or both have a value - if( !(is_null($row1->getIdSubDataTable()) - && is_null($row2->getIdSubDataTable()) - ) - ) - { - $subtable1 = $row1->getSubtable(); - $subtable2 = $row2->getSubtable(); - if(!Piwik_DataTable::isEqual($subtable1, $subtable2)) - { - return false; - } - } - return true; - } + throw new Exception("Column $name already in the array!"); + } + $this->c[self::COLUMNS][$name] = $value; + } + + /** + * Add columns to the row + * + * @param array $columns Name/Value pairs, e.g., array( name => value , ...) + * @return void + */ + public function addColumns($columns) + { + foreach ($columns as $name => $value) { + try { + $this->addColumn($name, $value); + } catch (Exception $e) { + } + } + + if (!empty($e)) { + throw $e; + } + } + + /** + * Add a new metadata to the row. If the column already exists, throws an exception. + * + * @param string $name name of the metadata to add + * @param mixed $value value of the metadata to set + * @throws Exception + */ + public function addMetadata($name, $value) + { + if (isset($this->c[self::METADATA][$name])) { + throw new Exception("Metadata $name already in the array!"); + } + $this->c[self::METADATA][$name] = $value; + } + + /** + * Sums the given $row columns values to the existing row' columns values. + * It will sum only the int or float values of $row. + * It will not sum the column 'label' even if it has a numeric value. + * + * If a given column doesn't exist in $this then it is added with the value of $row. + * If the column already exists in $this then we have + * this.columns[idThisCol] += $row.columns[idThisCol] + * + * @param Piwik_DataTable_Row $rowToSum + */ + public function sumRow(Piwik_DataTable_Row $rowToSum, $enableCopyMetadata = true) + { + foreach ($rowToSum->getColumns() as $columnToSumName => $columnToSumValue) { + if (!isset(self::$unsummableColumns[$columnToSumName])) // make sure we can add this column + { + $thisColumnValue = $this->getColumn($columnToSumName); + + // Max operation + if ($columnToSumName == Piwik_Archive::INDEX_MAX_ACTIONS) { + $newValue = max($thisColumnValue, $columnToSumValue); + } else { + $newValue = $this->sumRowArray($thisColumnValue, $columnToSumValue); + } + $this->setColumn($columnToSumName, $newValue); + } + } + + if ($enableCopyMetadata) { + $this->sumRowMetadata($rowToSum); + } + } + + public function sumRowMetadata($rowToSum) + { + if (!empty($rowToSum->c[self::METADATA]) + && !$this->isSummaryRow() + ) { + // We shall update metadata, and keep the metadata with the _most visits or pageviews_, rather than first or last seen + $visits = max($rowToSum->getColumn(Piwik_Archive::INDEX_PAGE_NB_HITS) || $rowToSum->getColumn(Piwik_Archive::INDEX_NB_VISITS), + // Old format pre-1.2, @see also method updateInterestStats() + $rowToSum->getColumn('nb_actions') || $rowToSum->getColumn('nb_visits')); + if (($visits && $visits > $this->maxVisitsSummed) + || empty($this->c[self::METADATA]) + ) { + $this->maxVisitsSummed = $visits; + $this->c[self::METADATA] = $rowToSum->c[self::METADATA]; + } + } + } + + public function isSummaryRow() + { + return $this->getColumn('label') === Piwik_DataTable::LABEL_SUMMARY_ROW; + } + + /** + * Helper function: sums 2 values + * + * @param number|bool $thisColumnValue + * @param number|array $columnToSumValue + * @return array|int + */ + protected function sumRowArray($thisColumnValue, $columnToSumValue) + { + if (is_numeric($columnToSumValue)) { + if ($thisColumnValue === false) { + $thisColumnValue = 0; + } + return $thisColumnValue + $columnToSumValue; + } + + if (is_array($columnToSumValue)) { + if ($thisColumnValue == false) { + return $columnToSumValue; + } + $newValue = $thisColumnValue; + foreach ($columnToSumValue as $arrayIndex => $arrayValue) { + if (!isset($newValue[$arrayIndex])) { + $newValue[$arrayIndex] = false; + } + $newValue[$arrayIndex] = $this->sumRowArray($newValue[$arrayIndex], $arrayValue); + } + return $newValue; + } + + if (is_string($columnToSumValue)) { + if ($thisColumnValue === false) { + return $columnToSumValue; + } else if ($columnToSumValue === false) { + return $thisColumnValue; + } else { + throw new Exception("Trying to add two strings values in DataTable_Row::sumRowArray: " + . "'$thisColumnValue' + '$columnToSumValue'"); + } + } + + return 0; + } + + /** + * Helper function to compare array elements + * + * @param mixed $elem1 + * @param mixed $elem2 + * @return bool + */ + static public function compareElements($elem1, $elem2) + { + if (is_array($elem1)) { + if (is_array($elem2)) { + return strcmp(serialize($elem1), serialize($elem2)); + } + return 1; + } + if (is_array($elem2)) + return -1; + + if ((string)$elem1 === (string)$elem2) + return 0; + + return ((string)$elem1 > (string)$elem2) ? 1 : -1; + } + + /** + * Helper function to test if two rows are equal. + * + * Two rows are equal + * - if they have exactly the same columns / metadata + * - if they have a subDataTable associated, then we check that both of them are the same. + * + * @param Piwik_DataTable_Row $row1 first to compare + * @param Piwik_DataTable_Row $row2 second to compare + * @return bool + */ + static public function isEqual(Piwik_DataTable_Row $row1, Piwik_DataTable_Row $row2) + { + //same columns + $cols1 = $row1->getColumns(); + $cols2 = $row2->getColumns(); + + $diff1 = array_udiff($cols1, $cols2, array(__CLASS__, 'compareElements')); + $diff2 = array_udiff($cols2, $cols1, array(__CLASS__, 'compareElements')); + + if ($diff1 != $diff2) { + return false; + } + + $dets1 = $row1->getMetadata(); + $dets2 = $row2->getMetadata(); + + ksort($dets1); + ksort($dets2); + + if ($dets1 != $dets2) { + return false; + } + + // either both are null + // or both have a value + if (!(is_null($row1->getIdSubDataTable()) + && is_null($row2->getIdSubDataTable()) + ) + ) { + $subtable1 = $row1->getSubtable(); + $subtable2 = $row2->getSubtable(); + if (!Piwik_DataTable::isEqual($subtable1, $subtable2)) { + return false; + } + } + return true; + } } diff --git a/core/DataTable/Row/DataTableSummary.php b/core/DataTable/Row/DataTableSummary.php index 7a38dc6118..f0ad234d54 100644 --- a/core/DataTable/Row/DataTableSummary.php +++ b/core/DataTable/Row/DataTableSummary.php @@ -1,63 +1,60 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * This class creates a row from a given DataTable. - * The row contains + * This class creates a row from a given DataTable. + * The row contains * - for each numeric column, the returned "summary" column is the sum of all the subRows * - for every other column, it is ignored and will not be in the "summary row" - * + * * @see Piwik_DataTable_Row::sumRow() for more information on the algorithm - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Row_DataTableSummary extends Piwik_DataTable_Row { - /** - * @param Piwik_DataTable $subTable - */ - function __construct($subTable = null) - { - parent::__construct(); - - if ($subTable !== null) - { - $this->sumTable($subTable); - } - } - - /** - * Reset this row to an empty one and sum the associated subtable again. - */ - public function recalculate() - { - $id = $this->getIdSubDataTable(); - if ($id !== null) - { - $subtable = Piwik_DataTable_Manager::getInstance()->getTable($id); - $this->sumTable($subtable); - } - } - - /** - * Sums a tables row with this one. - * - * @param Piwik_DataTable $table - */ - private function sumTable( $table ) - { - foreach($table->getRows() as $row) - { - $this->sumRow($row, $enableCopyMetadata = false); - } - } + /** + * @param Piwik_DataTable $subTable + */ + function __construct($subTable = null) + { + parent::__construct(); + + if ($subTable !== null) { + $this->sumTable($subTable); + } + } + + /** + * Reset this row to an empty one and sum the associated subtable again. + */ + public function recalculate() + { + $id = $this->getIdSubDataTable(); + if ($id !== null) { + $subtable = Piwik_DataTable_Manager::getInstance()->getTable($id); + $this->sumTable($subtable); + } + } + + /** + * Sums a tables row with this one. + * + * @param Piwik_DataTable $table + */ + private function sumTable($table) + { + foreach ($table->getRows() as $row) { + $this->sumRow($row, $enableCopyMetadata = false); + } + } } diff --git a/core/DataTable/Simple.php b/core/DataTable/Simple.php index bba122db20..7189fb0b2e 100644 --- a/core/DataTable/Simple.php +++ b/core/DataTable/Simple.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,40 +12,40 @@ /** * The DataTable_Simple is used to provide an easy way to create simple DataGrid. * A DataTable_Simple actually is a DataTable with 2 columns: 'label' and 'value'. - * - * It is usually best to return a DataTable_Simple instead of + * + * It is usually best to return a DataTable_Simple instead of * a PHP array (or other custom data structure) in API methods: * - the generic filters can be applied automatically (offset, limit, pattern search, sort, etc.) * - the renderer can be applied (XML, PHP, HTML, etc.) * So you don't have to write specific renderer for your data, it is already available in all the formats supported natively by Piwik. - * + * * @package Piwik * @subpackage Piwik_DataTable */ class Piwik_DataTable_Simple extends Piwik_DataTable { - /** - * Loads (append) in the DataTable the array information - * - * @param array $array Array containing the rows information - * array( - * 'Label row 1' => Value row 1, - * 'Label row 2' => Value row 2, - * ) - */ - public function addRowsFromArray($array) - { - $this->addRowsFromSimpleArray(array($array)); - } + /** + * Loads (append) in the DataTable the array information + * + * @param array $array Array containing the rows information + * array( + * 'Label row 1' => Value row 1, + * 'Label row 2' => Value row 2, + * ) + */ + public function addRowsFromArray($array) + { + $this->addRowsFromSimpleArray(array($array)); + } - /** - * Updates the given column with the given value - * - * @param string $columnName - * @param mixed $value - */ - public function setColumn($columnName, $value) - { - $this->getLastRow()->setColumn($columnName, $value); - } + /** + * Updates the given column with the given value + * + * @param string $columnName + * @param mixed $value + */ + public function setColumn($columnName, $value) + { + $this->getLastRow()->setColumn($columnName, $value); + } } diff --git a/core/Date.php b/core/Date.php index dcafb0d993..cd0fa6c238 100644 --- a/core/Date.php +++ b/core/Date.php @@ -16,658 +16,626 @@ */ class Piwik_Date { - /** - * The stored timestamp is always UTC based. - * The returned timestamp via getTimestamp() will have the conversion applied - * @var int|null - */ - protected $timestamp = null; - - /** - * Timezone the current date object is set to. - * Timezone will only affect the returned timestamp via getTimestamp() - * @var string - */ - protected $timezone = 'UTC'; - - const DATE_TIME_FORMAT = 'Y-m-d H:i:s'; - - /** - * Builds a Piwik_Date object - * - * @param int $timestamp - * @param string $timezone - * @throws Exception - */ - 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. - * - * @param string|self $dateString '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() - * @throws Exception - * @return Piwik_Date - */ - static public function factory($dateString, $timezone = null) - { - $invalidDateException = new Exception(Piwik_TranslateException('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "http://php.net/strtotime")).": $dateString"); - if($dateString instanceof self) - { - $dateString = $dateString->toString(); - } - if($dateString == 'now') - { - $date = self::now(); - } - elseif($dateString == 'today') - { - $date = self::today(); - } - elseif($dateString == 'yesterday') - { - $date = self::yesterday(); - } - elseif($dateString == 'yesterdaySameTime') - { - $date = self::yesterdaySameTime(); - } - elseif (!is_int($dateString) - && ( - // strtotime returns the timestamp for April 1st for a date like 2011-04-01,today - // but we don't want this, as this is a date range and supposed to throw the exception - strpos($dateString, ',') !== false - || - ($dateString = strtotime($dateString)) === false - )) - { - throw $invalidDateException; - } - else - { - $date = new Piwik_Date($dateString); - } - $timestamp = $date->getTimestamp(); - // can't be doing web analytics before the 1st website - // Tue, 06 Aug 1991 00:00:00 GMT - if($timestamp < 681436800 ) - { - throw $invalidDateException; - } - if(empty($timezone)) - { - return $date; - } - - $timestamp = self::adjustForTimezone($timestamp, $timezone); - return Piwik_Date::factory($timestamp); - } - - /** - * Returns the datetime of the current timestamp - * - * @return string - */ - function getDatetime() - { - return $this->toString(self::DATE_TIME_FORMAT); - } - - /** - * Returns the datetime start in UTC - * - * @return string - */ - function getDateStartUTC() - { - $dateStartUTC = gmdate('Y-m-d', $this->timestamp); - $date = Piwik_Date::factory($dateStartUTC)->setTimezone($this->timezone); - return $date->toString(self::DATE_TIME_FORMAT); - } - - /** - * Returns the datetime end in UTC - * - * @return string - */ - function getDateEndUTC() - { - $dateEndUTC = gmdate('Y-m-d 23:59:59', $this->timestamp); - $date = Piwik_Date::factory($dateEndUTC)->setTimezone($this->timezone); - return $date->toString(self::DATE_TIME_FORMAT); - } - - /** - * 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', ... - * @return Piwik_Date - */ - public function setTimezone($timezone) - { - return new Piwik_Date($this->timestamp, $timezone); - } - - /** - * 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 string $timezone - * @return int|bool utc offset or false - */ - static protected function extractUtcOffset($timezone) - { - if($timezone == 'UTC') - { - return 0; - } - $start = substr($timezone, 0, 4); - if($start != 'UTC-' - && $start != 'UTC+') - { - return false; - } - $offset = (float)substr($timezone, 4); - if($start == 'UTC-') { - $offset = -$offset; - } - return $offset; - } - - /** - * Adjusts a UNIX timestamp in UTC to a specific timezone. - * - * @param int $timestamp The UNIX timestamp to adjust. - * @param string $timezone The timezone to adjust to. - * @return int The adjusted time as seconds from EPOCH. - */ - static public function adjustForTimezone($timestamp, $timezone) - { - // manually adjust for UTC timezones - $utcOffset = self::extractUtcOffset($timezone); - if($utcOffset !== false) - { - return self::addHourTo($timestamp, $utcOffset); - } - - date_default_timezone_set($timezone); - $datetime = date(self::DATE_TIME_FORMAT, $timestamp); - date_default_timezone_set('UTC'); - - return strtotime($datetime); - } - - /** - * Returns the Unix timestamp of the date in UTC - * - * @return int - */ - public function getTimestampUTC() - { - return $this->timestamp; - } - - /** - * Returns the unix timestamp of the date in UTC, - * converted from the date timezone - * - * @return int - */ - public function getTimestamp() - { - if(empty($this->timezone)) - { - $this->timezone = 'UTC'; - } - $utcOffset = self::extractUtcOffset($this->timezone); - if($utcOffset !== false) { - return (int)($this->timestamp - $utcOffset * 3600); - } - // 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; - } - - /** - * Returns true if the current date is older than the given $date - * - * @param Piwik_Date $date - * @return bool - */ - public function isLater( Piwik_Date $date) - { - return $this->getTimestamp() > $date->getTimestamp(); - } - - /** - * Returns true if the current date is earlier than the given $date - * - * @param Piwik_Date $date - * @return bool - */ - public function isEarlier(Piwik_Date $date) - { - return $this->getTimestamp() < $date->getTimestamp(); - } - - /** - * Returns the Y-m-d representation of the string. - * You can specify the output, see the list on php.net/date - * - * @param string $part - * @return string - */ - public function toString($part = 'Y-m-d') - { - return date($part, $this->getTimestamp()); - } - - /** - * @see toString() - * - * @return string - */ - public function __toString() - { - return $this->toString(); - } - - /** - * 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 int 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 $date Month to compare - * @return int 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 - * - * @param int $day Day eg. 31 - * @return Piwik_Date new date - */ - public function setDay( $day ) - { - $ts = $this->timestamp; - $result = mktime( - date('H', $ts), - date('i', $ts), - date('s', $ts), - date('n', $ts), - $day, - date('Y', $ts) - ); - return new Piwik_Date( $result, $this->timezone ); - } - - /** - * Sets a new year - * Returned is the new date object - * Doesn't modify $this - * - * @param int $year 2010 - * @return Piwik_Date new date - */ - public function setYear( $year ) - { - $ts = $this->timestamp; - $result = mktime( - date('H', $ts), - date('i', $ts), - date('s', $ts), - date('n', $ts), - date('j', $ts), - $year - ); - 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 - * Doesn't modify $this - * - * @param int $n - * @return Piwik_Date new date - */ - public function subDay( $n ) - { - if($n === 0) - { - return clone $this; - } - $ts = strtotime("-$n day", $this->timestamp); - return new Piwik_Date( $ts, $this->timezone ); - } - - /** - * Subtracts weeks from the existing date object and returns a new Piwik_Date object - * Returned is the new date object - * Doesn't modify $this - * - * @param int $n - * @return Piwik_Date new date - */ - public function subWeek( $n ) - { - return $this->subDay( 7 * $n ); - } - - /** - * Subtracts a month from the existing date object. - * Returned is the new date object - * Doesn't modify $this - * - * @param int $n - * @return Piwik_Date new date - */ - public function subMonth( $n ) - { - if($n === 0) - { - return clone $this; - } - $ts = $this->timestamp; - $result = mktime( - date('H', $ts), - date('i', $ts), - date('s', $ts), - date('n', $ts) - $n, - 1, // we set the day to 1 - date('Y', $ts) - ); - return new Piwik_Date( $result, $this->timezone ); - } - - /** - * Subtracts a year from the existing date object. - * Returned is the new date object - * Doesn't modify $this - * - * @param int $n - * @return Piwik_Date new date - */ - public function subYear( $n ) - { - if($n === 0) - { - return clone $this; - } - $ts = $this->timestamp; - $result = mktime( - date('H', $ts), - date('i', $ts), - date('s', $ts), - 1, // we set the month to 1 - 1, // we set the day to 1 - date('Y', $ts) - $n - ); - return new Piwik_Date( $result, $this->timezone ); - } - - /** - * Returns a localized date string, given a template. - * Allowed tags are: %day%, %shortDay%, %longDay%, etc. - * - * @param string $template string eg. %shortMonth% %longYear% - * @return string eg. "Aug 2009" - */ - public function getLocalized($template) - { - $day = $this->toString('j'); - $dayOfWeek = $this->toString('N'); - $monthOfYear = $this->toString('n'); - $patternToValue = array( - "%day%" => $day, - "%shortMonth%" => Piwik_Translate('General_ShortMonth_'.$monthOfYear), - "%longMonth%" => Piwik_Translate('General_LongMonth_'.$monthOfYear), - "%shortDay%" => Piwik_Translate('General_ShortDay_'.$dayOfWeek), - "%longDay%" => Piwik_Translate('General_LongDay_'.$dayOfWeek), - "%longYear%" => $this->toString('Y'), - "%shortYear%" => $this->toString('y'), - "%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 - * Doesn't modify $this - * - * @param int $n Number of days to add - * @return Piwik_Date new date - */ - public function addDay( $n ) - { - $ts = strtotime("+$n day", $this->timestamp); - return new Piwik_Date( $ts, $this->timezone ); - } - - /** - * Adds hours to the existing date object. - * Returned is the new date object - * Doesn't modify $this - * - * @param int $n Number of hours to add - * @return Piwik_Date new date - */ - public function addHour( $n ) - { - $ts = self::addHourTo( $this->timestamp, $n ); - return new Piwik_Date( $ts, $this->timezone ); - } - - /** - * Adds N number of hours to a UNIX timestamp and returns the result. - * - * @param int $timestamp The timestamp to add to. - * @param number $n Number of hours to add. - * @return int The result as a UNIX timestamp. - */ - public static function addHourTo( $timestamp, $n ) - { - $isNegative = ($n < 0); - $minutes = 0; - if($n != round($n)) - { - if($n >= 1 || $n <= -1) - { - $extraMinutes = floor(abs($n)); - if($isNegative) - { - $extraMinutes = -$extraMinutes; - } - $minutes = abs($n - $extraMinutes) * 60; - if($isNegative) { - $minutes *= -1; - } - } - else - { - $minutes = $n * 60; - } - $n = floor(abs($n)); - if($isNegative) { - $n *= -1; - } - } - return (int)($timestamp + round($minutes * 60) + $n * 3600); - } - - /** - * Substract hour to the existing date object. - * Returned is the new date object - * Doesn't modify $this - * - * @param int $n Number of hours to substract - * @return Piwik_Date new date - */ - public function subHour( $n ) - { - return $this->addHour(-$n); - } - - /** - * Adds period to the existing date object. - * Returned is the new date object - * Doesn't modify $this - * - * @param int $n - * @param string $period period to add (WEEK, DAY,...) - * @return Piwik_Date new date - */ - public function addPeriod( $n, $period ) - { - if($n < 0) - { - $ts = strtotime("$n $period", $this->timestamp); - } - else - { - $ts = strtotime("+$n $period", $this->timestamp); - } - return new Piwik_Date( $ts, $this->timezone ); - } - - /** - * Subtracts period from the existing date object. - * Returned is the new date object - * Doesn't modify $this - * - * @param int $n - * @param string $period period to sub - * @return Piwik_Date new date - */ - public function subPeriod( $n, $period ) - { - return $this->addPeriod(-$n, $period ); - } + /** + * The stored timestamp is always UTC based. + * The returned timestamp via getTimestamp() will have the conversion applied + * @var int|null + */ + protected $timestamp = null; + + /** + * Timezone the current date object is set to. + * Timezone will only affect the returned timestamp via getTimestamp() + * @var string + */ + protected $timezone = 'UTC'; + + const DATE_TIME_FORMAT = 'Y-m-d H:i:s'; + + /** + * Builds a Piwik_Date object + * + * @param int $timestamp + * @param string $timezone + * @throws Exception + */ + 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. + * + * @param string|self $dateString '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() + * @throws Exception + * @return Piwik_Date + */ + static public function factory($dateString, $timezone = null) + { + $invalidDateException = new Exception(Piwik_TranslateException('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "http://php.net/strtotime")) . ": $dateString"); + if ($dateString instanceof self) { + $dateString = $dateString->toString(); + } + if ($dateString == 'now') { + $date = self::now(); + } elseif ($dateString == 'today') { + $date = self::today(); + } elseif ($dateString == 'yesterday') { + $date = self::yesterday(); + } elseif ($dateString == 'yesterdaySameTime') { + $date = self::yesterdaySameTime(); + } elseif (!is_int($dateString) + && ( + // strtotime returns the timestamp for April 1st for a date like 2011-04-01,today + // but we don't want this, as this is a date range and supposed to throw the exception + strpos($dateString, ',') !== false + || + ($dateString = strtotime($dateString)) === false + ) + ) { + throw $invalidDateException; + } else { + $date = new Piwik_Date($dateString); + } + $timestamp = $date->getTimestamp(); + // can't be doing web analytics before the 1st website + // Tue, 06 Aug 1991 00:00:00 GMT + if ($timestamp < 681436800) { + throw $invalidDateException; + } + if (empty($timezone)) { + return $date; + } + + $timestamp = self::adjustForTimezone($timestamp, $timezone); + return Piwik_Date::factory($timestamp); + } + + /** + * Returns the datetime of the current timestamp + * + * @return string + */ + function getDatetime() + { + return $this->toString(self::DATE_TIME_FORMAT); + } + + /** + * Returns the datetime start in UTC + * + * @return string + */ + function getDateStartUTC() + { + $dateStartUTC = gmdate('Y-m-d', $this->timestamp); + $date = Piwik_Date::factory($dateStartUTC)->setTimezone($this->timezone); + return $date->toString(self::DATE_TIME_FORMAT); + } + + /** + * Returns the datetime end in UTC + * + * @return string + */ + function getDateEndUTC() + { + $dateEndUTC = gmdate('Y-m-d 23:59:59', $this->timestamp); + $date = Piwik_Date::factory($dateEndUTC)->setTimezone($this->timezone); + return $date->toString(self::DATE_TIME_FORMAT); + } + + /** + * 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', ... + * @return Piwik_Date + */ + public function setTimezone($timezone) + { + return new Piwik_Date($this->timestamp, $timezone); + } + + /** + * 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 string $timezone + * @return int|bool utc offset or false + */ + static protected function extractUtcOffset($timezone) + { + if ($timezone == 'UTC') { + return 0; + } + $start = substr($timezone, 0, 4); + if ($start != 'UTC-' + && $start != 'UTC+' + ) { + return false; + } + $offset = (float)substr($timezone, 4); + if ($start == 'UTC-') { + $offset = -$offset; + } + return $offset; + } + + /** + * Adjusts a UNIX timestamp in UTC to a specific timezone. + * + * @param int $timestamp The UNIX timestamp to adjust. + * @param string $timezone The timezone to adjust to. + * @return int The adjusted time as seconds from EPOCH. + */ + static public function adjustForTimezone($timestamp, $timezone) + { + // manually adjust for UTC timezones + $utcOffset = self::extractUtcOffset($timezone); + if ($utcOffset !== false) { + return self::addHourTo($timestamp, $utcOffset); + } + + date_default_timezone_set($timezone); + $datetime = date(self::DATE_TIME_FORMAT, $timestamp); + date_default_timezone_set('UTC'); + + return strtotime($datetime); + } + + /** + * Returns the Unix timestamp of the date in UTC + * + * @return int + */ + public function getTimestampUTC() + { + return $this->timestamp; + } + + /** + * Returns the unix timestamp of the date in UTC, + * converted from the date timezone + * + * @return int + */ + public function getTimestamp() + { + if (empty($this->timezone)) { + $this->timezone = 'UTC'; + } + $utcOffset = self::extractUtcOffset($this->timezone); + if ($utcOffset !== false) { + return (int)($this->timestamp - $utcOffset * 3600); + } + // 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; + } + + /** + * Returns true if the current date is older than the given $date + * + * @param Piwik_Date $date + * @return bool + */ + public function isLater(Piwik_Date $date) + { + return $this->getTimestamp() > $date->getTimestamp(); + } + + /** + * Returns true if the current date is earlier than the given $date + * + * @param Piwik_Date $date + * @return bool + */ + public function isEarlier(Piwik_Date $date) + { + return $this->getTimestamp() < $date->getTimestamp(); + } + + /** + * Returns the Y-m-d representation of the string. + * You can specify the output, see the list on php.net/date + * + * @param string $part + * @return string + */ + public function toString($part = 'Y-m-d') + { + return date($part, $this->getTimestamp()); + } + + /** + * @see toString() + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } + + /** + * 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 int 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 $date Month to compare + * @return int 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 + * + * @param int $day Day eg. 31 + * @return Piwik_Date new date + */ + public function setDay($day) + { + $ts = $this->timestamp; + $result = mktime( + date('H', $ts), + date('i', $ts), + date('s', $ts), + date('n', $ts), + $day, + date('Y', $ts) + ); + return new Piwik_Date($result, $this->timezone); + } + + /** + * Sets a new year + * Returned is the new date object + * Doesn't modify $this + * + * @param int $year 2010 + * @return Piwik_Date new date + */ + public function setYear($year) + { + $ts = $this->timestamp; + $result = mktime( + date('H', $ts), + date('i', $ts), + date('s', $ts), + date('n', $ts), + date('j', $ts), + $year + ); + 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 + * Doesn't modify $this + * + * @param int $n + * @return Piwik_Date new date + */ + public function subDay($n) + { + if ($n === 0) { + return clone $this; + } + $ts = strtotime("-$n day", $this->timestamp); + return new Piwik_Date($ts, $this->timezone); + } + + /** + * Subtracts weeks from the existing date object and returns a new Piwik_Date object + * Returned is the new date object + * Doesn't modify $this + * + * @param int $n + * @return Piwik_Date new date + */ + public function subWeek($n) + { + return $this->subDay(7 * $n); + } + + /** + * Subtracts a month from the existing date object. + * Returned is the new date object + * Doesn't modify $this + * + * @param int $n + * @return Piwik_Date new date + */ + public function subMonth($n) + { + if ($n === 0) { + return clone $this; + } + $ts = $this->timestamp; + $result = mktime( + date('H', $ts), + date('i', $ts), + date('s', $ts), + date('n', $ts) - $n, + 1, // we set the day to 1 + date('Y', $ts) + ); + return new Piwik_Date($result, $this->timezone); + } + + /** + * Subtracts a year from the existing date object. + * Returned is the new date object + * Doesn't modify $this + * + * @param int $n + * @return Piwik_Date new date + */ + public function subYear($n) + { + if ($n === 0) { + return clone $this; + } + $ts = $this->timestamp; + $result = mktime( + date('H', $ts), + date('i', $ts), + date('s', $ts), + 1, // we set the month to 1 + 1, // we set the day to 1 + date('Y', $ts) - $n + ); + return new Piwik_Date($result, $this->timezone); + } + + /** + * Returns a localized date string, given a template. + * Allowed tags are: %day%, %shortDay%, %longDay%, etc. + * + * @param string $template string eg. %shortMonth% %longYear% + * @return string eg. "Aug 2009" + */ + public function getLocalized($template) + { + $day = $this->toString('j'); + $dayOfWeek = $this->toString('N'); + $monthOfYear = $this->toString('n'); + $patternToValue = array( + "%day%" => $day, + "%shortMonth%" => Piwik_Translate('General_ShortMonth_' . $monthOfYear), + "%longMonth%" => Piwik_Translate('General_LongMonth_' . $monthOfYear), + "%shortDay%" => Piwik_Translate('General_ShortDay_' . $dayOfWeek), + "%longDay%" => Piwik_Translate('General_LongDay_' . $dayOfWeek), + "%longYear%" => $this->toString('Y'), + "%shortYear%" => $this->toString('y'), + "%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 + * Doesn't modify $this + * + * @param int $n Number of days to add + * @return Piwik_Date new date + */ + public function addDay($n) + { + $ts = strtotime("+$n day", $this->timestamp); + return new Piwik_Date($ts, $this->timezone); + } + + /** + * Adds hours to the existing date object. + * Returned is the new date object + * Doesn't modify $this + * + * @param int $n Number of hours to add + * @return Piwik_Date new date + */ + public function addHour($n) + { + $ts = self::addHourTo($this->timestamp, $n); + return new Piwik_Date($ts, $this->timezone); + } + + /** + * Adds N number of hours to a UNIX timestamp and returns the result. + * + * @param int $timestamp The timestamp to add to. + * @param number $n Number of hours to add. + * @return int The result as a UNIX timestamp. + */ + public static function addHourTo($timestamp, $n) + { + $isNegative = ($n < 0); + $minutes = 0; + if ($n != round($n)) { + if ($n >= 1 || $n <= -1) { + $extraMinutes = floor(abs($n)); + if ($isNegative) { + $extraMinutes = -$extraMinutes; + } + $minutes = abs($n - $extraMinutes) * 60; + if ($isNegative) { + $minutes *= -1; + } + } else { + $minutes = $n * 60; + } + $n = floor(abs($n)); + if ($isNegative) { + $n *= -1; + } + } + return (int)($timestamp + round($minutes * 60) + $n * 3600); + } + + /** + * Substract hour to the existing date object. + * Returned is the new date object + * Doesn't modify $this + * + * @param int $n Number of hours to substract + * @return Piwik_Date new date + */ + public function subHour($n) + { + return $this->addHour(-$n); + } + + /** + * Adds period to the existing date object. + * Returned is the new date object + * Doesn't modify $this + * + * @param int $n + * @param string $period period to add (WEEK, DAY,...) + * @return Piwik_Date new date + */ + public function addPeriod($n, $period) + { + if ($n < 0) { + $ts = strtotime("$n $period", $this->timestamp); + } else { + $ts = strtotime("+$n $period", $this->timestamp); + } + return new Piwik_Date($ts, $this->timezone); + } + + /** + * Subtracts period from the existing date object. + * Returned is the new date object + * Doesn't modify $this + * + * @param int $n + * @param string $period period to sub + * @return Piwik_Date new date + */ + public function subPeriod($n, $period) + { + return $this->addPeriod(-$n, $period); + } } diff --git a/core/Db/Adapter.php b/core/Db/Adapter.php index 9e616c52fc..de3412e9a6 100644 --- a/core/Db/Adapter.php +++ b/core/Db/Adapter.php @@ -15,95 +15,89 @@ */ class Piwik_Db_Adapter { - /** - * Create adapter - * - * @param string $adapterName database adapter name - * @param array $dbInfos database connection info - * @param bool $connect - * @return Piwik_Db_Adapter_Interface - */ - public static function factory($adapterName, & $dbInfos, $connect = true) - { - if($connect) - { - if($dbInfos['port'][0] == '/') - { - $dbInfos['unix_socket'] = $dbInfos['port']; - unset($dbInfos['host']); - unset($dbInfos['port']); - } + /** + * Create adapter + * + * @param string $adapterName database adapter name + * @param array $dbInfos database connection info + * @param bool $connect + * @return Piwik_Db_Adapter_Interface + */ + public static function factory($adapterName, & $dbInfos, $connect = true) + { + if ($connect) { + 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']); - } + // not used by Zend Framework + unset($dbInfos['tables_prefix']); + unset($dbInfos['adapter']); + unset($dbInfos['schema']); + } - $className = self::getAdapterClassName($adapterName); - Piwik_Loader::loadClass($className); + $className = self::getAdapterClassName($adapterName); + Piwik_Loader::loadClass($className); - /* - * 5.2.1 fixes various bugs with references that caused PDO_MYSQL getConnection() - * to clobber $dbInfos. (#33282, #35106, #39944) - */ - if (version_compare(PHP_VERSION, '5.2.1') < 0) - { - $adapter = new $className(array_map('trim', $dbInfos)); - } - else - { - $adapter = new $className($dbInfos); - } + /* + * 5.2.1 fixes various bugs with references that caused PDO_MYSQL getConnection() + * to clobber $dbInfos. (#33282, #35106, #39944) + */ + if (version_compare(PHP_VERSION, '5.2.1') < 0) { + $adapter = new $className(array_map('trim', $dbInfos)); + } else { + $adapter = new $className($dbInfos); + } - if($connect) - { - $adapter->getConnection(); + if ($connect) { + $adapter->getConnection(); - Zend_Db_Table::setDefaultAdapter($adapter); - // we don't want the connection information to appear in the logs - $adapter->resetConfig(); - } + Zend_Db_Table::setDefaultAdapter($adapter); + // we don't want the connection information to appear in the logs + $adapter->resetConfig(); + } - return $adapter; - } + return $adapter; + } - /** - * Get adapter class name - * - * @param string $adapterName - * @return string - */ - private static function getAdapterClassName($adapterName) - { - return 'Piwik_Db_Adapter_' . str_replace(' ', '_', ucwords(str_replace('_', ' ', strtolower($adapterName)))); - } + /** + * Get adapter class name + * + * @param string $adapterName + * @return string + */ + private static function getAdapterClassName($adapterName) + { + return 'Piwik_Db_Adapter_' . str_replace(' ', '_', ucwords(str_replace('_', ' ', strtolower($adapterName)))); + } - /** - * Get default port for named adapter - * - * @param string $adapterName - * @return int - */ - public static function getDefaultPortForAdapter($adapterName) - { - $className = self::getAdapterClassName($adapterName); - return call_user_func(array($className, 'getDefaultPort')); - } + /** + * Get default port for named adapter + * + * @param string $adapterName + * @return int + */ + public static function getDefaultPortForAdapter($adapterName) + { + $className = self::getAdapterClassName($adapterName); + return call_user_func(array($className, 'getDefaultPort')); + } - /** - * Get list of adapters - * - * @return array - */ - public static function getAdapters() - { - static $adapterNames = array( - // currently supported by Piwik - 'Pdo_Mysql', - 'Mysqli', + /** + * Get list of adapters + * + * @return array + */ + public static function getAdapters() + { + static $adapterNames = array( + // currently supported by Piwik + 'Pdo_Mysql', + 'Mysqli', - // other adapters supported by Zend_Db + // other adapters supported by Zend_Db // 'Pdo_Pgsql', // 'Pdo_Mssql', // 'Sqlsrv', @@ -111,19 +105,17 @@ class Piwik_Db_Adapter // 'Db2', // 'Pdo_Oci', // 'Oracle', - ); + ); - $adapters = array(); + $adapters = array(); - foreach($adapterNames as $adapterName) - { - $className = 'Piwik_Db_Adapter_'.$adapterName; - if(call_user_func(array($className, 'isEnabled'))) - { - $adapters[strtoupper($adapterName)] = call_user_func(array($className, 'getDefaultPort')); - } - } + foreach ($adapterNames as $adapterName) { + $className = 'Piwik_Db_Adapter_' . $adapterName; + if (call_user_func(array($className, 'isEnabled'))) { + $adapters[strtoupper($adapterName)] = call_user_func(array($className, 'getDefaultPort')); + } + } - return $adapters; - } + return $adapters; + } } diff --git a/core/Db/Adapter/Interface.php b/core/Db/Adapter/Interface.php index db0762161a..bd81c0834d 100644 --- a/core/Db/Adapter/Interface.php +++ b/core/Db/Adapter/Interface.php @@ -15,59 +15,59 @@ */ interface Piwik_Db_Adapter_Interface { - /** - * Reset the configuration variables in this adapter. - */ - public function resetConfig(); + /** + * Reset the configuration variables in this adapter. + */ + public function resetConfig(); - /** - * Return default port. - * - * @return int - */ - public static function getDefaultPort(); + /** + * Return default port. + * + * @return int + */ + public static function getDefaultPort(); - /** - * Check database server version - * - * @throws Exception if database version is less than required version - */ - public function checkServerVersion(); + /** + * Check database server version + * + * @throws Exception if database version is less than required version + */ + public function checkServerVersion(); - /** - * Returns true if this adapter's required extensions are enabled - * - * @return bool - */ - public static function isEnabled(); + /** + * Returns true if this adapter's required extensions are enabled + * + * @return bool + */ + public static function isEnabled(); - /** - * Returns true if this adapter supports blobs as fields - * - * @return bool - */ - public function hasBlobDataType(); + /** + * Returns true if this adapter supports blobs as fields + * + * @return bool + */ + public function hasBlobDataType(); - /** - * Returns true if this adapter supports bulk loading - * - * @return bool - */ - public function hasBulkLoader(); + /** + * Returns true if this adapter supports bulk loading + * + * @return bool + */ + public function hasBulkLoader(); - /** - * Test error number - * - * @param Exception $e - * @param string $errno - * @return bool - */ - public function isErrNo($e, $errno); + /** + * Test error number + * + * @param Exception $e + * @param string $errno + * @return bool + */ + public function isErrNo($e, $errno); - /** - * Is the connection character set equal to utf8? - * - * @return bool - */ - public function isConnectionUTF8(); + /** + * Is the connection character set equal to utf8? + * + * @return bool + */ + public function isConnectionUTF8(); } diff --git a/core/Db/Adapter/Mysqli.php b/core/Db/Adapter/Mysqli.php index db38734f81..efb309afa5 100644 --- a/core/Db/Adapter/Mysqli.php +++ b/core/Db/Adapter/Mysqli.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,163 +15,159 @@ */ class Piwik_Db_Adapter_Mysqli extends Zend_Db_Adapter_Mysqli implements Piwik_Db_Adapter_Interface { - /** - * Constructor - * - * @param array|Zend_Config $config database configuration - */ - public function __construct($config) - { - // Enable LOAD DATA INFILE - $config['driver_options'][MYSQLI_OPT_LOCAL_INFILE] = true; - parent::__construct($config); - } + /** + * Constructor + * + * @param array|Zend_Config $config database configuration + */ + public function __construct($config) + { + // Enable LOAD DATA INFILE + $config['driver_options'][MYSQLI_OPT_LOCAL_INFILE] = true; + parent::__construct($config); + } - /** - * Reset the configuration variables in this adapter. - */ - public function resetConfig() - { - $this->_config = array(); - } + /** + * Reset the configuration variables in this adapter. + */ + public function resetConfig() + { + $this->_config = array(); + } - /** - * Return default port. - * - * @return int - */ - public static function getDefaultPort() - { - return 3306; - } + /** + * Return default port. + * + * @return int + */ + public static function getDefaultPort() + { + return 3306; + } - /** - * Check MySQL version - * - * @throws Exception - */ - public function checkServerVersion() - { - $serverVersion = $this->getServerVersion(); - $requiredVersion = Piwik_Config::getInstance()->General['minimum_mysql_version']; - if(version_compare($serverVersion, $requiredVersion) === -1) - { - throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MySQL', $serverVersion, $requiredVersion))); - } - } + /** + * Check MySQL version + * + * @throws Exception + */ + public function checkServerVersion() + { + $serverVersion = $this->getServerVersion(); + $requiredVersion = Piwik_Config::getInstance()->General['minimum_mysql_version']; + if (version_compare($serverVersion, $requiredVersion) === -1) { + throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MySQL', $serverVersion, $requiredVersion))); + } + } - /** - * Check client version compatibility against database server - * - * @throws Exception - */ - public function checkClientVersion() - { - $serverVersion = $this->getServerVersion(); - $clientVersion = $this->getClientVersion(); - // incompatible change to DECIMAL implementation in 5.0.3 - if(version_compare($serverVersion, '5.0.3') >= 0 - && version_compare($clientVersion, '5.0.3') < 0) - { - throw new Exception(Piwik_TranslateException('General_ExceptionIncompatibleClientServerVersions', array('MySQL', $clientVersion, $serverVersion))); - } - } + /** + * Check client version compatibility against database server + * + * @throws Exception + */ + public function checkClientVersion() + { + $serverVersion = $this->getServerVersion(); + $clientVersion = $this->getClientVersion(); + // incompatible change to DECIMAL implementation in 5.0.3 + if (version_compare($serverVersion, '5.0.3') >= 0 + && version_compare($clientVersion, '5.0.3') < 0 + ) { + throw new Exception(Piwik_TranslateException('General_ExceptionIncompatibleClientServerVersions', array('MySQL', $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('mysqli', $extensions); - } + /** + * Returns true if this adapter's required extensions are enabled + * + * @return bool + */ + public static function isEnabled() + { + $extensions = @get_loaded_extensions(); + return in_array('mysqli', $extensions); + } - /** - * Returns true if this adapter supports blobs as fields - * - * @return bool - */ - public function hasBlobDataType() - { - return true; - } + /** + * Returns true if this adapter supports blobs as fields + * + * @return bool + */ + public function hasBlobDataType() + { + return true; + } - /** - * Returns true if this adapter supports bulk loading - * - * @return bool - */ - public function hasBulkLoader() - { - return true; - } + /** + * Returns true if this adapter supports bulk loading + * + * @return bool + */ + public function hasBulkLoader() + { + return true; + } - /** - * Test error number - * - * @param Exception $e - * @param string $errno - * @return bool - */ - 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; - } + /** + * Test error number + * + * @param Exception $e + * @param string $errno + * @return bool + */ + 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; - } + return mysqli_errno($this->_connection) == $errno; + } - /** - * Execute unprepared SQL query and throw away the result - * - * Workaround some SQL statements not compatible with prepare(). - * See http://framework.zend.com/issues/browse/ZF-1398 - * - * @param string $sqlQuery - * @return int Number of rows affected (SELECT/INSERT/UPDATE/DELETE) - */ - public function exec( $sqlQuery ) - { - $rc = mysqli_query($this->_connection, $sqlQuery); - $rowsAffected = mysqli_affected_rows($this->_connection); - if(!is_bool($rc)) - { - mysqli_free_result($rc); - } - return $rowsAffected; - } + /** + * Execute unprepared SQL query and throw away the result + * + * Workaround some SQL statements not compatible with prepare(). + * See http://framework.zend.com/issues/browse/ZF-1398 + * + * @param string $sqlQuery + * @return int Number of rows affected (SELECT/INSERT/UPDATE/DELETE) + */ + public function exec($sqlQuery) + { + $rc = mysqli_query($this->_connection, $sqlQuery); + $rowsAffected = mysqli_affected_rows($this->_connection); + if (!is_bool($rc)) { + mysqli_free_result($rc); + } + return $rowsAffected; + } - /** - * Is the connection character set equal to utf8? - * - * @return bool - */ - public function isConnectionUTF8() - { - $charset = mysqli_character_set_name($this->_connection); - return $charset === 'utf8'; - } + /** + * Is the connection character set equal to utf8? + * + * @return bool + */ + public function isConnectionUTF8() + { + $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; - } + /** + * 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 index 56fe7691ac..6754962e6d 100644 --- a/core/Db/Adapter/Pdo/Mssql.php +++ b/core/Db/Adapter/Pdo/Mssql.php @@ -15,264 +15,245 @@ */ class Piwik_Db_Pdo_Mssql extends Zend_Db_Adapter_Pdo_Mssql implements Piwik_Db_Adapter_Interface { - /** - * Returns connection handle - * - * @throws Zend_Db_Adapter_Exception - * @return resource - */ - public function getConnection() - { - // if we already have a PDO object, no need to re-connect. - if ($this->_connection) - { - return $this->_connection; - } + /** + * Returns connection handle + * + * @throws Zend_Db_Adapter_Exception + * @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(); + $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 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'); - } + // 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); + // 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; - } + // 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"]; - } + 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 )); + $this->_connection = new PDO("sqlsrv:$serverName", $uid, $pwd, array('Database' => $database)); - if( $this->_connection === false ) - { - die( self::FormatErrors( sqlsrv_errors() ) ); - } + 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->_connection = new PDO( + $dsn, + $this->_config['username'], + $this->_config['password'], + $this->_config['driver_options'] + ); + */ - $this->_profiler->queryEnd($q); + $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); + // 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); + // 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); - } - } + 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(); - } + /** + * Reset the configuration variables in this adapter. + */ + public function resetConfig() + { + $this->_config = array(); + } - /** - * Return default port. - * - * @return int - */ - public static function getDefaultPort() - { - return 1433; - } + /** + * Return default port. + * + * @return int + */ + public static function getDefaultPort() + { + return 1433; + } - /** - * Check MSSQL version - * - * @throws Exception - */ - public function checkServerVersion() - { - $serverVersion = $this->getServerVersion(); - $requiredVersion = Piwik_Config::getInstance()->General['minimum_mssql_version']; - if(version_compare($serverVersion, $requiredVersion) === -1) - { - throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MSSQL', $serverVersion, $requiredVersion))); - } + /** + * Check MSSQL version + * + * @throws Exception + */ + public function checkServerVersion() + { + $serverVersion = $this->getServerVersion(); + $requiredVersion = Piwik_Config::getInstance()->General['minimum_mssql_version']; + if (version_compare($serverVersion, $requiredVersion) === -1) { + throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MSSQL', $serverVersion, $requiredVersion))); + } - } + } - /** - * Returns the Mssql server version - * - * @return null|string - */ - 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) - { - } + /** + * Returns the Mssql server version + * + * @return null|string + */ + 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; - } + return null; + } - /** - * Check client version compatibility against database server - * - * @throws Exception - */ - 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))); - } - } + /** + * Check client version compatibility against database server + * + * @throws Exception + */ + 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'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; - } + /** + * Returns true if this adapter supports blobs as fields + * + * @return bool + */ + public function hasBlobDataType() + { + return true; + } - /** - * Returns true if this adapter supports bulk loading - * - * @return bool - */ - public function hasBulkLoader() - { - /** - * BULK INSERT doesn't have a way to escape a terminator that appears in a value - * - * @link http://msdn.microsoft.com/en-us/library/ms188365.aspx - */ - return false; - } + /** + * Returns true if this adapter supports bulk loading + * + * @return bool + */ + public function hasBulkLoader() + { + /** + * BULK INSERT doesn't have a way to escape a terminator that appears in a value + * + * @link http://msdn.microsoft.com/en-us/library/ms188365.aspx + */ + return false; + } - /** - * 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; - } + /** + * 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; - } + /** + * 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 - * - * @throws Exception - * @return string - */ - public function getClientVersion() - { - $this->_connect(); - try - { - $version = $this->_connection->getAttribute(PDO::ATTR_CLIENT_VERSION); - $requiredVersion = Piwik_Config::getInstance()->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 - } + /** + * Retrieve client version in PHP style + * + * @throws Exception + * @return string + */ + public function getClientVersion() + { + $this->_connect(); + try { + $version = $this->_connection->getAttribute(PDO::ATTR_CLIENT_VERSION); + $requiredVersion = Piwik_Config::getInstance()->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; - } + return null; + } } diff --git a/core/Db/Adapter/Pdo/Mysql.php b/core/Db/Adapter/Pdo/Mysql.php index d8753016c1..515019cb4f 100644 --- a/core/Db/Adapter/Pdo/Mysql.php +++ b/core/Db/Adapter/Pdo/Mysql.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,212 +15,206 @@ */ class Piwik_Db_Adapter_Pdo_Mysql extends Zend_Db_Adapter_Pdo_Mysql implements Piwik_Db_Adapter_Interface { - /** - * Constructor - * - * @param array|Zend_Config $config database configuration - */ - public function __construct($config) - { - // Enable LOAD DATA INFILE - if(defined('PDO::MYSQL_ATTR_LOCAL_INFILE')) - { - $config['driver_options'][PDO::MYSQL_ATTR_LOCAL_INFILE] = true; - } - parent::__construct($config); - } - - /** - * Returns connection handle - * - * @return resource - */ - public function getConnection() - { - if($this->_connection) - { - return $this->_connection; - } - - $this->_connect(); - - /** - * Before MySQL 5.1.17, server-side prepared statements - * do not use the query cache. - * @see http://dev.mysql.com/doc/refman/5.1/en/query-cache-operation.html - * - * MySQL also does not support preparing certain DDL and SHOW - * statements. - * @see http://framework.zend.com/issues/browse/ZF-1398 - */ - $this->_connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); - $this->_connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); - - return $this->_connection; - } - - /** - * Reset the configuration variables in this adapter. - */ - public function resetConfig() - { - $this->_config = array(); - } - - /** - * Return default port. - * - * @return int - */ - public static function getDefaultPort() - { - return 3306; - } - - /** - * Check MySQL version - * - * @throws Exception - */ - public function checkServerVersion() - { - $serverVersion = $this->getServerVersion(); - $requiredVersion = Piwik_Config::getInstance()->General['minimum_mysql_version']; - if(version_compare($serverVersion, $requiredVersion) === -1) - { - throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MySQL', $serverVersion, $requiredVersion))); - } - } - - /** - * Check client version compatibility against database server - * - * @throws Exception - */ - public function checkClientVersion() - { - $serverVersion = $this->getServerVersion(); - $clientVersion = $this->getClientVersion(); - // incompatible change to DECIMAL implementation in 5.0.3 - if(version_compare($serverVersion, '5.0.3') >= 0 - && version_compare($clientVersion, '5.0.3') < 0) - { - throw new Exception(Piwik_TranslateException('General_ExceptionIncompatibleClientServerVersions', array('MySQL', $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_mysql', $extensions) && in_array('mysql', PDO::getAvailableDrivers()); - } - - /** - * Returns true if this adapter supports blobs as fields - * - * @return bool - */ - public function hasBlobDataType() - { - return true; - } - - /** - * Returns true if this adapter supports bulk loading - * - * @return bool - */ - public function hasBulkLoader() - { - 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() - { - $charsetInfo = $this->fetchAll('SHOW VARIABLES LIKE ?', array('character_set_connection')); - if(empty($charsetInfo)) { - return false; - } - $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; - } - - private $cachePreparedStatement = array(); - - /** - * Prepares and executes an SQL statement with bound data. - * Caches prepared statements to avoid preparing the same query more than once - * - * @param string|Zend_Db_Select $sql The SQL statement with placeholders. - * @param array $bind An array of data to bind to the placeholders. - * @return Zend_Db_Statement_Interface - */ - public function query($sql, $bind = array()) - { - if(!is_string($sql)) - { - return parent::query($sql, $bind); - } - - if(isset($this->cachePreparedStatement[$sql])) - { - if (!is_array($bind)) { - $bind = array($bind); - } - - $stmt = $this->cachePreparedStatement[$sql]; - $stmt->execute($bind); - return $stmt; - } - - $stmt = parent::query($sql, $bind); - $this->cachePreparedStatement[$sql] = $stmt; - return $stmt; - } + /** + * Constructor + * + * @param array|Zend_Config $config database configuration + */ + public function __construct($config) + { + // Enable LOAD DATA INFILE + if (defined('PDO::MYSQL_ATTR_LOCAL_INFILE')) { + $config['driver_options'][PDO::MYSQL_ATTR_LOCAL_INFILE] = true; + } + parent::__construct($config); + } + + /** + * Returns connection handle + * + * @return resource + */ + public function getConnection() + { + if ($this->_connection) { + return $this->_connection; + } + + $this->_connect(); + + /** + * Before MySQL 5.1.17, server-side prepared statements + * do not use the query cache. + * @see http://dev.mysql.com/doc/refman/5.1/en/query-cache-operation.html + * + * MySQL also does not support preparing certain DDL and SHOW + * statements. + * @see http://framework.zend.com/issues/browse/ZF-1398 + */ + $this->_connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + $this->_connection->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true); + + return $this->_connection; + } + + /** + * Reset the configuration variables in this adapter. + */ + public function resetConfig() + { + $this->_config = array(); + } + + /** + * Return default port. + * + * @return int + */ + public static function getDefaultPort() + { + return 3306; + } + + /** + * Check MySQL version + * + * @throws Exception + */ + public function checkServerVersion() + { + $serverVersion = $this->getServerVersion(); + $requiredVersion = Piwik_Config::getInstance()->General['minimum_mysql_version']; + if (version_compare($serverVersion, $requiredVersion) === -1) { + throw new Exception(Piwik_TranslateException('General_ExceptionDatabaseVersion', array('MySQL', $serverVersion, $requiredVersion))); + } + } + + /** + * Check client version compatibility against database server + * + * @throws Exception + */ + public function checkClientVersion() + { + $serverVersion = $this->getServerVersion(); + $clientVersion = $this->getClientVersion(); + // incompatible change to DECIMAL implementation in 5.0.3 + if (version_compare($serverVersion, '5.0.3') >= 0 + && version_compare($clientVersion, '5.0.3') < 0 + ) { + throw new Exception(Piwik_TranslateException('General_ExceptionIncompatibleClientServerVersions', array('MySQL', $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_mysql', $extensions) && in_array('mysql', PDO::getAvailableDrivers()); + } + + /** + * Returns true if this adapter supports blobs as fields + * + * @return bool + */ + public function hasBlobDataType() + { + return true; + } + + /** + * Returns true if this adapter supports bulk loading + * + * @return bool + */ + public function hasBulkLoader() + { + 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() + { + $charsetInfo = $this->fetchAll('SHOW VARIABLES LIKE ?', array('character_set_connection')); + if (empty($charsetInfo)) { + return false; + } + $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; + } + + private $cachePreparedStatement = array(); + + /** + * Prepares and executes an SQL statement with bound data. + * Caches prepared statements to avoid preparing the same query more than once + * + * @param string|Zend_Db_Select $sql The SQL statement with placeholders. + * @param array $bind An array of data to bind to the placeholders. + * @return Zend_Db_Statement_Interface + */ + public function query($sql, $bind = array()) + { + if (!is_string($sql)) { + return parent::query($sql, $bind); + } + + if (isset($this->cachePreparedStatement[$sql])) { + if (!is_array($bind)) { + $bind = array($bind); + } + + $stmt = $this->cachePreparedStatement[$sql]; + $stmt->execute($bind); + return $stmt; + } + + $stmt = parent::query($sql, $bind); + $this->cachePreparedStatement[$sql] = $stmt; + return $stmt; + } } diff --git a/core/Db/Adapter/Pdo/Pgsql.php b/core/Db/Adapter/Pdo/Pgsql.php index 035a508892..c6b07bedf3 100644 --- a/core/Db/Adapter/Pdo/Pgsql.php +++ b/core/Db/Adapter/Pdo/Pgsql.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,165 +15,163 @@ */ class Piwik_Db_Adapter_Pdo_Pgsql extends Zend_Db_Adapter_Pdo_Pgsql implements Piwik_Db_Adapter_Interface { - /** - * Reset the configuration variables in this adapter. - */ - public function resetConfig() - { - $this->_config = array(); - } - - /** - * Return default port. - * - * @return int - */ - public static function getDefaultPort() - { - return 5432; - } - - /** - * Check PostgreSQL version - * - * @throws Exception - */ - public function checkServerVersion() - { - $databaseVersion = $this->getServerVersion(); - $requiredVersion = Piwik_Config::getInstance()->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() - { - } - - /** - * 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_pgsql', $extensions); - } - - /** - * Returns true if this adapter supports blobs as fields - * - * @return bool - */ - public function hasBlobDataType() - { - // large objects must be loaded from a file using a non-SQL API - // and then referenced by the object ID (oid); - // the alternative, bytea fields, incur a space and time - // penalty for encoding/decoding - return false; - } - - /** - * Returns true if this adapter supports bulk loading - * - * @return bool - */ - public function hasBulkLoader() - { - /** - * COPY ? - * - * @link http://www.postgresql.org/docs/current/interactive/sql-copy.html - */ - return false; - } - - /** - * Test error number - * - * @param Exception $e - * @param string $errno - * @return bool - */ - public function isErrNo($e, $errno) - { - // map MySQL driver-specific error codes to PostgreSQL SQLSTATE - $map = array( - // MySQL: Unknown database '%s' - // PostgreSQL: database "%s" does not exist - '1049' => '08006', - - // MySQL: Table '%s' already exists - // PostgreSQL: relation "%s" already exists - '1050' => '42P07', - - // MySQL: Unknown column '%s' in '%s' - // PostgreSQL: column "%s" does not exist - '1054' => '42703', - - // MySQL: Duplicate column name '%s' - // 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', - - // MySQL: Can't DROP '%s'; check that column/key exists - // PostgreSQL: index "%s" does not exist - '1091' => '42704', - - // MySQL: Table '%s.%s' doesn't exist - // PostgreSQL: relation "%s" does not exist - '1146' => '42P01', - ); - - if(preg_match('/([0-9]{2}[0-9P][0-9]{2})/', $e->getMessage(), $match)) - { - return $match[1] == $map[$errno]; - } - return false; - } - - /** - * Is the connection character set equal to utf8? - * - * @return bool - */ - public function isConnectionUTF8() - { - $charset = $this->fetchOne('SHOW client_encoding'); - return strtolower($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; - } + /** + * Reset the configuration variables in this adapter. + */ + public function resetConfig() + { + $this->_config = array(); + } + + /** + * Return default port. + * + * @return int + */ + public static function getDefaultPort() + { + return 5432; + } + + /** + * Check PostgreSQL version + * + * @throws Exception + */ + public function checkServerVersion() + { + $databaseVersion = $this->getServerVersion(); + $requiredVersion = Piwik_Config::getInstance()->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() + { + } + + /** + * 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_pgsql', $extensions); + } + + /** + * Returns true if this adapter supports blobs as fields + * + * @return bool + */ + public function hasBlobDataType() + { + // large objects must be loaded from a file using a non-SQL API + // and then referenced by the object ID (oid); + // the alternative, bytea fields, incur a space and time + // penalty for encoding/decoding + return false; + } + + /** + * Returns true if this adapter supports bulk loading + * + * @return bool + */ + public function hasBulkLoader() + { + /** + * COPY ? + * + * @link http://www.postgresql.org/docs/current/interactive/sql-copy.html + */ + return false; + } + + /** + * Test error number + * + * @param Exception $e + * @param string $errno + * @return bool + */ + public function isErrNo($e, $errno) + { + // map MySQL driver-specific error codes to PostgreSQL SQLSTATE + $map = array( + // MySQL: Unknown database '%s' + // PostgreSQL: database "%s" does not exist + '1049' => '08006', + + // MySQL: Table '%s' already exists + // PostgreSQL: relation "%s" already exists + '1050' => '42P07', + + // MySQL: Unknown column '%s' in '%s' + // PostgreSQL: column "%s" does not exist + '1054' => '42703', + + // MySQL: Duplicate column name '%s' + // 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', + + // MySQL: Can't DROP '%s'; check that column/key exists + // PostgreSQL: index "%s" does not exist + '1091' => '42704', + + // MySQL: Table '%s.%s' doesn't exist + // PostgreSQL: relation "%s" does not exist + '1146' => '42P01', + ); + + if (preg_match('/([0-9]{2}[0-9P][0-9]{2})/', $e->getMessage(), $match)) { + return $match[1] == $map[$errno]; + } + return false; + } + + /** + * Is the connection character set equal to utf8? + * + * @return bool + */ + public function isConnectionUTF8() + { + $charset = $this->fetchOne('SHOW client_encoding'); + return strtolower($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/Schema.php b/core/Db/Schema.php index fbce36722d..8cf56ccb85 100644 --- a/core/Db/Schema.php +++ b/core/Db/Schema.php @@ -19,254 +19,245 @@ */ class Piwik_Db_Schema { - /** - * Singleton instance - * - * @var Piwik_Db_Schema - */ - static private $instance = null; + /** + * Singleton instance + * + * @var Piwik_Db_Schema + */ + static private $instance = null; - /** - * Type of database schema - * - * @var string - */ - private $schema = null; + /** + * Type of database schema + * + * @var string + */ + private $schema = null; - /** - * Returns the singleton Piwik_Db_Schema - * - * @return Piwik_Db_Schema - */ - static public function getInstance() - { - if (self::$instance === null) - { - self::$instance = new self; - } - return self::$instance; - } + /** + * Returns the singleton Piwik_Db_Schema + * + * @return Piwik_Db_Schema + */ + static public function getInstance() + { + if (self::$instance === null) { + self::$instance = new self; + } + 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 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 - * - * @param string $adapterName - * @return array - */ - public static function getSchemas($adapterName) - { - static $allSchemaNames = array( - // MySQL storage engines - 'MYSQL' => array( - 'Myisam', + /** + * Get list of schemas + * + * @param string $adapterName + * @return array + */ + public static function getSchemas($adapterName) + { + static $allSchemaNames = array( + // MySQL storage engines + 'MYSQL' => array( + 'Myisam', // 'Innodb', // 'Infinidb', - ), + ), - // Microsoft SQL Server + // Microsoft SQL Server // 'MSSQL' => array( 'Mssql' ), - // PostgreSQL + // PostgreSQL // 'PDO_PGSQL' => array( 'Pgsql' ), - // IBM DB2 + // IBM DB2 // 'IBM' => array( 'Ibm' ), - // Oracle + // Oracle // 'OCI' => array( 'Oci' ), - ); + ); - $adapterName = strtoupper($adapterName); - switch($adapterName) - { - case 'PDO_MYSQL': - case 'MYSQLI': - $adapterName = 'MYSQL'; - break; + $adapterName = strtoupper($adapterName); + switch ($adapterName) { + case 'PDO_MYSQL': + case 'MYSQLI': + $adapterName = 'MYSQL'; + break; - case 'PDO_MSSQL': - case 'SQLSRV': - $adapterName = 'MSSQL'; - break; + case 'PDO_MSSQL': + case 'SQLSRV': + $adapterName = 'MSSQL'; + break; - case 'PDO_IBM': - case 'DB2': - $adapterName = 'IBM'; - break; + case 'PDO_IBM': + case 'DB2': + $adapterName = 'IBM'; + break; - case 'PDO_OCI': - case 'ORACLE': - $adapterName = 'OCI'; - break; - } - $schemaNames = $allSchemaNames[$adapterName]; + case 'PDO_OCI': + case 'ORACLE': + $adapterName = 'OCI'; + break; + } + $schemaNames = $allSchemaNames[$adapterName]; - $schemas = array(); + $schemas = array(); - foreach($schemaNames as $schemaName) - { - $className = 'Piwik_Db_Schema_'.$schemaName; - if(call_user_func(array($className, 'isAvailable'))) - { - $schemas[] = $schemaName; - } - } + foreach ($schemaNames as $schemaName) { + $className = 'Piwik_Db_Schema_' . $schemaName; + if (call_user_func(array($className, 'isAvailable'))) { + $schemas[] = $schemaName; + } + } - return $schemas; - } + return $schemas; + } - /** - * Load schema - */ - private function loadSchema() - { - $schema = null; - Piwik_PostEvent('Schema.loadSchema', $schema); - if($schema === null) - { - $config = Piwik_Config::getInstance(); - $dbInfos = $config->database; - if(isset($dbInfos['schema'])) - { - $schemaName = $dbInfos['schema']; - } - else - { - $schemaName = 'Myisam'; - } - $className = self::getSchemaClassName($schemaName); - $schema = new $className(); - } - $this->schema = $schema; - } + /** + * Load schema + */ + private function loadSchema() + { + $schema = null; + Piwik_PostEvent('Schema.loadSchema', $schema); + if ($schema === null) { + $config = Piwik_Config::getInstance(); + $dbInfos = $config->database; + 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; - } + /** + * 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 - * - * @param string $tableName name of the table to create - * @return string SQL - */ - public function getTableCreateSql( $tableName ) - { - return $this->getSchema()->getTableCreateSql($tableName); - } + /** + * Get the SQL to create a specific Piwik table + * + * @param string $tableName name of the table to create + * @return string SQL + */ + public function getTableCreateSql($tableName) + { + return $this->getSchema()->getTableCreateSql($tableName); + } - /** - * Get the SQL to create Piwik tables - * - * @return array array of strings containing SQL - */ - public function getTablesCreateSql() - { - return $this->getSchema()->getTablesCreateSql(); - } + /** + * Get the SQL to create Piwik tables + * + * @return array array of strings containing SQL + */ + public function getTablesCreateSql() + { + return $this->getSchema()->getTablesCreateSql(); + } - /** - * Create database - * - * @param null|string $dbName database name to create - */ - public function createDatabase( $dbName = null ) - { - $this->getSchema()->createDatabase($dbName); - } + /** + * Create database + * + * @param null|string $dbName database name to create + */ + public function createDatabase($dbName = null) + { + $this->getSchema()->createDatabase($dbName); + } - /** - * Drop database - */ - public function dropDatabase() - { - $this->getSchema()->dropDatabase(); - } + /** + * Drop database + */ + public function dropDatabase() + { + $this->getSchema()->dropDatabase(); + } - /** - * Create all tables - */ - public function createTables() - { - $this->getSchema()->createTables(); - } + /** + * 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(); - } + /** + * 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(); - } + /** + * Truncate all tables + */ + public function truncateAllTables() + { + $this->getSchema()->truncateAllTables(); + } - /** - * Drop specific tables - * - * @param array $doNotDelete - */ - public function dropTables( $doNotDelete = array() ) - { - $this->getSchema()->dropTables($doNotDelete); - } + /** + * Drop specific tables + * + * @param array $doNotDelete + */ + 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(); - } + /** + * 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 - * @return array installed tables - */ - public function getTablesInstalled($forceReload = true) - { - return $this->getSchema()->getTablesInstalled($forceReload); - } + /** + * Get list of tables installed + * + * @param bool $forceReload Invalidate cache + * @return array installed tables + */ + public function getTablesInstalled($forceReload = true) + { + return $this->getSchema()->getTablesInstalled($forceReload); + } - /** - * Returns true if Piwik tables exist - * - * @return bool True if tables exist; false otherwise - */ - public function hasTables() - { - return $this->getSchema()->hasTables(); - } + /** + * Returns true if Piwik tables exist + * + * @return bool True if tables exist; false otherwise + */ + public function hasTables() + { + return $this->getSchema()->hasTables(); + } }
\ No newline at end of file diff --git a/core/Db/Schema/Interface.php b/core/Db/Schema/Interface.php index 6bada30a70..a56f79eb03 100644 --- a/core/Db/Schema/Interface.php +++ b/core/Db/Schema/Interface.php @@ -17,82 +17,82 @@ */ interface Piwik_Db_Schema_Interface { - /** - * Is this schema available? - * - * @return bool True if schema is available; false otherwise - */ - static public function isAvailable(); + /** + * Is this schema available? + * + * @return bool True if schema is available; false otherwise + */ + static public function isAvailable(); - /** - * Get the SQL to create a specific Piwik table - * - * @param string $tableName - * @return string SQL - */ - public function getTableCreateSql($tableName); + /** + * Get the SQL to create a specific Piwik table + * + * @param string $tableName + * @return string SQL + */ + public function getTableCreateSql($tableName); - /** - * Get the SQL to create Piwik tables - * - * @return array array of strings containing SQL - */ - public function getTablesCreateSql(); + /** + * Get the SQL to create Piwik tables + * + * @return array array of strings containing SQL + */ + public function getTablesCreateSql(); - /** - * Create database - * - * @param string $dbName Name of the database to create - */ - public function createDatabase( $dbName = null ); + /** + * Create database + * + * @param string $dbName Name of the database to create + */ + public function createDatabase($dbName = null); - /** - * Drop database - */ - public function dropDatabase(); + /** + * Drop database + */ + public function dropDatabase(); - /** - * Create all tables - */ - public function createTables(); + /** + * Create all tables + */ + public function createTables(); - /** - * Creates an entry in the User table for the "anonymous" user. - */ - public function createAnonymousUser(); + /** + * Creates an entry in the User table for the "anonymous" user. + */ + public function createAnonymousUser(); - /** - * Truncate all tables - */ - public function truncateAllTables(); + /** + * Truncate all tables + */ + public function truncateAllTables(); - /** - * Drop specific tables - * - * @param array $doNotDelete Names of tables to not delete - */ - public function dropTables( $doNotDelete = array() ); + /** + * Drop specific tables + * + * @param array $doNotDelete Names of tables to not delete + */ + public function dropTables($doNotDelete = array()); - /** - * Names of all the prefixed tables in piwik - * Doesn't use the DB - * - * @return array Table names - */ - public function getTablesNames(); + /** + * Names of all the prefixed tables in piwik + * Doesn't use the DB + * + * @return array Table names + */ + public function getTablesNames(); - /** - * Get list of tables installed - * - * @param bool $forceReload Invalidate cache - * @return array installed Tables - */ - public function getTablesInstalled($forceReload = true); + /** + * Get list of tables installed + * + * @param bool $forceReload Invalidate cache + * @return array installed Tables + */ + public function getTablesInstalled($forceReload = true); - /** - * Checks whether any table exists - * - * @return bool True if tables exist; false otherwise - */ - public function hasTables(); + /** + * Checks whether any table exists + * + * @return bool True if tables exist; false otherwise + */ + public function hasTables(); } diff --git a/core/Db/Schema/Myisam.php b/core/Db/Schema/Myisam.php index 1bbbb1940f..8ebacabcd9 100644 --- a/core/Db/Schema/Myisam.php +++ b/core/Db/Schema/Myisam.php @@ -17,45 +17,44 @@ */ 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(array_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 array of strings containing SQL - */ - public function getTablesCreateSql() - { - $config = Piwik_Config::getInstance(); - $prefixTables = $config->database['tables_prefix']; - $tables = array( - 'user' => "CREATE TABLE {$prefixTables}user ( + /** + * 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 (array_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 array of strings containing SQL + */ + public function getTablesCreateSql() + { + $config = Piwik_Config::getInstance(); + $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, @@ -67,7 +66,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'access' => "CREATE TABLE {$prefixTables}access ( + 'access' => "CREATE TABLE {$prefixTables}access ( login VARCHAR(100) NOT NULL, idsite INTEGER UNSIGNED NOT NULL, access VARCHAR(10) NULL, @@ -75,7 +74,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'site' => "CREATE TABLE {$prefixTables}site ( + 'site' => "CREATE TABLE {$prefixTables}site ( idsite INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(90) NOT NULL, main_url VARCHAR(255) NOT NULL, @@ -95,14 +94,14 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'site_url' => "CREATE TABLE {$prefixTables}site_url ( + '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` ( + 'goal' => " CREATE TABLE `{$prefixTables}goal` ( `idsite` int(11) NOT NULL, `idgoal` int(11) NOT NULL, `name` varchar(50) NOT NULL, @@ -117,7 +116,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'logger_message' => "CREATE TABLE {$prefixTables}logger_message ( + 'logger_message' => "CREATE TABLE {$prefixTables}logger_message ( idlogger_message INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, timestamp TIMESTAMP NULL, message TEXT NULL, @@ -125,7 +124,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'logger_api_call' => "CREATE TABLE {$prefixTables}logger_api_call ( + '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, @@ -139,7 +138,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'logger_error' => "CREATE TABLE {$prefixTables}logger_error ( + 'logger_error' => "CREATE TABLE {$prefixTables}logger_error ( idlogger_error INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, timestamp TIMESTAMP NULL, message TEXT NULL, @@ -151,7 +150,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'logger_exception' => "CREATE TABLE {$prefixTables}logger_exception ( + 'logger_exception' => "CREATE TABLE {$prefixTables}logger_exception ( idlogger_exception INTEGER UNSIGNED NOT NULL AUTO_INCREMENT, timestamp TIMESTAMP NULL, message TEXT NULL, @@ -163,7 +162,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'log_action' => "CREATE TABLE {$prefixTables}log_action ( + 'log_action' => "CREATE TABLE {$prefixTables}log_action ( idaction INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, name TEXT, hash INTEGER(10) UNSIGNED NOT NULL, @@ -174,7 +173,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'log_visit' => "CREATE TABLE {$prefixTables}log_visit ( + 'log_visit' => "CREATE TABLE {$prefixTables}log_visit ( idvisit INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, idsite INTEGER(10) UNSIGNED NOT NULL, idvisitor BINARY(8) NOT NULL, @@ -237,8 +236,8 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface INDEX index_idsite_idvisitor (idsite, idvisitor) ) DEFAULT CHARSET=utf8 ", - - 'log_conversion_item' => "CREATE TABLE `{$prefixTables}log_conversion_item` ( + + 'log_conversion_item' => "CREATE TABLE `{$prefixTables}log_conversion_item` ( idsite int(10) UNSIGNED NOT NULL, idvisitor BINARY(8) NOT NULL, server_time DATETIME NOT NULL, @@ -261,7 +260,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'log_conversion' => "CREATE TABLE `{$prefixTables}log_conversion` ( + 'log_conversion' => "CREATE TABLE `{$prefixTables}log_conversion` ( idvisit int(10) unsigned NOT NULL, idsite int(10) unsigned NOT NULL, idvisitor BINARY(8) NOT NULL, @@ -309,7 +308,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'log_link_visit_action' => "CREATE TABLE {$prefixTables}log_link_visit_action ( + 'log_link_visit_action' => "CREATE TABLE {$prefixTables}log_link_visit_action ( idlink_va INTEGER(11) UNSIGNED NOT NULL AUTO_INCREMENT, idsite int(10) UNSIGNED NOT NULL, idvisitor BINARY(8) NOT NULL, @@ -337,7 +336,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'log_profiling' => "CREATE TABLE {$prefixTables}log_profiling ( + 'log_profiling' => "CREATE TABLE {$prefixTables}log_profiling ( query TEXT NOT NULL, count INTEGER UNSIGNED NULL, sum_time_ms FLOAT NULL, @@ -345,7 +344,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'option' => "CREATE TABLE `{$prefixTables}option` ( + 'option' => "CREATE TABLE `{$prefixTables}option` ( option_name VARCHAR( 255 ) NOT NULL, option_value LONGTEXT NOT NULL, autoload TINYINT NOT NULL DEFAULT '1', @@ -354,7 +353,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'session' => "CREATE TABLE {$prefixTables}session ( + 'session' => "CREATE TABLE {$prefixTables}session ( id CHAR(32) NOT NULL, modified INTEGER, lifetime INTEGER, @@ -363,7 +362,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'archive_numeric' => "CREATE TABLE {$prefixTables}archive_numeric ( + 'archive_numeric' => "CREATE TABLE {$prefixTables}archive_numeric ( idarchive INTEGER UNSIGNED NOT NULL, name VARCHAR(255) NOT NULL, idsite INTEGER UNSIGNED NULL, @@ -378,7 +377,7 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface ) DEFAULT CHARSET=utf8 ", - 'archive_blob' => "CREATE TABLE {$prefixTables}archive_blob ( + 'archive_blob' => "CREATE TABLE {$prefixTables}archive_blob ( idarchive INTEGER UNSIGNED NOT NULL, name VARCHAR(255) NOT NULL, idsite INTEGER UNSIGNED NULL, @@ -391,191 +390,183 @@ class Piwik_Db_Schema_Myisam implements Piwik_Db_Schema_Interface INDEX index_period_archived(period, ts_archived) ) DEFAULT CHARSET=utf8 ", - ); - return $tables; - } - - /** - * Get the SQL to create a specific Piwik table - * - * @param string $tableName - * @throws Exception - * @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 = Piwik_Config::getInstance(); - $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 - * @return array installed Tables - */ - public function getTablesInstalled($forceReload = true) - { - if(is_null($this->tablesInstalled) - || $forceReload === true) - { - $db = Zend_Registry::get('db'); - $config = Piwik_Config::getInstance(); - $prefixTables = $config->database['tables_prefix']; - - // '_' matches any character; force it to be literal - $prefixTables = str_replace('_', '\_', $prefixTables); - - $allTables = $db->fetchCol("SHOW TABLES LIKE '".$prefixTables."%'"); - - // 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) - $allArchiveNumeric = $db->fetchCol("SHOW TABLES LIKE '".$prefixTables."archive_numeric%'"); - $allArchiveBlob = $db->fetchCol("SHOW TABLES LIKE '".$prefixTables."archive_blob%'"); - - $allTablesReallyInstalled = array_merge($tablesInstalled, $allArchiveNumeric, $allArchiveBlob); - - $this->tablesInstalled = $allTablesReallyInstalled; - } - return $this->tablesInstalled; - } - - /** - * Checks whether any table exists - * - * @return bool True if tables exist; false otherwise - */ - public function hasTables() - { - return count($this->getTablesInstalled()) != 0; - } - - /** - * Create database - * - * @param string $dbName Name of the database to create - */ - public function createDatabase( $dbName = null ) - { - if(is_null($dbName)) - { - $dbName = Piwik_Config::getInstance()->database['dbname']; - } - Piwik_Exec("CREATE DATABASE IF NOT EXISTS ".$dbName." DEFAULT CHARACTER SET utf8"); - } - - /** - * Drop database - */ - public function dropDatabase() - { - $dbName = Piwik_Config::getInstance()->database['dbname']; - Piwik_Exec("DROP DATABASE IF EXISTS " . $dbName); - } - - /** - * Create all tables - */ - public function createTables() - { - $db = Zend_Registry::get('db'); - $config = Piwik_Config::getInstance(); - $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`"); - } - } - } + ); + return $tables; + } + + /** + * Get the SQL to create a specific Piwik table + * + * @param string $tableName + * @throws Exception + * @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 = Piwik_Config::getInstance(); + $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 + * @return array installed Tables + */ + public function getTablesInstalled($forceReload = true) + { + if (is_null($this->tablesInstalled) + || $forceReload === true + ) { + $db = Zend_Registry::get('db'); + $config = Piwik_Config::getInstance(); + $prefixTables = $config->database['tables_prefix']; + + // '_' matches any character; force it to be literal + $prefixTables = str_replace('_', '\_', $prefixTables); + + $allTables = $db->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "%'"); + + // 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) + $allArchiveNumeric = $db->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "archive_numeric%'"); + $allArchiveBlob = $db->fetchCol("SHOW TABLES LIKE '" . $prefixTables . "archive_blob%'"); + + $allTablesReallyInstalled = array_merge($tablesInstalled, $allArchiveNumeric, $allArchiveBlob); + + $this->tablesInstalled = $allTablesReallyInstalled; + } + return $this->tablesInstalled; + } + + /** + * Checks whether any table exists + * + * @return bool True if tables exist; false otherwise + */ + public function hasTables() + { + return count($this->getTablesInstalled()) != 0; + } + + /** + * Create database + * + * @param string $dbName Name of the database to create + */ + public function createDatabase($dbName = null) + { + if (is_null($dbName)) { + $dbName = Piwik_Config::getInstance()->database['dbname']; + } + Piwik_Exec("CREATE DATABASE IF NOT EXISTS " . $dbName . " DEFAULT CHARACTER SET utf8"); + } + + /** + * Drop database + */ + public function dropDatabase() + { + $dbName = Piwik_Config::getInstance()->database['dbname']; + Piwik_Exec("DROP DATABASE IF EXISTS " . $dbName); + } + + /** + * Create all tables + */ + public function createTables() + { + $db = Zend_Registry::get('db'); + $config = Piwik_Config::getInstance(); + $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/ErrorHandler.php b/core/ErrorHandler.php index cfb342babb..f2bcde168c 100644 --- a/core/ErrorHandler.php +++ b/core/ErrorHandler.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,75 +12,68 @@ /** * Error handler used to display nicely errors in Piwik * - * @param int $errno Error number - * @param string $errstr Error message - * @param string $errfile File name - * @param int $errline Line number + * @param int $errno Error number + * @param string $errstr Error message + * @param string $errfile File name + * @param int $errline Line number * @return void */ function Piwik_ErrorHandler($errno, $errstr, $errfile, $errline) { - // if the error has been suppressed by the @ we don't handle the error - if( error_reporting() == 0 ) - { - return; - } + // if the error has been suppressed by the @ we don't handle the error + if (error_reporting() == 0) { + return; + } + + if (function_exists('debug_backtrace')) { + $backtrace = ''; + $bt = @debug_backtrace(); + if ($bt !== null && isset($bt[0])) { + foreach ($bt as $i => $debug) { + $backtrace .= "#$i " + . (isset($debug['class']) ? $debug['class'] : '') + . (isset($debug['type']) ? $debug['type'] : '') + . (isset($debug['function']) ? $debug['function'] : '') + . '(...) called at [' + . (isset($debug['file']) ? $debug['file'] : '') . ':' + . (isset($debug['line']) ? $debug['line'] : '') . ']' . "\n"; + } + } + } else { + ob_start(); + @debug_print_backtrace(); + $backtrace = ob_get_contents(); + ob_end_clean(); + } - if(function_exists('debug_backtrace')) - { - $backtrace = ''; - $bt = @debug_backtrace(); - if($bt !== null && isset($bt[0])) - { - foreach($bt as $i => $debug) - { - $backtrace .= "#$i " - .(isset($debug['class']) ? $debug['class'] : '') - .(isset($debug['type']) ? $debug['type'] : '') - .(isset($debug['function']) ? $debug['function'] : '') - .'(...) called at [' - .(isset($debug['file']) ? $debug['file'] : '').':' - .(isset($debug['line']) ? $debug['line'] : '').']'."\n"; - } - } - } - else - { - ob_start(); - @debug_print_backtrace(); - $backtrace = ob_get_contents(); - ob_end_clean(); - } + try { + Zend_Registry::get('logger_error')->logEvent($errno, $errstr, $errfile, $errline, $backtrace); + } catch (Exception $e) { + // in case the error occurs before the logger creation, we simply display it + print("<pre>$errstr \nin '$errfile' at the line $errline\n\n$backtrace\n</pre>"); + exit; + } + switch ($errno) { + case E_ERROR: + case E_PARSE: + case E_CORE_ERROR: + case E_CORE_WARNING: + case E_COMPILE_ERROR: + case E_COMPILE_WARNING: + case E_USER_ERROR: + exit; + break; - try { - Zend_Registry::get('logger_error')->logEvent($errno, $errstr, $errfile, $errline, $backtrace); - } catch(Exception $e) { - // in case the error occurs before the logger creation, we simply display it - print("<pre>$errstr \nin '$errfile' at the line $errline\n\n$backtrace\n</pre>"); - exit; - } - switch($errno) - { - case E_ERROR: - case E_PARSE: - case E_CORE_ERROR: - case E_CORE_WARNING: - case E_COMPILE_ERROR: - case E_COMPILE_WARNING: - case E_USER_ERROR: - exit; - break; - - case E_WARNING: - case E_NOTICE: - case E_USER_WARNING: - case E_USER_NOTICE: - case E_STRICT: - case E_RECOVERABLE_ERROR: - case E_DEPRECATED: - case E_USER_DEPRECATED: - default: - // do not exit - break; - } + case E_WARNING: + case E_NOTICE: + case E_USER_WARNING: + case E_USER_NOTICE: + case E_STRICT: + case E_RECOVERABLE_ERROR: + case E_DEPRECATED: + case E_USER_DEPRECATED: + default: + // do not exit + break; + } } diff --git a/core/ExceptionHandler.php b/core/ExceptionHandler.php index 7bae1057b2..b3dd6692fe 100644 --- a/core/ExceptionHandler.php +++ b/core/ExceptionHandler.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,34 +12,33 @@ /** * Exception handler used to display nicely exceptions in Piwik * - * @param Exception $exception + * @param Exception $exception * @throws Exception */ -function Piwik_ExceptionHandler(Exception $exception) +function Piwik_ExceptionHandler(Exception $exception) { - try { - Zend_Registry::get('logger_exception')->logEvent($exception); - } catch(Exception $e) { - - if(Piwik_FrontController::shouldRethrowException()) - { - throw $exception; - } - - // case when the exception is raised before the logger being ready - // we handle the exception a la mano, but using the Logger formatting properties - $event = array(); - $event['errno'] = $exception->getCode(); - $event['message'] = $exception->getMessage(); - $event['errfile'] = $exception->getFile(); - $event['errline'] = $exception->getLine(); - $event['backtrace'] = $exception->getTraceAsString(); + try { + Zend_Registry::get('logger_exception')->logEvent($exception); + } catch (Exception $e) { + + if (Piwik_FrontController::shouldRethrowException()) { + throw $exception; + } + + // case when the exception is raised before the logger being ready + // we handle the exception a la mano, but using the Logger formatting properties + $event = array(); + $event['errno'] = $exception->getCode(); + $event['message'] = $exception->getMessage(); + $event['errfile'] = $exception->getFile(); + $event['errline'] = $exception->getLine(); + $event['backtrace'] = $exception->getTraceAsString(); + + $formatter = new Piwik_Log_Exception_Formatter_ScreenFormatter(); + + $message = $formatter->format($event); + $message .= "<br /><br />And this exception raised another exception \"" . $e->getMessage() . "\""; - $formatter = new Piwik_Log_Exception_Formatter_ScreenFormatter(); - - $message = $formatter->format($event); - $message .= "<br /><br />And this exception raised another exception \"". $e->getMessage()."\""; - - Piwik::exitWithErrorMessage( $message ); - } + Piwik::exitWithErrorMessage($message); + } } diff --git a/core/FrontController.php b/core/FrontController.php index 203a911f8a..8f136acd65 100644 --- a/core/FrontController.php +++ b/core/FrontController.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -22,356 +22,334 @@ require_once PIWIK_INCLUDE_PATH . '/core/Option.php'; * Front controller. * This is the class hit in the first place. * It dispatches the request to the right controller. - * + * * For a detailed explanation, see the documentation on http://piwik.org/docs/plugins/framework-overview - * + * * @package Piwik * @subpackage Piwik_FrontController */ class Piwik_FrontController { - /** - * Set to false and the Front Controller will not dispatch the request - * - * @var bool - */ - static public $enableDispatch = true; - - static private $instance = null; - - /** - * returns singleton - * - * @return Piwik_FrontController - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Dispatches the request to the right plugin and executes the requested action on the plugin controller. - * - * @throws Exception|Piwik_FrontController_PluginDeactivatedException in case the plugin doesn't exist, the action doesn't exist, there is not enough permission, etc. - * - * @param string $module - * @param string $action - * @param array $parameters - * @return mixed The returned value of the calls, often nothing as the module print but don't return data - * @see fetchDispatch() - */ - function dispatch( $module = null, $action = null, $parameters = null) - { - if( self::$enableDispatch === false) - { - return; - } - - if(is_null($module)) - { - $defaultModule = 'CoreHome'; - $module = Piwik_Common::getRequestVar('module', $defaultModule, 'string'); - } - - if(is_null($action)) - { - $action = Piwik_Common::getRequestVar('action', false); - } - - if(!Piwik_Session::isFileBasedSessions() - && ($module !== 'API' || ($action && $action !== 'index'))) - { - Piwik_Session::start(); - } - - if(is_null($parameters)) - { - $parameters = array(); - } - - if(!ctype_alnum($module)) - { - throw new Exception("Invalid module name '$module'"); - } - - if( ! Piwik_PluginsManager::getInstance()->isPluginActivated( $module )) - { - throw new Piwik_FrontController_PluginDeactivatedException($module); - } - - $controllerClassName = 'Piwik_'.$module.'_Controller'; - - // FrontController's autoloader - if(!class_exists($controllerClassName, false)) - { - $moduleController = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/Controller.php'; - if(!is_readable($moduleController)) - { - throw new Exception("Module controller $moduleController not found!"); - } - require_once $moduleController; // prefixed by PIWIK_INCLUDE_PATH - } - - $controller = new $controllerClassName(); - if($action === false) - { - $action = $controller->getDefaultAction(); - } - - if( !is_callable(array($controller, $action))) - { - throw new Exception("Action '$action' not found in the controller '$controllerClassName'."); - } - - // Generic hook that plugins can use to modify any input to the function, - // or even change the plugin being called - $params = array($controller, $action, $parameters); - Piwik_PostEvent('FrontController.dispatch', $params); - - try { - return call_user_func_array( array($params[0], $params[1] ), $params[2]); - } catch(Piwik_Access_NoAccessException $e) { - Piwik_PostEvent('FrontController.NoAccessException', $e); - } catch(Exception $e) { - $debugTrace = $e->getTraceAsString(); - $message = Piwik_Common::sanitizeInputValue($e->getMessage()); - Piwik_ExitWithMessage($message, '' /* $debugTrace */, true); - } - } - - /** - * Often plugins controller display stuff using echo/print. - * Using this function instead of dispatch() returns the output string form the actions calls. - * - * @param string $controllerName - * @param string $actionName - * @param array $parameters - * @return string - */ - function fetchDispatch( $controllerName = null, $actionName = null, $parameters = null) - { - ob_start(); - $output = $this->dispatch( $controllerName, $actionName, $parameters); - // if nothing returned we try to load something that was printed on the screen - if(empty($output)) - { - $output = ob_get_contents(); - } - ob_end_clean(); - return $output; - } - - /** - * Called at the end of the page generation - * - */ - function __destruct() - { - try { - Piwik::printSqlProfilingReportZend(); - Piwik::printQueryCount(); - Piwik::printTimer(); - } catch(Exception $e) {} - } - - // Should we show exceptions messages directly rather than display an html error page? - public static function shouldRethrowException() - { - // If we are in no dispatch mode, eg. a script reusing Piwik libs, - // then we should return the exception directly, rather than trigger the event "bad config file" - // which load the HTML page of the installer with the error. - // This is at least required for misc/cron/archive.php and useful to all other scripts - return (defined('PIWIK_ENABLE_DISPATCH') && !PIWIK_ENABLE_DISPATCH) - || Piwik_Common::isPhpCliMode() - || Piwik_Common::isArchivePhpTriggered() - ; - } - - /** - * Loads the config file and assign to the global registry - * This is overriden in tests to ensure test config file is used - */ - protected function createConfigObject() - { - $exceptionToThrow = false; - try { - Piwik::createConfigObject(); - } catch(Exception $e) { - Piwik_PostEvent('FrontController.NoConfigurationFile', $e, $info = array(), $pending = true); - $exceptionToThrow = $e; - } - return $exceptionToThrow; - } - - protected function createAccessObject() - { - Piwik::createAccessObject(); - } - - /** - * Must be called before dispatch() - * - checks that directories are writable, - * - loads the configuration file, - * - loads the plugin, - * - inits the DB connection, - * - etc. - * @throws Exception - * @throws Exception - * @throws bool|Exception - * @return - */ - function init() - { - static $initialized = false; - if($initialized) - { - return; - } - $initialized = true; - - - try { - Zend_Registry::set('timer', new Piwik_Timer); - - $directoriesToCheck = array( - '/tmp/', - '/tmp/templates_c/', - '/tmp/cache/', - '/tmp/assets/', - '/tmp/tcpdf/' - ); - - Piwik::checkDirectoriesWritableOrDie($directoriesToCheck); - Piwik_Common::assignCliParametersToRequest(); - - Piwik_Translate::getInstance()->loadEnglishTranslation(); - - $exceptionToThrow = $this->createConfigObject(); - - if(Piwik_Session::isFileBasedSessions()) - { - Piwik_Session::start(); - } - - $this->handleMaintenanceMode(); - $this->handleSSLRedirection(); - - $pluginsManager = Piwik_PluginsManager::getInstance(); - $pluginsToLoad = Piwik_Config::getInstance()->Plugins['Plugins']; - $pluginsManager->loadPlugins( $pluginsToLoad ); - - if($exceptionToThrow) - { - throw $exceptionToThrow; - } - - try { - Piwik::createDatabaseObject(); - } catch(Exception $e) { - if(self::shouldRethrowException()) - { - throw $e; - } - Piwik_PostEvent('FrontController.badConfigurationFile', $e, $info = array(), $pending = true); - throw $e; - } - - Piwik::createLogObject(); - - // creating the access object, so that core/Updates/* can enforce Super User and use some APIs - $this->createAccessObject(); - Piwik_PostEvent('FrontController.dispatchCoreAndPluginUpdatesScreen'); - - Piwik_PluginsManager::getInstance()->installLoadedPlugins(); - Piwik::install(); - - // ensure the current Piwik URL is known for later use - if(method_exists('Piwik', 'getPiwikUrl')) - { - $host = Piwik::getPiwikUrl(); - } - - Piwik_PostEvent('FrontController.initAuthenticationObject'); - try { - $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? + /** + * Set to false and the Front Controller will not dispatch the request + * + * @var bool + */ + static public $enableDispatch = true; + + static private $instance = null; + + /** + * returns singleton + * + * @return Piwik_FrontController + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Dispatches the request to the right plugin and executes the requested action on the plugin controller. + * + * @throws Exception|Piwik_FrontController_PluginDeactivatedException in case the plugin doesn't exist, the action doesn't exist, there is not enough permission, etc. + * + * @param string $module + * @param string $action + * @param array $parameters + * @return mixed The returned value of the calls, often nothing as the module print but don't return data + * @see fetchDispatch() + */ + function dispatch($module = null, $action = null, $parameters = null) + { + if (self::$enableDispatch === false) { + return; + } + + if (is_null($module)) { + $defaultModule = 'CoreHome'; + $module = Piwik_Common::getRequestVar('module', $defaultModule, 'string'); + } + + if (is_null($action)) { + $action = Piwik_Common::getRequestVar('action', false); + } + + if (!Piwik_Session::isFileBasedSessions() + && ($module !== 'API' || ($action && $action !== 'index')) + ) { + Piwik_Session::start(); + } + + if (is_null($parameters)) { + $parameters = array(); + } + + if (!ctype_alnum($module)) { + throw new Exception("Invalid module name '$module'"); + } + + if (!Piwik_PluginsManager::getInstance()->isPluginActivated($module)) { + throw new Piwik_FrontController_PluginDeactivatedException($module); + } + + $controllerClassName = 'Piwik_' . $module . '_Controller'; + + // FrontController's autoloader + if (!class_exists($controllerClassName, false)) { + $moduleController = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/Controller.php'; + if (!is_readable($moduleController)) { + throw new Exception("Module controller $moduleController not found!"); + } + require_once $moduleController; // prefixed by PIWIK_INCLUDE_PATH + } + + $controller = new $controllerClassName(); + if ($action === false) { + $action = $controller->getDefaultAction(); + } + + if (!is_callable(array($controller, $action))) { + throw new Exception("Action '$action' not found in the controller '$controllerClassName'."); + } + + // Generic hook that plugins can use to modify any input to the function, + // or even change the plugin being called + $params = array($controller, $action, $parameters); + Piwik_PostEvent('FrontController.dispatch', $params); + + try { + return call_user_func_array(array($params[0], $params[1]), $params[2]); + } catch (Piwik_Access_NoAccessException $e) { + Piwik_PostEvent('FrontController.NoAccessException', $e); + } catch (Exception $e) { + $debugTrace = $e->getTraceAsString(); + $message = Piwik_Common::sanitizeInputValue($e->getMessage()); + Piwik_ExitWithMessage($message, '' /* $debugTrace */, true); + } + } + + /** + * Often plugins controller display stuff using echo/print. + * Using this function instead of dispatch() returns the output string form the actions calls. + * + * @param string $controllerName + * @param string $actionName + * @param array $parameters + * @return string + */ + function fetchDispatch($controllerName = null, $actionName = null, $parameters = null) + { + ob_start(); + $output = $this->dispatch($controllerName, $actionName, $parameters); + // if nothing returned we try to load something that was printed on the screen + if (empty($output)) { + $output = ob_get_contents(); + } + ob_end_clean(); + return $output; + } + + /** + * Called at the end of the page generation + * + */ + function __destruct() + { + try { + Piwik::printSqlProfilingReportZend(); + Piwik::printQueryCount(); + Piwik::printTimer(); + } catch (Exception $e) { + } + } + + // Should we show exceptions messages directly rather than display an html error page? + public static function shouldRethrowException() + { + // If we are in no dispatch mode, eg. a script reusing Piwik libs, + // then we should return the exception directly, rather than trigger the event "bad config file" + // which load the HTML page of the installer with the error. + // This is at least required for misc/cron/archive.php and useful to all other scripts + return (defined('PIWIK_ENABLE_DISPATCH') && !PIWIK_ENABLE_DISPATCH) + || Piwik_Common::isPhpCliMode() + || Piwik_Common::isArchivePhpTriggered(); + } + + /** + * Loads the config file and assign to the global registry + * This is overriden in tests to ensure test config file is used + */ + protected function createConfigObject() + { + $exceptionToThrow = false; + try { + Piwik::createConfigObject(); + } catch (Exception $e) { + Piwik_PostEvent('FrontController.NoConfigurationFile', $e, $info = array(), $pending = true); + $exceptionToThrow = $e; + } + return $exceptionToThrow; + } + + protected function createAccessObject() + { + Piwik::createAccessObject(); + } + + /** + * Must be called before dispatch() + * - checks that directories are writable, + * - loads the configuration file, + * - loads the plugin, + * - inits the DB connection, + * - etc. + * @throws Exception + * @throws Exception + * @throws bool|Exception + * @return + */ + function init() + { + static $initialized = false; + if ($initialized) { + return; + } + $initialized = true; + + + try { + Zend_Registry::set('timer', new Piwik_Timer); + + $directoriesToCheck = array( + '/tmp/', + '/tmp/templates_c/', + '/tmp/cache/', + '/tmp/assets/', + '/tmp/tcpdf/' + ); + + Piwik::checkDirectoriesWritableOrDie($directoriesToCheck); + Piwik_Common::assignCliParametersToRequest(); + + Piwik_Translate::getInstance()->loadEnglishTranslation(); + + $exceptionToThrow = $this->createConfigObject(); + + if (Piwik_Session::isFileBasedSessions()) { + Piwik_Session::start(); + } + + $this->handleMaintenanceMode(); + $this->handleSSLRedirection(); + + $pluginsManager = Piwik_PluginsManager::getInstance(); + $pluginsToLoad = Piwik_Config::getInstance()->Plugins['Plugins']; + $pluginsManager->loadPlugins($pluginsToLoad); + + if ($exceptionToThrow) { + throw $exceptionToThrow; + } + + try { + Piwik::createDatabaseObject(); + } catch (Exception $e) { + if (self::shouldRethrowException()) { + throw $e; + } + Piwik_PostEvent('FrontController.badConfigurationFile', $e, $info = array(), $pending = true); + throw $e; + } + + Piwik::createLogObject(); + + // creating the access object, so that core/Updates/* can enforce Super User and use some APIs + $this->createAccessObject(); + Piwik_PostEvent('FrontController.dispatchCoreAndPluginUpdatesScreen'); + + Piwik_PluginsManager::getInstance()->installLoadedPlugins(); + Piwik::install(); + + // ensure the current Piwik URL is known for later use + if (method_exists('Piwik', 'getPiwikUrl')) { + $host = Piwik::getPiwikUrl(); + } + + Piwik_PostEvent('FrontController.initAuthenticationObject'); + try { + $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 /> under the <code>[Plugins]</code> section in your config/config.ini.php"); - } - Zend_Registry::get('access')->reloadAccess($authAdapter); - - // Force the auth to use the token_auth if specified, so that embed dashboard - // and all other non widgetized controller methods works fine - if(($token_auth = Piwik_Common::getRequestVar('token_auth', false, 'string')) !== false) - { - Piwik_API_Request::reloadAuthUsingTokenAuth(); - } - Piwik::raiseMemoryLimitIfNecessary(); - - Piwik_Translate::getInstance()->reloadLanguage(); - $pluginsManager->postLoadPlugins(); - - Piwik_PostEvent('FrontController.checkForUpdates'); - } catch(Exception $e) { - - if(self::shouldRethrowException()) - { - throw $e; - } - - Piwik_ExitWithMessage($e->getMessage(), false, true); - } - } - - protected function handleMaintenanceMode() - { - if(Piwik_Config::getInstance()->General['maintenance_mode'] == 1 - && !Piwik_Common::isPhpCliMode()) - { - $format = Piwik_Common::getRequestVar('format', ''); - - $message = "Piwik is in scheduled maintenance. Please come back later." - . " The administrator can disable maintenance by editing the file piwik/config/config.ini.php and removing the following: " - . " maintenance_mode=1 "; - if(Piwik_Config::getInstance()->Tracker['record_statistics'] == 0) - { - $message .= ' and record_statistics=0'; - } - - $exception = new Exception($message); - // extend explain how to re-enable - // show error message when record stats = 0 - if(empty($format)) - { - throw $exception; - } - $response = new Piwik_API_ResponseBuilder( $format ); - echo $response->getResponseException( $exception ); - exit; - } - } - - protected function handleSSLRedirection() - { - if(!Piwik_Common::isPhpCliMode() - && Piwik_Config::getInstance()->General['force_ssl'] == 1 - && !Piwik::isHttps() - // Specifically disable for the opt out iframe - && !(Piwik_Common::getRequestVar('module', '') == 'CoreAdminHome' - && Piwik_Common::getRequestVar('action', '') == 'optOut') - ) - { - $url = Piwik_Url::getCurrentUrl(); - $url = str_replace("http://", "https://", $url); - Piwik_Url::redirectToUrl($url); - } - } + } + Zend_Registry::get('access')->reloadAccess($authAdapter); + + // Force the auth to use the token_auth if specified, so that embed dashboard + // and all other non widgetized controller methods works fine + if (($token_auth = Piwik_Common::getRequestVar('token_auth', false, 'string')) !== false) { + Piwik_API_Request::reloadAuthUsingTokenAuth(); + } + Piwik::raiseMemoryLimitIfNecessary(); + + Piwik_Translate::getInstance()->reloadLanguage(); + $pluginsManager->postLoadPlugins(); + + Piwik_PostEvent('FrontController.checkForUpdates'); + } catch (Exception $e) { + + if (self::shouldRethrowException()) { + throw $e; + } + + Piwik_ExitWithMessage($e->getMessage(), false, true); + } + } + + protected function handleMaintenanceMode() + { + if (Piwik_Config::getInstance()->General['maintenance_mode'] == 1 + && !Piwik_Common::isPhpCliMode() + ) { + $format = Piwik_Common::getRequestVar('format', ''); + + $message = "Piwik is in scheduled maintenance. Please come back later." + . " The administrator can disable maintenance by editing the file piwik/config/config.ini.php and removing the following: " + . " maintenance_mode=1 "; + if (Piwik_Config::getInstance()->Tracker['record_statistics'] == 0) { + $message .= ' and record_statistics=0'; + } + + $exception = new Exception($message); + // extend explain how to re-enable + // show error message when record stats = 0 + if (empty($format)) { + throw $exception; + } + $response = new Piwik_API_ResponseBuilder($format); + echo $response->getResponseException($exception); + exit; + } + } + + protected function handleSSLRedirection() + { + if (!Piwik_Common::isPhpCliMode() + && Piwik_Config::getInstance()->General['force_ssl'] == 1 + && !Piwik::isHttps() + // Specifically disable for the opt out iframe + && !(Piwik_Common::getRequestVar('module', '') == 'CoreAdminHome' + && Piwik_Common::getRequestVar('action', '') == 'optOut') + ) { + $url = Piwik_Url::getCurrentUrl(); + $url = str_replace("http://", "https://", $url); + Piwik_Url::redirectToUrl($url); + } + } } /** @@ -382,9 +360,9 @@ class Piwik_FrontController */ class Piwik_FrontController_PluginDeactivatedException extends Exception { - function __construct($module) - { - parent::__construct("The plugin $module is not enabled. You can activate the plugin on Settings > Plugins page in Piwik."); - } + function __construct($module) + { + parent::__construct("The plugin $module is not enabled. You can activate the plugin on Settings > Plugins page in Piwik."); + } } diff --git a/core/HTMLPurifier.php b/core/HTMLPurifier.php index bbc0f21a14..cb5db8e473 100644 --- a/core/HTMLPurifier.php +++ b/core/HTMLPurifier.php @@ -16,40 +16,35 @@ */ class Piwik_HTMLPurifier { - static private $instance = null; + static private $instance = null; - /** - * Returns the singleton HTMLPurifier or a mock object - * - * @return HTMLPurifier|Piwik_HTMLPurifier - */ - static public function getInstance() - { - if (self::$instance == null) - { - if(file_exists(PIWIK_INCLUDE_PATH . '/libs/HTMLPurifier.php')) - { - if(!class_exists('HTMLPurifier_Bootstrap', false)) - { - HTMLPurifier_Bootstrap::registerAutoload(); - } + /** + * Returns the singleton HTMLPurifier or a mock object + * + * @return HTMLPurifier|Piwik_HTMLPurifier + */ + static public function getInstance() + { + if (self::$instance == null) { + if (file_exists(PIWIK_INCLUDE_PATH . '/libs/HTMLPurifier.php')) { + if (!class_exists('HTMLPurifier_Bootstrap', false)) { + HTMLPurifier_Bootstrap::registerAutoload(); + } - $config = HTMLPurifier_Config::createDefault(); - $config->set('Cache.SerializerPath', PIWIK_USER_PATH . '/tmp/purifier'); + $config = HTMLPurifier_Config::createDefault(); + $config->set('Cache.SerializerPath', PIWIK_USER_PATH . '/tmp/purifier'); - self::$instance = new HTMLPurifier($config); - } - else - { - $c = __CLASS__; - self::$instance = new $c(); - } - } - return self::$instance; - } + self::$instance = new HTMLPurifier($config); + } else { + $c = __CLASS__; + self::$instance = new $c(); + } + } + return self::$instance; + } - public function purify($html, $config = null) - { - return $html; - } + public function purify($html, $config = null) + { + return $html; + } } diff --git a/core/Http.php b/core/Http.php index 7e921d354d..1b0e328184 100644 --- a/core/Http.php +++ b/core/Http.php @@ -17,752 +17,683 @@ */ class Piwik_Http { - /** - * Get "best" available transport method for sendHttpRequest() calls. - * - * @return string - */ - static public function getTransportMethod() - { - $method = 'curl'; - if(!self::isCurlEnabled()) - { - $method = 'fopen'; - if(@ini_get('allow_url_fopen') != '1') - { - $method = 'socket'; - if(!self::isSocketEnabled()) - { - return null; - } - } - } - return $method; - } - - protected static function isSocketEnabled() - { - return function_exists('fsockopen'); - } - - protected static function isCurlEnabled() - { - return function_exists('curl_init'); - } - - /** - * 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 - * @param bool $acceptLanguage - * @param array $byteRange For Range: header. Should be two element array of bytes, eg, array(0, 1024) - * Doesn't work w/ fopen method. - * @param bool $getExtendedInfo True to return status code, headers & response, false if just response. - * @param string $httpMethod The HTTP method to use. Defaults to 'GET'. - * @throws Exception - * @return bool true (or string) on success; false on HTTP response error code (1xx or 4xx) - */ - static public function sendHttpRequest($aUrl, $timeout, $userAgent = null, $destinationPath = null, $followDepth = 0, $acceptLanguage = false, $byteRange = false, $getExtendedInfo = false, $httpMethod = 'GET' -) - { - // create output file - $file = null; - if($destinationPath) - { - // Ensure destination directory exists - Piwik_Common::mkdir(dirname($destinationPath)); - if (($file = @fopen($destinationPath, 'wb')) === false || !is_resource($file)) - { - throw new Exception('Error while creating the file: ' . $destinationPath); - } - } - - $acceptLanguage = $acceptLanguage ? 'Accept-Language: '.$acceptLanguage : ''; - return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth, $acceptLanguage, $acceptInvalidSslCertificate = false, $byteRange, $getExtendedInfo, $httpMethod); - } - - /** - * 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 - * @param bool|string $acceptLanguage Accept-language header - * @param bool $acceptInvalidSslCertificate Only used with $method == 'curl'. If set to true (NOT recommended!) the SSL certificate will not be checked - * @param array $byteRange For Range: header. Should be two element array of bytes, eg, array(0, 1024) - * Doesn't work w/ fopen method. - * @param bool $getExtendedInfo True to return status code, headers & response, false if just response. - * @param string $httpMethod The HTTP method to use. Defaults to 'GET'. - * @throws Exception - * @return bool true (or string/array) on success; false on HTTP response error code (1xx or 4xx) - */ - static public function sendHttpRequestBy( - $method = 'socket', - $aUrl, - $timeout, - $userAgent = null, - $destinationPath = null, - $file = null, - $followDepth = 0, - $acceptLanguage = false, - $acceptInvalidSslCertificate = false, - $byteRange = false, - $getExtendedInfo = false, - $httpMethod = 'GET' - ) - { - if ($followDepth > 5) - { - throw new Exception('Too many redirects ('.$followDepth.')'); - } - - $contentLength = 0; - $fileLength = 0; - - // Piwik services behave like a proxy, so we should act like one. - $xff = 'X-Forwarded-For: ' - . (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] . ',' : '') - . Piwik_IP::getIpFromHeader(); - - if(empty($userAgent)) - { - $userAgent = self::getUserAgent(); - } - - $via = 'Via: ' - . (isset($_SERVER['HTTP_VIA']) && !empty($_SERVER['HTTP_VIA']) ? $_SERVER['HTTP_VIA'] . ', ' : '') - . Piwik_Version::VERSION . ' ' - . ($userAgent ? " ($userAgent)" : ''); - - // range header - $rangeHeader = ''; - if (!empty($byteRange)) - { - $rangeHeader = 'Range: bytes='.$byteRange[0].'-'.$byteRange[1]."\r\n"; - } - - // proxy configuration - $proxyHost = Piwik_Config::getInstance()->proxy['host']; - $proxyPort = Piwik_Config::getInstance()->proxy['port']; - $proxyUser = Piwik_Config::getInstance()->proxy['username']; - $proxyPassword = Piwik_Config::getInstance()->proxy['password']; - - // other result data - $status = null; - $headers = array(); - - if($method == 'socket') - { - if(!self::isSocketEnabled()) { - // can be triggered in tests - throw new Exception("HTTP socket support is not enabled (php function fsockopen is not available) "); - } - // 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; - - if ((!empty($proxyHost) && !empty($proxyPort)) - || !empty($byteRange)) - { - $httpVer = '1.1'; - } - else - { - $httpVer = '1.0'; - } - - $proxyAuth = null; - if(!empty($proxyHost) && !empty($proxyPort)) - { - $connectHost = $proxyHost; - $connectPort = $proxyPort; - if(!empty($proxyUser) && !empty($proxyPassword)) - { - $proxyAuth = 'Proxy-Authorization: Basic '.base64_encode("$proxyUser:$proxyPassword") ."\r\n"; - } - $requestHeader = "$httpMethod $aUrl HTTP/$httpVer\r\n"; - } - else - { - $connectHost = $host; - $connectPort = $port; - $requestHeader = "$httpMethod $path HTTP/$httpVer\r\n"; - } - - // connection attempt - if (($fsock = @fsockopen($connectHost, $connectPort, $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 - $requestHeader .= - "Host: $host".($port != 80 ? ':'.$port : '')."\r\n" - .($proxyAuth ? $proxyAuth : '') - .'User-Agent: '.$userAgent."\r\n" - . ($acceptLanguage ? $acceptLanguage ."\r\n" : '') - .$xff."\r\n" - .$via."\r\n" - .$rangeHeader - ."Connection: close\r\n" - ."\r\n"; - fwrite($fsock, $requestHeader); - - $streamMetaData = array('timed_out' => false); - @stream_set_blocking($fsock, true); - - if (function_exists('stream_set_timeout')) - { - @stream_set_timeout($fsock, $timeout); - } - elseif (function_exists('socket_set_timeout')) - { - @socket_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); - - if (!$getExtendedInfo) - { - return false; - } - else - { - return array('status' => $status); - } - } - - 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, - $destinationPath, - $file, - $followDepth+1, - $acceptLanguage, - $acceptInvalidSslCertificate = false, - $byteRange, - $getExtendedInfo, - $httpMethod - ); - } - - // save expected content length for later verification - if(preg_match('/^Content-Length:\s*(\d+)/', $line, $m)) - { - $contentLength = (integer) $m[1]; - } - - self::parseHeaderLine($headers, $line); - } - - if (feof($fsock) - && $httpMethod != 'HEAD') - { - 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 += Piwik_Common::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 == 'fopen') - { - $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: '.$userAgent."\r\n" - .($acceptLanguage ? $acceptLanguage."\r\n" : '') - .$xff."\r\n" - .$via."\r\n" - .$rangeHeader, - 'max_redirects' => 5, // PHP 5.1.0 - 'timeout' => $timeout, // PHP 5.2.1 - ) - ); - - if(!empty($proxyHost) && !empty($proxyPort)) - { - $stream_options['http']['proxy'] = 'tcp://'.$proxyHost.':'.$proxyPort; - $stream_options['http']['request_fulluri'] = true; // required by squid proxy - if(!empty($proxyUser) && !empty($proxyPassword)) - { - $stream_options['http']['header'] .= 'Proxy-Authorization: Basic '.base64_encode("$proxyUser:$proxyPassword")."\r\n"; - } - } - - $ctx = stream_context_create($stream_options); - } - - // save to file - if(is_resource($file)) - { - $handle = fopen($aUrl, 'rb', false, $ctx); - while(!feof($handle)) - { - $response = fread($handle, 8192); - $fileLength += Piwik_Common::strlen($response); - fwrite($file, $response); - } - fclose($handle); - } - else - { - $response = file_get_contents($aUrl, 0, $ctx); - $fileLength = Piwik_Common::strlen($response); - } - - // restore the socket_timeout value - if(!empty($default_socket_timeout)) - { - @ini_set('default_socket_timeout', $default_socket_timeout); - } - } - else if($method == 'curl') - { - if(!self::isCurlEnabled()) { - // can be triggered in tests - throw new Exception("CURL is not enabled in php.ini, but is being used."); - } - $ch = @curl_init(); - - if(!empty($proxyHost) && !empty($proxyPort)) - { - @curl_setopt($ch, CURLOPT_PROXY, $proxyHost.':'.$proxyPort); - if(!empty($proxyUser) && !empty($proxyPassword)) - { - // PROXYAUTH defaults to BASIC - @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyUser.':'.$proxyPassword); - } - } - - $curl_options = array( - // internal to ext/curl - CURLOPT_BINARYTRANSFER => is_resource($file), - - // curl options (sorted oldest to newest) - CURLOPT_URL => $aUrl, - CURLOPT_USERAGENT => $userAgent, - CURLOPT_HTTPHEADER => array( - $xff, - $via, - $rangeHeader, - $acceptLanguage - ), - // only get header info if not saving directly to file - CURLOPT_HEADER => is_resource($file) ? false : true, - CURLOPT_CONNECTTIMEOUT => $timeout, - ); - // Case archive.php is triggering archiving on https:// and the certificate is not valid - if($acceptInvalidSslCertificate) - { - $curl_options += array( - CURLOPT_SSL_VERIFYHOST => false, - CURLOPT_SSL_VERIFYPEER => false, - ); - } - @curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $httpMethod); - if ($httpMethod == 'HEAD') - { - @curl_setopt($ch, CURLOPT_NOBODY, true); - } - - @curl_setopt_array($ch, $curl_options); - self::configCurlCertificate($ch); - - - /* - * 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 = ''; - } - else - { - $header = ''; - // redirects are included in the output html, so we look for the last line that starts w/ HTTP/... - // to split the response - while (substr($response, 0, 5) == "HTTP/") - { - list($header, $response) = explode("\r\n\r\n", $response, 2); - } - - foreach (explode("\r\n", $header) as $line) - { - self::parseHeaderLine($headers, $line); - } - } - - $contentLength = @curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); - $fileLength = is_resource($file) ? @curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : Piwik_Common::strlen($response); - $status = @curl_getinfo($ch, CURLINFO_HTTP_CODE); - - @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 (!$getExtendedInfo) - { - return trim($response); - } - else - { - return array( - 'status' => $status, - 'headers' => $headers, - 'data' => $response - ); - } - } - - /** - * Downloads the next chunk of a specific file. The next chunk's byte range - * is determined by the existing file's size and the expected file size, which - * is stored in the piwik_option table before starting a download. - * - * Note this function uses the Range HTTP header to accomplish downloading in - * parts. - * - * @param string $url The url to download from. - * @param string $outputPath The path to the file to save/append to. - * @param bool $isContinuation True if this is the continuation of a download, - * or if we're starting a fresh one. - */ - public static function downloadChunk( $url, $outputPath, $isContinuation ) - { - // make sure file doesn't already exist if we're starting a new download - if (!$isContinuation - && file_exists($outputPath)) - { - throw new Exception( - Piwik_Translate('General_DownloadFail_FileExists', "'".$outputPath."'") - . ' ' . Piwik_Translate('General_DownloadPleaseRemoveExisting')); - } - - // if we're starting a download, get the expected file size & save as an option - $downloadOption = $outputPath.'_expectedDownloadSize'; - if (!$isContinuation) - { - $expectedFileSizeResult = Piwik_Http::sendHttpRequest( - $url, - $timeout = 300, - $userAgent = null, - $destinationPath = null, - $followDepth = 0, - $acceptLanguage = false, - $byteRange = false, - $getExtendedInfo = true, - $httpMethod = 'HEAD' - ); - - $expectedFileSize = 0; - if (isset($expectedFileSizeResult['headers']['Content-Length'])) - { - $expectedFileSize = (int)$expectedFileSizeResult['headers']['Content-Length']; - } - - if ($expectedFileSize == 0) - { - Piwik::log(sprintf("HEAD request for '%s' failed, got following: %s", $url, print_r($expectedFileSizeResult, true))); - throw new Exception(Piwik_Translate('General_DownloadFail_HttpRequestFail')); - } - - Piwik_SetOption($downloadOption, $expectedFileSize); - } - else - { - $expectedFileSize = (int)Piwik_GetOption($downloadOption); - if ($expectedFileSize === false) // sanity check - { - throw new Exception("Trying to continue a download that never started?! That's not supposed to happen..."); - } - } - - // if existing file is already big enough, then fail so we don't accidentally overwrite - // existing DB - $existingSize = file_exists($outputPath) ? filesize($outputPath) : 0; - if ($existingSize >= $expectedFileSize) - { - throw new Exception( - Piwik_Translate('General_DownloadFail_FileExistsContinue', "'".$outputPath."'") - . ' ' . Piwik_Translate('General_DownloadPleaseRemoveExisting')); - } - - // download a chunk of the file - $result = Piwik_Http::sendHttpRequest( - $url, - $timeout = 300, - $userAgent = null, - $destinationPath = null, - $followDepth = 0, - $acceptLanguage = false, - $byteRange = array($existingSize, min($existingSize + 1024 * 1024 - 1, $expectedFileSize)), - $getExtendedInfo = true - ); - - if ($result === false - || $result['status'] < 200 - || $result['status'] > 299) - { - $result['data'] = self::truncateStr($result['data'], 1024); - Piwik::log("Failed to download range '".$byteRange[0]."-".$byteRange[1] - . "' of file from url '$url'. Got result: ".print_r($result, true)); - - throw new Exception(Piwik_Translate('General_DownloadFail_HttpRequestFail')); - } - - // write chunk to file - $f = fopen($outputPath, 'ab'); - fwrite($f, $result['data']); - fclose($f); - - clearstatcache($clear_realpath_cache = true, $outputPath); - return array( - 'current_size' => filesize($outputPath), - 'expected_file_size' => $expectedFileSize, - ); - } - - /** - * Will configure CURL handle $ch - * to use local list of Certificate Authorities, - */ - public static function configCurlCertificate( &$ch ) - { - if (file_exists(PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem')) - { - @curl_setopt($ch, CURLOPT_CAINFO, PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem'); - } - } - - public static function getUserAgent() - { - return !empty($_SERVER['HTTP_USER_AGENT']) - ? $_SERVER['HTTP_USER_AGENT'] - : 'Piwik/' . Piwik_Version::VERSION; - } - - /** - * Fetch the file at $url in the destination $destinationPath - * - * @param string $url - * @param string $destinationPath - * @param int $tries - * @throws Exception - * @return bool true on success, throws Exception on failure - */ - static public function fetchRemoteFile($url, $destinationPath = null, $tries = 0) - { - @ignore_user_abort(true); - Piwik::setMaxExecutionTime(0); - return self::sendHttpRequest($url, 10, 'Update', $destinationPath, $tries); - } - - /** - * Utility function, parses an HTTP header line into key/value & sets header - * array with them. - * - * @param array $headers - * @param string $line - */ - private static function parseHeaderLine( &$headers, $line ) - { - $parts = explode(':', $line, 2); - if (count($parts) == 1) - { - return; - } - - list($name, $value) = $parts; - $headers[trim($name)] = trim($value); - } - - /** - * Utility function that truncates a string to an arbitrary limit. - * - * @param string $str The string to truncate. - * @param int $limit The maximum length of the truncated string. - * @return string - */ - private static function truncateStr( $str, $limit ) - { - if (strlen($str) > $limit) - { - return substr($str, 0, $limit).'...'; - } - return $str; - } + /** + * Get "best" available transport method for sendHttpRequest() calls. + * + * @return string + */ + static public function getTransportMethod() + { + $method = 'curl'; + if (!self::isCurlEnabled()) { + $method = 'fopen'; + if (@ini_get('allow_url_fopen') != '1') { + $method = 'socket'; + if (!self::isSocketEnabled()) { + return null; + } + } + } + return $method; + } + + protected static function isSocketEnabled() + { + return function_exists('fsockopen'); + } + + protected static function isCurlEnabled() + { + return function_exists('curl_init'); + } + + /** + * 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 + * @param bool $acceptLanguage + * @param array $byteRange For Range: header. Should be two element array of bytes, eg, array(0, 1024) + * Doesn't work w/ fopen method. + * @param bool $getExtendedInfo True to return status code, headers & response, false if just response. + * @param string $httpMethod The HTTP method to use. Defaults to 'GET'. + * @throws Exception + * @return bool true (or string) on success; false on HTTP response error code (1xx or 4xx) + */ + static public function sendHttpRequest($aUrl, $timeout, $userAgent = null, $destinationPath = null, $followDepth = 0, $acceptLanguage = false, $byteRange = false, $getExtendedInfo = false, $httpMethod = 'GET' + ) + { + // create output file + $file = null; + if ($destinationPath) { + // Ensure destination directory exists + Piwik_Common::mkdir(dirname($destinationPath)); + if (($file = @fopen($destinationPath, 'wb')) === false || !is_resource($file)) { + throw new Exception('Error while creating the file: ' . $destinationPath); + } + } + + $acceptLanguage = $acceptLanguage ? 'Accept-Language: ' . $acceptLanguage : ''; + return self::sendHttpRequestBy(self::getTransportMethod(), $aUrl, $timeout, $userAgent, $destinationPath, $file, $followDepth, $acceptLanguage, $acceptInvalidSslCertificate = false, $byteRange, $getExtendedInfo, $httpMethod); + } + + /** + * 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 + * @param bool|string $acceptLanguage Accept-language header + * @param bool $acceptInvalidSslCertificate Only used with $method == 'curl'. If set to true (NOT recommended!) the SSL certificate will not be checked + * @param array $byteRange For Range: header. Should be two element array of bytes, eg, array(0, 1024) + * Doesn't work w/ fopen method. + * @param bool $getExtendedInfo True to return status code, headers & response, false if just response. + * @param string $httpMethod The HTTP method to use. Defaults to 'GET'. + * @throws Exception + * @return bool true (or string/array) on success; false on HTTP response error code (1xx or 4xx) + */ + static public function sendHttpRequestBy( + $method = 'socket', + $aUrl, + $timeout, + $userAgent = null, + $destinationPath = null, + $file = null, + $followDepth = 0, + $acceptLanguage = false, + $acceptInvalidSslCertificate = false, + $byteRange = false, + $getExtendedInfo = false, + $httpMethod = 'GET' + ) + { + if ($followDepth > 5) { + throw new Exception('Too many redirects (' . $followDepth . ')'); + } + + $contentLength = 0; + $fileLength = 0; + + // Piwik services behave like a proxy, so we should act like one. + $xff = 'X-Forwarded-For: ' + . (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] . ',' : '') + . Piwik_IP::getIpFromHeader(); + + if (empty($userAgent)) { + $userAgent = self::getUserAgent(); + } + + $via = 'Via: ' + . (isset($_SERVER['HTTP_VIA']) && !empty($_SERVER['HTTP_VIA']) ? $_SERVER['HTTP_VIA'] . ', ' : '') + . Piwik_Version::VERSION . ' ' + . ($userAgent ? " ($userAgent)" : ''); + + // range header + $rangeHeader = ''; + if (!empty($byteRange)) { + $rangeHeader = 'Range: bytes=' . $byteRange[0] . '-' . $byteRange[1] . "\r\n"; + } + + // proxy configuration + $proxyHost = Piwik_Config::getInstance()->proxy['host']; + $proxyPort = Piwik_Config::getInstance()->proxy['port']; + $proxyUser = Piwik_Config::getInstance()->proxy['username']; + $proxyPassword = Piwik_Config::getInstance()->proxy['password']; + + // other result data + $status = null; + $headers = array(); + + if ($method == 'socket') { + if (!self::isSocketEnabled()) { + // can be triggered in tests + throw new Exception("HTTP socket support is not enabled (php function fsockopen is not available) "); + } + // 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; + + if ((!empty($proxyHost) && !empty($proxyPort)) + || !empty($byteRange) + ) { + $httpVer = '1.1'; + } else { + $httpVer = '1.0'; + } + + $proxyAuth = null; + if (!empty($proxyHost) && !empty($proxyPort)) { + $connectHost = $proxyHost; + $connectPort = $proxyPort; + if (!empty($proxyUser) && !empty($proxyPassword)) { + $proxyAuth = 'Proxy-Authorization: Basic ' . base64_encode("$proxyUser:$proxyPassword") . "\r\n"; + } + $requestHeader = "$httpMethod $aUrl HTTP/$httpVer\r\n"; + } else { + $connectHost = $host; + $connectPort = $port; + $requestHeader = "$httpMethod $path HTTP/$httpVer\r\n"; + } + + // connection attempt + if (($fsock = @fsockopen($connectHost, $connectPort, $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 + $requestHeader .= + "Host: $host" . ($port != 80 ? ':' . $port : '') . "\r\n" + . ($proxyAuth ? $proxyAuth : '') + . 'User-Agent: ' . $userAgent . "\r\n" + . ($acceptLanguage ? $acceptLanguage . "\r\n" : '') + . $xff . "\r\n" + . $via . "\r\n" + . $rangeHeader + . "Connection: close\r\n" + . "\r\n"; + fwrite($fsock, $requestHeader); + + $streamMetaData = array('timed_out' => false); + @stream_set_blocking($fsock, true); + + if (function_exists('stream_set_timeout')) { + @stream_set_timeout($fsock, $timeout); + } elseif (function_exists('socket_set_timeout')) { + @socket_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); + + if (!$getExtendedInfo) { + return false; + } else { + return array('status' => $status); + } + } + + 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, + $destinationPath, + $file, + $followDepth + 1, + $acceptLanguage, + $acceptInvalidSslCertificate = false, + $byteRange, + $getExtendedInfo, + $httpMethod + ); + } + + // save expected content length for later verification + if (preg_match('/^Content-Length:\s*(\d+)/', $line, $m)) { + $contentLength = (integer)$m[1]; + } + + self::parseHeaderLine($headers, $line); + } + + if (feof($fsock) + && $httpMethod != 'HEAD' + ) { + 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 += Piwik_Common::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 == 'fopen') { + $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: ' . $userAgent . "\r\n" + . ($acceptLanguage ? $acceptLanguage . "\r\n" : '') + . $xff . "\r\n" + . $via . "\r\n" + . $rangeHeader, + 'max_redirects' => 5, // PHP 5.1.0 + 'timeout' => $timeout, // PHP 5.2.1 + ) + ); + + if (!empty($proxyHost) && !empty($proxyPort)) { + $stream_options['http']['proxy'] = 'tcp://' . $proxyHost . ':' . $proxyPort; + $stream_options['http']['request_fulluri'] = true; // required by squid proxy + if (!empty($proxyUser) && !empty($proxyPassword)) { + $stream_options['http']['header'] .= 'Proxy-Authorization: Basic ' . base64_encode("$proxyUser:$proxyPassword") . "\r\n"; + } + } + + $ctx = stream_context_create($stream_options); + } + + // save to file + if (is_resource($file)) { + $handle = fopen($aUrl, 'rb', false, $ctx); + while (!feof($handle)) { + $response = fread($handle, 8192); + $fileLength += Piwik_Common::strlen($response); + fwrite($file, $response); + } + fclose($handle); + } else { + $response = file_get_contents($aUrl, 0, $ctx); + $fileLength = Piwik_Common::strlen($response); + } + + // restore the socket_timeout value + if (!empty($default_socket_timeout)) { + @ini_set('default_socket_timeout', $default_socket_timeout); + } + } else if ($method == 'curl') { + if (!self::isCurlEnabled()) { + // can be triggered in tests + throw new Exception("CURL is not enabled in php.ini, but is being used."); + } + $ch = @curl_init(); + + if (!empty($proxyHost) && !empty($proxyPort)) { + @curl_setopt($ch, CURLOPT_PROXY, $proxyHost . ':' . $proxyPort); + if (!empty($proxyUser) && !empty($proxyPassword)) { + // PROXYAUTH defaults to BASIC + @curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyUser . ':' . $proxyPassword); + } + } + + $curl_options = array( + // internal to ext/curl + CURLOPT_BINARYTRANSFER => is_resource($file), + + // curl options (sorted oldest to newest) + CURLOPT_URL => $aUrl, + CURLOPT_USERAGENT => $userAgent, + CURLOPT_HTTPHEADER => array( + $xff, + $via, + $rangeHeader, + $acceptLanguage + ), + // only get header info if not saving directly to file + CURLOPT_HEADER => is_resource($file) ? false : true, + CURLOPT_CONNECTTIMEOUT => $timeout, + ); + // Case archive.php is triggering archiving on https:// and the certificate is not valid + if ($acceptInvalidSslCertificate) { + $curl_options += array( + CURLOPT_SSL_VERIFYHOST => false, + CURLOPT_SSL_VERIFYPEER => false, + ); + } + @curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $httpMethod); + if ($httpMethod == 'HEAD') { + @curl_setopt($ch, CURLOPT_NOBODY, true); + } + + @curl_setopt_array($ch, $curl_options); + self::configCurlCertificate($ch); + + + /* + * 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 = ''; + } else { + $header = ''; + // redirects are included in the output html, so we look for the last line that starts w/ HTTP/... + // to split the response + while (substr($response, 0, 5) == "HTTP/") { + list($header, $response) = explode("\r\n\r\n", $response, 2); + } + + foreach (explode("\r\n", $header) as $line) { + self::parseHeaderLine($headers, $line); + } + } + + $contentLength = @curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); + $fileLength = is_resource($file) ? @curl_getinfo($ch, CURLINFO_SIZE_DOWNLOAD) : Piwik_Common::strlen($response); + $status = @curl_getinfo($ch, CURLINFO_HTTP_CODE); + + @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 (!$getExtendedInfo) { + return trim($response); + } else { + return array( + 'status' => $status, + 'headers' => $headers, + 'data' => $response + ); + } + } + + /** + * Downloads the next chunk of a specific file. The next chunk's byte range + * is determined by the existing file's size and the expected file size, which + * is stored in the piwik_option table before starting a download. + * + * Note this function uses the Range HTTP header to accomplish downloading in + * parts. + * + * @param string $url The url to download from. + * @param string $outputPath The path to the file to save/append to. + * @param bool $isContinuation True if this is the continuation of a download, + * or if we're starting a fresh one. + */ + public static function downloadChunk($url, $outputPath, $isContinuation) + { + // make sure file doesn't already exist if we're starting a new download + if (!$isContinuation + && file_exists($outputPath) + ) { + throw new Exception( + Piwik_Translate('General_DownloadFail_FileExists', "'" . $outputPath . "'") + . ' ' . Piwik_Translate('General_DownloadPleaseRemoveExisting')); + } + + // if we're starting a download, get the expected file size & save as an option + $downloadOption = $outputPath . '_expectedDownloadSize'; + if (!$isContinuation) { + $expectedFileSizeResult = Piwik_Http::sendHttpRequest( + $url, + $timeout = 300, + $userAgent = null, + $destinationPath = null, + $followDepth = 0, + $acceptLanguage = false, + $byteRange = false, + $getExtendedInfo = true, + $httpMethod = 'HEAD' + ); + + $expectedFileSize = 0; + if (isset($expectedFileSizeResult['headers']['Content-Length'])) { + $expectedFileSize = (int)$expectedFileSizeResult['headers']['Content-Length']; + } + + if ($expectedFileSize == 0) { + Piwik::log(sprintf("HEAD request for '%s' failed, got following: %s", $url, print_r($expectedFileSizeResult, true))); + throw new Exception(Piwik_Translate('General_DownloadFail_HttpRequestFail')); + } + + Piwik_SetOption($downloadOption, $expectedFileSize); + } else { + $expectedFileSize = (int)Piwik_GetOption($downloadOption); + if ($expectedFileSize === false) // sanity check + { + throw new Exception("Trying to continue a download that never started?! That's not supposed to happen..."); + } + } + + // if existing file is already big enough, then fail so we don't accidentally overwrite + // existing DB + $existingSize = file_exists($outputPath) ? filesize($outputPath) : 0; + if ($existingSize >= $expectedFileSize) { + throw new Exception( + Piwik_Translate('General_DownloadFail_FileExistsContinue', "'" . $outputPath . "'") + . ' ' . Piwik_Translate('General_DownloadPleaseRemoveExisting')); + } + + // download a chunk of the file + $result = Piwik_Http::sendHttpRequest( + $url, + $timeout = 300, + $userAgent = null, + $destinationPath = null, + $followDepth = 0, + $acceptLanguage = false, + $byteRange = array($existingSize, min($existingSize + 1024 * 1024 - 1, $expectedFileSize)), + $getExtendedInfo = true + ); + + if ($result === false + || $result['status'] < 200 + || $result['status'] > 299 + ) { + $result['data'] = self::truncateStr($result['data'], 1024); + Piwik::log("Failed to download range '" . $byteRange[0] . "-" . $byteRange[1] + . "' of file from url '$url'. Got result: " . print_r($result, true)); + + throw new Exception(Piwik_Translate('General_DownloadFail_HttpRequestFail')); + } + + // write chunk to file + $f = fopen($outputPath, 'ab'); + fwrite($f, $result['data']); + fclose($f); + + clearstatcache($clear_realpath_cache = true, $outputPath); + return array( + 'current_size' => filesize($outputPath), + 'expected_file_size' => $expectedFileSize, + ); + } + + /** + * Will configure CURL handle $ch + * to use local list of Certificate Authorities, + */ + public static function configCurlCertificate(&$ch) + { + if (file_exists(PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem')) { + @curl_setopt($ch, CURLOPT_CAINFO, PIWIK_INCLUDE_PATH . '/core/DataFiles/cacert.pem'); + } + } + + public static function getUserAgent() + { + return !empty($_SERVER['HTTP_USER_AGENT']) + ? $_SERVER['HTTP_USER_AGENT'] + : 'Piwik/' . Piwik_Version::VERSION; + } + + /** + * Fetch the file at $url in the destination $destinationPath + * + * @param string $url + * @param string $destinationPath + * @param int $tries + * @throws Exception + * @return bool true on success, throws Exception on failure + */ + static public function fetchRemoteFile($url, $destinationPath = null, $tries = 0) + { + @ignore_user_abort(true); + Piwik::setMaxExecutionTime(0); + return self::sendHttpRequest($url, 10, 'Update', $destinationPath, $tries); + } + + /** + * Utility function, parses an HTTP header line into key/value & sets header + * array with them. + * + * @param array $headers + * @param string $line + */ + private static function parseHeaderLine(&$headers, $line) + { + $parts = explode(':', $line, 2); + if (count($parts) == 1) { + return; + } + + list($name, $value) = $parts; + $headers[trim($name)] = trim($value); + } + + /** + * Utility function that truncates a string to an arbitrary limit. + * + * @param string $str The string to truncate. + * @param int $limit The maximum length of the truncated string. + * @return string + */ + private static function truncateStr($str, $limit) + { + if (strlen($str) > $limit) { + return substr($str, 0, $limit) . '...'; + } + return $str; + } } diff --git a/core/IP.php b/core/IP.php index f830301814..33cce5cdbb 100644 --- a/core/IP.php +++ b/core/IP.php @@ -9,23 +9,27 @@ * @package Piwik */ -if(Piwik_Common::isWindows() || !function_exists('inet_ntop')) { - function _inet_ntop($in_addr) { - return php_compat_inet_ntop($in_addr); - } +if (Piwik_Common::isWindows() || !function_exists('inet_ntop')) { + function _inet_ntop($in_addr) + { + return php_compat_inet_ntop($in_addr); + } } else { - function _inet_ntop($in_addr) { - return inet_ntop($in_addr); - } + function _inet_ntop($in_addr) + { + return inet_ntop($in_addr); + } } -if(Piwik_Common::isWindows() || !function_exists('inet_pton')) { - function _inet_pton($address) { - return php_compat_inet_pton($address); - } +if (Piwik_Common::isWindows() || !function_exists('inet_pton')) { + function _inet_pton($address) + { + return php_compat_inet_pton($address); + } } else { - function _inet_pton($address) { - return inet_pton($address); - } + function _inet_pton($address) + { + return inet_pton($address); + } } /** @@ -47,434 +51,397 @@ if(Piwik_Common::isWindows() || !function_exists('inet_pton')) { */ class Piwik_IP { - const MAPPED_IPv4_START = '::ffff:'; - - /** - * Sanitize human-readable IP address. - * - * @param string $ipString IP address - * @return string|false - */ - static public function sanitizeIp($ipString) - { - $ipString = trim($ipString); - - // CIDR notation, A.B.C.D/E - $posSlash = strrpos($ipString, '/'); - if($posSlash !== false) - { - $ipString = substr($ipString, 0, $posSlash); - } - - $posColon = strrpos($ipString, ':'); - $posDot = strrpos($ipString, '.'); - if($posColon !== false) - { - // IPv6 address with port, [A:B:C:D:E:F:G:H]:EEEE - $posRBrac = strrpos($ipString, ']'); - if($posRBrac !== false && $ipString[0] == '[') - { - $ipString = substr($ipString, 1, $posRBrac - 1); - } - - if($posDot !== false) - { - // IPv4 address with port, A.B.C.D:EEEE - if($posColon > $posDot) - { - $ipString = substr($ipString, 0, $posColon); - } - // else: Dotted quad IPv6 address, A:B:C:D:E:F:G.H.I.J - } - else if(strpos($ipString, ':') === $posColon) - { - $ipString = substr($ipString, 0, $posColon); - } - // else: IPv6 address, A:B:C:D:E:F:G:H - } - // else: IPv4 address, A.B.C.D - - return $ipString; - } - - /** - * Sanitize human-readable (user-supplied) IP address range. - * - * Accepts the following formats for $ipRange: - * - single IPv4 address, e.g., 127.0.0.1 - * - single IPv6 address, e.g., ::1/128 - * - IPv4 block using CIDR notation, e.g., 192.168.0.0/22 represents the IPv4 addresses from 192.168.0.0 to 192.168.3.255 - * - IPv6 block using CIDR notation, e.g., 2001:DB8::/48 represents the IPv6 addresses from 2001:DB8:0:0:0:0:0:0 to 2001:DB8:0:FFFF:FFFF:FFFF:FFFF:FFFF - * - wildcards, e.g., 192.168.0.* - * - * @param string $ipRangeString IP address range - * @return string|false IP address range in CIDR notation - */ - static public function sanitizeIpRange($ipRangeString) - { - $ipRangeString = trim($ipRangeString); - if(empty($ipRangeString)) - { - return false; - } - - // IPv4 address with wildcards '*' - if(strpos($ipRangeString, '*') !== false) - { - if(preg_match('~(^|\.)\*\.\d+(\.|$)~D', $ipRangeString)) - { - return false; - } - - $bits = 32 - 8 * substr_count($ipRangeString, '*'); - $ipRangeString = str_replace('*', '0', $ipRangeString); - } - - // CIDR - if(($pos = strpos($ipRangeString, '/')) !== false) - { - $bits = substr($ipRangeString, $pos + 1); - $ipRangeString = substr($ipRangeString, 0, $pos); - } - - // single IP - if(($ip = @_inet_pton($ipRangeString)) === false) - return false; - - $maxbits = Piwik_Common::strlen($ip) * 8; - if(!isset($bits)) - $bits = $maxbits; - - if($bits < 0 || $bits > $maxbits) - { - return false; - } - - return "$ipRangeString/$bits"; - } - - /** - * Convert presentation format IP address to network address format - * - * @param string $ipString IP address, either IPv4 or IPv6, e.g., "127.0.0.1" - * @return string Binary-safe string, e.g., "\x7F\x00\x00\x01" - */ - static public function P2N($ipString) - { - // use @inet_pton() because it throws an exception and E_WARNING on invalid input - $ip = @_inet_pton($ipString); - return $ip === false ? "\x00\x00\x00\x00" : $ip; - } - - /** - * Convert network address format to presentation format - * - * @see prettyPrint() - * - * @param string $ip IP address in network address format - * @return string IP address in presentation format - */ - static public function N2P($ip) - { - // use @inet_ntop() because it throws an exception and E_WARNING on invalid input - $ipStr = @_inet_ntop($ip); - return $ipStr === false ? '0.0.0.0' : $ipStr; - } - - /** - * Alias for N2P() - * - * @param string $ip IP address in network address format - * @return string IP address in presentation format - */ - static public function prettyPrint($ip) - { - return self::N2P($ip); - } - - /** - * Is this an IPv4, IPv4-compat, or IPv4-mapped address? - * - * @param string $ip IP address in network address format - * @return bool True if IPv4, else false - */ - static public function isIPv4($ip) - { - // in case mbstring overloads strlen and substr functions - $strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen'; - $substr = function_exists('mb_orig_substr') ? 'mb_orig_substr' : 'substr'; - - // IPv4 - if($strlen($ip) == 4) - { - return true; - } - - // IPv6 - transitional address? - if($strlen($ip) == 16) - { - if(substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 0, 12) === 0 - || substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0, 12) === 0) - { - return true; - } - } - - return false; - } - - /** - * Convert IP address (in network address format) to presentation format. - * This is a backward compatibility function for code that only expects - * IPv4 addresses (i.e., doesn't support IPv6). - * - * This function does not support the long (or its string representation) - * returned by the built-in ip2long() function, from Piwik 1.3 and earlier. - * - * @param string $ip IPv4 address in network address format - * @return string IP address in presentation format - */ - static public function long2ip($ip) - { - // IPv4 - if(Piwik_Common::strlen($ip) == 4) - { - return self::N2P($ip); - } - - // IPv6 - transitional address? - if(Piwik_Common::strlen($ip) == 16) - { - if(substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 0, 12) === 0 - || substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0, 12) === 0) - { - // remap 128-bit IPv4-mapped and IPv4-compat addresses - return self::N2P(Piwik_Common::substr($ip, 12)); - } - } - - return '0.0.0.0'; - } - - /** - * Returns true if $ip is an IPv6 address, false if otherwise. This function does - * a naive check. It assumes that whatever format $ip is in, it is well-formed. - * - * @param string $ip - * @return bool - */ - static public function isIPv6( $ip ) - { - return strpos($ip, ':') !== false; - } - - /** - * Returns true if $ip is a IPv4 mapped address, false if otherwise. - * - * @param string $ip - * @return bool - */ - static public function isMappedIPv4($ip) - { - return substr($ip, 0, strlen(self::MAPPED_IPv4_START)) === self::MAPPED_IPv4_START; - } - - /** - * Returns - */ - static public function getIPv4FromMappedIPv6($ip) - { - return substr($ip, strlen(self::MAPPED_IPv4_START)); - } - - /** - * Get low and high IP addresses for a specified range. - * - * @param array $ipRange An IP address range in presentation format - * @return array|false Array ($lowIp, $highIp) in network address format, or false if failure - */ - static public function getIpsForRange($ipRange) - { - if(strpos($ipRange, '/') === false) - { - $ipRange = self::sanitizeIpRange($ipRange); - } - $pos = strpos($ipRange, '/'); - - $bits = substr($ipRange, $pos + 1); - $range = substr($ipRange, 0, $pos); - $high = $low = @_inet_pton($range); - if($low === false) - { - return false; - } - - $lowLen = Piwik_Common::strlen($low); - $i = $lowLen - 1; - $bits = $lowLen * 8 - $bits; - - for($n = (int)($bits / 8); $n > 0; $n--, $i--) - { - $low[$i] = chr(0); - $high[$i] = chr(255); - } - - $n = $bits % 8; - if($n) - { - $low[$i] = chr(ord($low[$i]) & ~((1 << $n) - 1)); - $high[$i] = chr(ord($high[$i]) | ((1 << $n) - 1)); - } - - return array($low, $high); - } - - /** - * Determines if an IP address is in a specified IP address range. - * - * An IPv4-mapped address should be range checked with an IPv4-mapped address range. - * - * @param string $ip IP address in network address format - * @param array $ipRanges List of IP address ranges - * @return bool True if in any of the specified IP address ranges; else false. - */ - static public function isIpInRange($ip, $ipRanges) - { - $ipLen = Piwik_Common::strlen($ip); - if(empty($ip) || empty($ipRanges) || ($ipLen != 4 && $ipLen != 16)) - { - return false; - } - - foreach($ipRanges as $range) - { - if(is_array($range)) - { - // already split into low/high IP addresses - $range[0] = self::P2N($range[0]); - $range[1] = self::P2N($range[1]); - } - else - { - // expect CIDR format but handle some variations - $range = self::getIpsForRange($range); - } - if($range === false) - { - continue; - } - - $low = $range[0]; - $high = $range[1]; - if(Piwik_Common::strlen($low) != $ipLen) - { - continue; - } - - // binary-safe string comparison - if($ip >= $low && $ip <= $high) - { - return true; - } - } - - return false; - } - - /** - * Returns the best possible IP of the current user, in the format A.B.C.D - * For example, this could be the proxy client's IP address. - * - * @return string IP address in presentation format - */ - static public function getIpFromHeader() - { - $clientHeaders = @Piwik_Config::getInstance()->General['proxy_client_headers']; - if(!is_array($clientHeaders)) - { - $clientHeaders = array(); - } - - $default = '0.0.0.0'; - if(isset($_SERVER['REMOTE_ADDR'])) - { - $default = $_SERVER['REMOTE_ADDR']; - } - - $ipString = self::getNonProxyIpFromHeader($default, $clientHeaders); - return self::sanitizeIp($ipString); - } - - /** - * Returns a non-proxy IP address from header - * - * @param string $default Default value to return if no matching proxy header - * @param array $proxyHeaders List of proxy headers - * @return string - */ - static public function getNonProxyIpFromHeader($default, $proxyHeaders) - { - $proxyIps = @Piwik_Config::getInstance()->General['proxy_ips']; - if(!is_array($proxyIps)) - { - $proxyIps = array(); - } - $proxyIps[] = $default; - - // examine proxy headers - foreach($proxyHeaders as $proxyHeader) - { - if(!empty($_SERVER[$proxyHeader])) - { - $proxyIp = self::getLastIpFromList($_SERVER[$proxyHeader], $proxyIps); - if(strlen($proxyIp) && stripos($proxyIp, 'unknown') === false) - { - return $proxyIp; - } - } - } - - return $default; - } - - /** - * Returns the last IP address in a comma separated list, subject to an optional exclusion list. - * - * @param string $csv Comma separated list of elements - * @param array $excludedIps Optional list of excluded IP addresses (or IP address ranges) - * @return string Last (non-excluded) IP address in the list - */ - static public function getLastIpFromList($csv, $excludedIps = null) - { - $p = strrpos($csv, ','); - if($p !== false) - { - $elements = explode(',', $csv); - for($i = count($elements); $i--; ) - { - $element = trim(Piwik_Common::sanitizeInputValue($elements[$i])); - if(empty($excludedIps) || (!in_array($element, $excludedIps) && !self::isIpInRange(self::P2N(self::sanitizeIp($element)), $excludedIps))) - { - return $element; - } - } - } - return trim(Piwik_Common::sanitizeInputValue($csv)); - } - - /** - * Get hostname for a given IP address - * - * @param string $ipStr Human-readable IP address - * @return string Hostname or unmodified $ipStr if failure - */ - static public function getHostByAddr($ipStr) - { - // PHP's reverse lookup supports ipv4 and ipv6 - // except on Windows before PHP 5.3 - $host = strtolower(@gethostbyaddr($ipStr)); - return $host === '' ? $ipStr : $host; - } + const MAPPED_IPv4_START = '::ffff:'; + + /** + * Sanitize human-readable IP address. + * + * @param string $ipString IP address + * @return string|false + */ + static public function sanitizeIp($ipString) + { + $ipString = trim($ipString); + + // CIDR notation, A.B.C.D/E + $posSlash = strrpos($ipString, '/'); + if ($posSlash !== false) { + $ipString = substr($ipString, 0, $posSlash); + } + + $posColon = strrpos($ipString, ':'); + $posDot = strrpos($ipString, '.'); + if ($posColon !== false) { + // IPv6 address with port, [A:B:C:D:E:F:G:H]:EEEE + $posRBrac = strrpos($ipString, ']'); + if ($posRBrac !== false && $ipString[0] == '[') { + $ipString = substr($ipString, 1, $posRBrac - 1); + } + + if ($posDot !== false) { + // IPv4 address with port, A.B.C.D:EEEE + if ($posColon > $posDot) { + $ipString = substr($ipString, 0, $posColon); + } + // else: Dotted quad IPv6 address, A:B:C:D:E:F:G.H.I.J + } else if (strpos($ipString, ':') === $posColon) { + $ipString = substr($ipString, 0, $posColon); + } + // else: IPv6 address, A:B:C:D:E:F:G:H + } + // else: IPv4 address, A.B.C.D + + return $ipString; + } + + /** + * Sanitize human-readable (user-supplied) IP address range. + * + * Accepts the following formats for $ipRange: + * - single IPv4 address, e.g., 127.0.0.1 + * - single IPv6 address, e.g., ::1/128 + * - IPv4 block using CIDR notation, e.g., 192.168.0.0/22 represents the IPv4 addresses from 192.168.0.0 to 192.168.3.255 + * - IPv6 block using CIDR notation, e.g., 2001:DB8::/48 represents the IPv6 addresses from 2001:DB8:0:0:0:0:0:0 to 2001:DB8:0:FFFF:FFFF:FFFF:FFFF:FFFF + * - wildcards, e.g., 192.168.0.* + * + * @param string $ipRangeString IP address range + * @return string|false IP address range in CIDR notation + */ + static public function sanitizeIpRange($ipRangeString) + { + $ipRangeString = trim($ipRangeString); + if (empty($ipRangeString)) { + return false; + } + + // IPv4 address with wildcards '*' + if (strpos($ipRangeString, '*') !== false) { + if (preg_match('~(^|\.)\*\.\d+(\.|$)~D', $ipRangeString)) { + return false; + } + + $bits = 32 - 8 * substr_count($ipRangeString, '*'); + $ipRangeString = str_replace('*', '0', $ipRangeString); + } + + // CIDR + if (($pos = strpos($ipRangeString, '/')) !== false) { + $bits = substr($ipRangeString, $pos + 1); + $ipRangeString = substr($ipRangeString, 0, $pos); + } + + // single IP + if (($ip = @_inet_pton($ipRangeString)) === false) + return false; + + $maxbits = Piwik_Common::strlen($ip) * 8; + if (!isset($bits)) + $bits = $maxbits; + + if ($bits < 0 || $bits > $maxbits) { + return false; + } + + return "$ipRangeString/$bits"; + } + + /** + * Convert presentation format IP address to network address format + * + * @param string $ipString IP address, either IPv4 or IPv6, e.g., "127.0.0.1" + * @return string Binary-safe string, e.g., "\x7F\x00\x00\x01" + */ + static public function P2N($ipString) + { + // use @inet_pton() because it throws an exception and E_WARNING on invalid input + $ip = @_inet_pton($ipString); + return $ip === false ? "\x00\x00\x00\x00" : $ip; + } + + /** + * Convert network address format to presentation format + * + * @see prettyPrint() + * + * @param string $ip IP address in network address format + * @return string IP address in presentation format + */ + static public function N2P($ip) + { + // use @inet_ntop() because it throws an exception and E_WARNING on invalid input + $ipStr = @_inet_ntop($ip); + return $ipStr === false ? '0.0.0.0' : $ipStr; + } + + /** + * Alias for N2P() + * + * @param string $ip IP address in network address format + * @return string IP address in presentation format + */ + static public function prettyPrint($ip) + { + return self::N2P($ip); + } + + /** + * Is this an IPv4, IPv4-compat, or IPv4-mapped address? + * + * @param string $ip IP address in network address format + * @return bool True if IPv4, else false + */ + static public function isIPv4($ip) + { + // in case mbstring overloads strlen and substr functions + $strlen = function_exists('mb_orig_strlen') ? 'mb_orig_strlen' : 'strlen'; + $substr = function_exists('mb_orig_substr') ? 'mb_orig_substr' : 'substr'; + + // IPv4 + if ($strlen($ip) == 4) { + return true; + } + + // IPv6 - transitional address? + if ($strlen($ip) == 16) { + if (substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 0, 12) === 0 + || substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0, 12) === 0 + ) { + return true; + } + } + + return false; + } + + /** + * Convert IP address (in network address format) to presentation format. + * This is a backward compatibility function for code that only expects + * IPv4 addresses (i.e., doesn't support IPv6). + * + * This function does not support the long (or its string representation) + * returned by the built-in ip2long() function, from Piwik 1.3 and earlier. + * + * @param string $ip IPv4 address in network address format + * @return string IP address in presentation format + */ + static public function long2ip($ip) + { + // IPv4 + if (Piwik_Common::strlen($ip) == 4) { + return self::N2P($ip); + } + + // IPv6 - transitional address? + if (Piwik_Common::strlen($ip) == 16) { + if (substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff", 0, 12) === 0 + || substr_compare($ip, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 0, 12) === 0 + ) { + // remap 128-bit IPv4-mapped and IPv4-compat addresses + return self::N2P(Piwik_Common::substr($ip, 12)); + } + } + + return '0.0.0.0'; + } + + /** + * Returns true if $ip is an IPv6 address, false if otherwise. This function does + * a naive check. It assumes that whatever format $ip is in, it is well-formed. + * + * @param string $ip + * @return bool + */ + static public function isIPv6($ip) + { + return strpos($ip, ':') !== false; + } + + /** + * Returns true if $ip is a IPv4 mapped address, false if otherwise. + * + * @param string $ip + * @return bool + */ + static public function isMappedIPv4($ip) + { + return substr($ip, 0, strlen(self::MAPPED_IPv4_START)) === self::MAPPED_IPv4_START; + } + + /** + * Returns + */ + static public function getIPv4FromMappedIPv6($ip) + { + return substr($ip, strlen(self::MAPPED_IPv4_START)); + } + + /** + * Get low and high IP addresses for a specified range. + * + * @param array $ipRange An IP address range in presentation format + * @return array|false Array ($lowIp, $highIp) in network address format, or false if failure + */ + static public function getIpsForRange($ipRange) + { + if (strpos($ipRange, '/') === false) { + $ipRange = self::sanitizeIpRange($ipRange); + } + $pos = strpos($ipRange, '/'); + + $bits = substr($ipRange, $pos + 1); + $range = substr($ipRange, 0, $pos); + $high = $low = @_inet_pton($range); + if ($low === false) { + return false; + } + + $lowLen = Piwik_Common::strlen($low); + $i = $lowLen - 1; + $bits = $lowLen * 8 - $bits; + + for ($n = (int)($bits / 8); $n > 0; $n--, $i--) { + $low[$i] = chr(0); + $high[$i] = chr(255); + } + + $n = $bits % 8; + if ($n) { + $low[$i] = chr(ord($low[$i]) & ~((1 << $n) - 1)); + $high[$i] = chr(ord($high[$i]) | ((1 << $n) - 1)); + } + + return array($low, $high); + } + + /** + * Determines if an IP address is in a specified IP address range. + * + * An IPv4-mapped address should be range checked with an IPv4-mapped address range. + * + * @param string $ip IP address in network address format + * @param array $ipRanges List of IP address ranges + * @return bool True if in any of the specified IP address ranges; else false. + */ + static public function isIpInRange($ip, $ipRanges) + { + $ipLen = Piwik_Common::strlen($ip); + if (empty($ip) || empty($ipRanges) || ($ipLen != 4 && $ipLen != 16)) { + return false; + } + + foreach ($ipRanges as $range) { + if (is_array($range)) { + // already split into low/high IP addresses + $range[0] = self::P2N($range[0]); + $range[1] = self::P2N($range[1]); + } else { + // expect CIDR format but handle some variations + $range = self::getIpsForRange($range); + } + if ($range === false) { + continue; + } + + $low = $range[0]; + $high = $range[1]; + if (Piwik_Common::strlen($low) != $ipLen) { + continue; + } + + // binary-safe string comparison + if ($ip >= $low && $ip <= $high) { + return true; + } + } + + return false; + } + + /** + * Returns the best possible IP of the current user, in the format A.B.C.D + * For example, this could be the proxy client's IP address. + * + * @return string IP address in presentation format + */ + static public function getIpFromHeader() + { + $clientHeaders = @Piwik_Config::getInstance()->General['proxy_client_headers']; + if (!is_array($clientHeaders)) { + $clientHeaders = array(); + } + + $default = '0.0.0.0'; + if (isset($_SERVER['REMOTE_ADDR'])) { + $default = $_SERVER['REMOTE_ADDR']; + } + + $ipString = self::getNonProxyIpFromHeader($default, $clientHeaders); + return self::sanitizeIp($ipString); + } + + /** + * Returns a non-proxy IP address from header + * + * @param string $default Default value to return if no matching proxy header + * @param array $proxyHeaders List of proxy headers + * @return string + */ + static public function getNonProxyIpFromHeader($default, $proxyHeaders) + { + $proxyIps = @Piwik_Config::getInstance()->General['proxy_ips']; + if (!is_array($proxyIps)) { + $proxyIps = array(); + } + $proxyIps[] = $default; + + // examine proxy headers + foreach ($proxyHeaders as $proxyHeader) { + if (!empty($_SERVER[$proxyHeader])) { + $proxyIp = self::getLastIpFromList($_SERVER[$proxyHeader], $proxyIps); + if (strlen($proxyIp) && stripos($proxyIp, 'unknown') === false) { + return $proxyIp; + } + } + } + + return $default; + } + + /** + * Returns the last IP address in a comma separated list, subject to an optional exclusion list. + * + * @param string $csv Comma separated list of elements + * @param array $excludedIps Optional list of excluded IP addresses (or IP address ranges) + * @return string Last (non-excluded) IP address in the list + */ + static public function getLastIpFromList($csv, $excludedIps = null) + { + $p = strrpos($csv, ','); + if ($p !== false) { + $elements = explode(',', $csv); + for ($i = count($elements); $i--;) { + $element = trim(Piwik_Common::sanitizeInputValue($elements[$i])); + if (empty($excludedIps) || (!in_array($element, $excludedIps) && !self::isIpInRange(self::P2N(self::sanitizeIp($element)), $excludedIps))) { + return $element; + } + } + } + return trim(Piwik_Common::sanitizeInputValue($csv)); + } + + /** + * Get hostname for a given IP address + * + * @param string $ipStr Human-readable IP address + * @return string Hostname or unmodified $ipStr if failure + */ + static public function getHostByAddr($ipStr) + { + // PHP's reverse lookup supports ipv4 and ipv6 + // except on Windows before PHP 5.3 + $host = strtolower(@gethostbyaddr($ipStr)); + return $host === '' ? $ipStr : $host; + } } /** @@ -482,72 +449,67 @@ class Piwik_IP * * @link http://php.net/inet_ntop * - * @param string $in_addr 32-bit IPv4 or 128-bit IPv6 address + * @param string $in_addr 32-bit IPv4 or 128-bit IPv6 address * @return string|false string representation of address or false on failure */ function php_compat_inet_ntop($in_addr) { - $r = bin2hex($in_addr); - - switch (Piwik_Common::strlen($in_addr)) - { - case 4: - // IPv4 address - $prefix = ''; - break; - - case 16: - // IPv4-mapped address - if(substr_compare($r, '00000000000000000000ffff', 0, 24) === 0) - { - $prefix = '::ffff:'; - $r = substr($r, 24); - break; - } - - // IPv4-compat address - if(substr_compare($r, '000000000000000000000000', 0, 24) === 0 && - substr_compare($r, '0000', 24, 4) !== 0) - { - $prefix = '::'; - $r = substr($r, 24); - break; - } - - $r = str_split($r, 4); - $r = implode(':', $r); - - // compress leading zeros - $r = preg_replace( - '/(^|:)0{1,3}/', - '$1', - $r - ); - - // compress longest (and leftmost) consecutive groups of zeros - if(preg_match_all('/(?:^|:)(0(:|$))+/D', $r, $matches)) - { - $longestMatch = 0; - foreach($matches[0] as $aMatch) - { - if(strlen($aMatch) > strlen($longestMatch)) - { - $longestMatch = $aMatch; - } - } - $r = substr_replace($r, '::', strpos($r, $longestMatch), strlen($longestMatch)); - } - - return $r; - - default: - return false; - } - - $r = str_split($r, 2); - $r = array_map('hexdec', $r); - $r = implode('.', $r); - return $prefix . $r; + $r = bin2hex($in_addr); + + switch (Piwik_Common::strlen($in_addr)) { + case 4: + // IPv4 address + $prefix = ''; + break; + + case 16: + // IPv4-mapped address + if (substr_compare($r, '00000000000000000000ffff', 0, 24) === 0) { + $prefix = '::ffff:'; + $r = substr($r, 24); + break; + } + + // IPv4-compat address + if (substr_compare($r, '000000000000000000000000', 0, 24) === 0 && + substr_compare($r, '0000', 24, 4) !== 0 + ) { + $prefix = '::'; + $r = substr($r, 24); + break; + } + + $r = str_split($r, 4); + $r = implode(':', $r); + + // compress leading zeros + $r = preg_replace( + '/(^|:)0{1,3}/', + '$1', + $r + ); + + // compress longest (and leftmost) consecutive groups of zeros + if (preg_match_all('/(?:^|:)(0(:|$))+/D', $r, $matches)) { + $longestMatch = 0; + foreach ($matches[0] as $aMatch) { + if (strlen($aMatch) > strlen($longestMatch)) { + $longestMatch = $aMatch; + } + } + $r = substr_replace($r, '::', strpos($r, $longestMatch), strlen($longestMatch)); + } + + return $r; + + default: + return false; + } + + $r = str_split($r, 2); + $r = array_map('hexdec', $r); + $r = implode('.', $r); + return $prefix . $r; } /** @@ -555,87 +517,79 @@ function php_compat_inet_ntop($in_addr) * * @link http://php.net/inet_pton * - * @param string $address a human readable IPv4 or IPv6 address + * @param string $address a human readable IPv4 or IPv6 address * @return string in_addr representation or false on failure */ function php_compat_inet_pton($address) { - // IPv4 (or IPv4-compat, or IPv4-mapped) - if(preg_match('/(^|:)([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/iD', $address, $matches)) - { - for($i = count($matches); $i-- > 2; ) - { - if($matches[$i] > 255 || - ($matches[$i][0] == '0' && strlen($matches[$i]) > 1)) - { - return false; - } - } - - if(empty($matches[1])) - { - $r = ip2long($address); - if($r === false) - { - return false; - } - - return pack('N', $r); - } - - $suffix = sprintf("%02x%02x:%02x%02x", $matches[2], $matches[3], $matches[4], $matches[5]); - $address = substr_replace($address, $matches[1] . $suffix, strrpos($address, $matches[0])); - } - - // IPv6 - if(strpos($address, ':') === false || - strspn($address, '01234567890abcdefABCDEF:') !== strlen($address)) - { - return false; - } - - if(substr($address, 0, 2) == '::') - { - $address = '0'.$address; - } - - if(substr($address, -2) == '::') - { - $address .= '0'; - } - - $r = explode(':', $address); - $count = count($r); - - // grouped zeros - if(strpos($address, '::') !== false - && $count < 8) - { - $zeroGroup = array_search('', $r, 1); - - // we're replacing this cell, so we splice (8 - $count + 1) cells containing '0' - array_splice($r, $zeroGroup, 1, array_fill(0, 9 - $count, '0')); - } - - // guard against excessive ':' or '::' - if($count > 8 || - array_search('', $r, 1) !== false) - { - return false; - } - - // leading zeros - foreach($r as $v) - { - if(strlen(ltrim($v, '0')) > 4) - { - return false; - } - } - - $r = array_map('hexdec', $r); - array_unshift($r, 'n*'); - $r = call_user_func_array('pack', $r); - - return $r; + // IPv4 (or IPv4-compat, or IPv4-mapped) + if (preg_match('/(^|:)([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$/iD', $address, $matches)) { + for ($i = count($matches); $i-- > 2;) { + if ($matches[$i] > 255 || + ($matches[$i][0] == '0' && strlen($matches[$i]) > 1) + ) { + return false; + } + } + + if (empty($matches[1])) { + $r = ip2long($address); + if ($r === false) { + return false; + } + + return pack('N', $r); + } + + $suffix = sprintf("%02x%02x:%02x%02x", $matches[2], $matches[3], $matches[4], $matches[5]); + $address = substr_replace($address, $matches[1] . $suffix, strrpos($address, $matches[0])); + } + + // IPv6 + if (strpos($address, ':') === false || + strspn($address, '01234567890abcdefABCDEF:') !== strlen($address) + ) { + return false; + } + + if (substr($address, 0, 2) == '::') { + $address = '0' . $address; + } + + if (substr($address, -2) == '::') { + $address .= '0'; + } + + $r = explode(':', $address); + $count = count($r); + + // grouped zeros + if (strpos($address, '::') !== false + && $count < 8 + ) { + $zeroGroup = array_search('', $r, 1); + + // we're replacing this cell, so we splice (8 - $count + 1) cells containing '0' + array_splice($r, $zeroGroup, 1, array_fill(0, 9 - $count, '0')); + } + + // guard against excessive ':' or '::' + if ($count > 8 || + array_search('', $r, 1) !== false + ) { + return false; + } + + // leading zeros + foreach ($r as $v) { + if (strlen(ltrim($v, '0')) > 4) { + return false; + } + } + + $r = array_map('hexdec', $r); + array_unshift($r, 'n*'); + $r = call_user_func_array('pack', $r); + + return $r; } diff --git a/core/Loader.php b/core/Loader.php index ae375d03a7..271e20c1a3 100644 --- a/core/Loader.php +++ b/core/Loader.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -16,117 +16,101 @@ */ class Piwik_Loader { - // our class search path; current directory is intentionally excluded - protected static $dirs = array( '/core/', '/plugins/' ); + // our class search path; current directory is intentionally excluded + protected static $dirs = array('/core/', '/plugins/'); - /** - * Get class file name - * - * @param string $class Class name - * @return string Class file name - * @throws Exception if class name is invalid - */ - protected static function getClassFileName($class) - { - if(!preg_match("/^[A-Za-z0-9_]+$/", $class)) - { - throw new Exception("Invalid class name \"$class\"."); - } + /** + * Get class file name + * + * @param string $class Class name + * @return string Class file name + * @throws Exception if class name is invalid + */ + protected static function getClassFileName($class) + { + if (!preg_match("/^[A-Za-z0-9_]+$/", $class)) { + throw new Exception("Invalid class name \"$class\"."); + } - $class = str_replace('_', '/', $class); + $class = str_replace('_', '/', $class); - if($class == 'Piwik') - { - return $class; - } + if ($class == 'Piwik') { + return $class; + } - if(!strncmp($class, 'Piwik/', 6)) - { - return substr($class, 6); - } + if (!strncmp($class, 'Piwik/', 6)) { + return substr($class, 6); + } - return $class; - } + return $class; + } - /** - * Load class by name - * - * @param string $class Class name - * @throws Exception if class not found - */ - public static function loadClass($class) - { - $classPath = self::getClassFileName($class); - if($class == 'Piwik' || !strncmp($class, 'Piwik_', 6)) - { - // Piwik classes are in core/ or plugins/ - do - { - // auto-discover class location - foreach(self::$dirs as $dir) - { - $path = PIWIK_INCLUDE_PATH . $dir . $classPath . '.php'; - if(file_exists($path)) - { - require_once $path; // prefixed by PIWIK_INCLUDE_PATH - if(class_exists($class, false) || interface_exists($class, false)) - { - return; - } - } - } + /** + * Load class by name + * + * @param string $class Class name + * @throws Exception if class not found + */ + public static function loadClass($class) + { + $classPath = self::getClassFileName($class); + if ($class == 'Piwik' || !strncmp($class, 'Piwik_', 6)) { + // Piwik classes are in core/ or plugins/ + do { + // auto-discover class location + foreach (self::$dirs as $dir) { + $path = PIWIK_INCLUDE_PATH . $dir . $classPath . '.php'; + if (file_exists($path)) { + require_once $path; // prefixed by PIWIK_INCLUDE_PATH + if (class_exists($class, false) || interface_exists($class, false)) { + return; + } + } + } - // truncate to find file with multiple class definitions - $lastSlash = strrpos($classPath, '/'); - $classPath = ($lastSlash === false) ? '' : substr($classPath, 0, $lastSlash); - } while(!empty($classPath)); - } - else - { - // non-Piwik classes (e.g., Zend Framework) are in libs/ - $path = PIWIK_INCLUDE_PATH . '/libs/' . $classPath . '.php'; - if(file_exists($path)) - { - require_once $path; // prefixed by PIWIK_INCLUDE_PATH - if(class_exists($class, false) || interface_exists($class, false)) - { - return; - } - } - } - throw new Exception("Class \"$class\" not found."); - } + // truncate to find file with multiple class definitions + $lastSlash = strrpos($classPath, '/'); + $classPath = ($lastSlash === false) ? '' : substr($classPath, 0, $lastSlash); + } while (!empty($classPath)); + } else { + // non-Piwik classes (e.g., Zend Framework) are in libs/ + $path = PIWIK_INCLUDE_PATH . '/libs/' . $classPath . '.php'; + if (file_exists($path)) { + require_once $path; // prefixed by PIWIK_INCLUDE_PATH + if (class_exists($class, false) || interface_exists($class, false)) { + return; + } + } + } + throw new Exception("Class \"$class\" not found."); + } - /** - * Autoloader - * - * @param string $class Class name - */ - public static function autoload($class) - { - try { - self::loadClass($class); - } catch (Exception $e) { - } - } + /** + * Autoloader + * + * @param string $class Class name + */ + public static function autoload($class) + { + try { + self::loadClass($class); + } catch (Exception $e) { + } + } } // Note: only one __autoload per PHP instance -if(function_exists('spl_autoload_register')) -{ - // use the SPL autoload stack - spl_autoload_register(array('Piwik_Loader', 'autoload')); +if (function_exists('spl_autoload_register')) { + // use the SPL autoload stack + spl_autoload_register(array('Piwik_Loader', 'autoload')); - // preserve any existing __autoload - if(function_exists('__autoload')) - { - spl_autoload_register('__autoload'); - } -} -else -{ - function __autoload($class) - { - Piwik_Loader::autoload($class); - } + // preserve any existing __autoload + if (function_exists('__autoload')) { + spl_autoload_register('__autoload'); + } +} else { + function __autoload($class) + { + Piwik_Loader::autoload($class); + } } diff --git a/core/Log.php b/core/Log.php index 871ae9d787..5e658094f6 100644 --- a/core/Log.php +++ b/core/Log.php @@ -1,200 +1,192 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * + * * @package Piwik * @subpackage Piwik_Log * @see Zend_Log, libs/Zend/Log.php - * @link http://framework.zend.com/manual/en/zend.log.html + * @link http://framework.zend.com/manual/en/zend.log.html */ abstract class Piwik_Log extends Zend_Log { - protected $logToDatabaseTableName = null; - protected $logToDatabaseColumnMapping = null; - protected $logToFileFilename = null; - protected $fileFormatter = null; - protected $screenFormatter = null; - protected $currentRequestKey; - - function __construct( $logToFileFilename, - $fileFormatter, - $screenFormatter, - $logToDatabaseTableName, - $logToDatabaseColumnMapping ) - { - parent::__construct(); - - $this->currentRequestKey = substr( Piwik_Common::generateUniqId(), 0, 8); - - $log_dir = Piwik_Config::getInstance()->log['logger_file_path']; - if($log_dir[0] != '/' && $log_dir[0] != DIRECTORY_SEPARATOR) - { - $log_dir = PIWIK_USER_PATH . '/' . $log_dir; - } - $this->logToFileFilename = $log_dir . '/' . $logToFileFilename; - - $this->fileFormatter = $fileFormatter; - $this->screenFormatter = $screenFormatter; - $this->logToDatabaseTableName = Piwik_Common::prefixTable($logToDatabaseTableName); - $this->logToDatabaseColumnMapping = $logToDatabaseColumnMapping; - } - - function addWriteToFile() - { - Piwik_Common::mkdir(dirname($this->logToFileFilename)); - $writerFile = new Zend_Log_Writer_Stream($this->logToFileFilename); - $writerFile->setFormatter( $this->fileFormatter ); - $this->addWriter($writerFile); - } - - function addWriteToNull() - { - $this->addWriter( new Zend_Log_Writer_Null ); - } - - function addWriteToDatabase() - { - $writerDb = new Zend_Log_Writer_Db( - Zend_Registry::get('db'), - $this->logToDatabaseTableName, - $this->logToDatabaseColumnMapping); - - $this->addWriter($writerDb); - } - - function addWriteToScreen() - { - $writerScreen = new Zend_Log_Writer_Stream('php://output'); - $writerScreen->setFormatter( $this->screenFormatter ); - $this->addWriter($writerScreen); - } - - public function getWritersCount() - { - return count($this->_writers); - } - - /** - * Log an event - * @param string $event - * @param int $priority - * @param null $extras - * @throws Zend_Log_Exception - * @return void - */ - public function log($event, $priority, $extras = null) - { - // sanity checks - if (empty($this->_writers)) { - throw new Zend_Log_Exception('No writers were added'); - } - - $event['timestamp'] = date('Y-m-d H:i:s'); - $event['requestKey'] = $this->currentRequestKey; - // pack into event required by filters and writers - $event = array_merge( $event, $this->_extras); - - // one message must stay on one line - if(isset($event['message'])) - { - $event['message'] = str_replace(array(PHP_EOL, "\n"), " ", $event['message']); - } - - // Truncate the backtrace which can be too long to display in the browser - if(!empty($event['backtrace'])) - { - $maxSizeOutputBytes = 1024 * 1024; // no more than 1M output please - $truncateBacktraceLineAfter = 1000; - $maxLines = ceil($maxSizeOutputBytes / $truncateBacktraceLineAfter); - $bt = explode("\n", $event['backtrace']); - foreach($bt as $count => &$line) - { - if(strlen($line) > $truncateBacktraceLineAfter) - { - $line = substr($line, 0, $truncateBacktraceLineAfter) . '...'; - } - if($count > $maxLines) - { - $line .= "\nTruncated error message."; - break; - } - } - $event['backtrace'] = implode("\n", $bt); - } - // abort if rejected by the global filters - foreach ($this->_filters as $filter) { - if (! $filter->accept($event)) { - return; - } - } - - // send to each writer - foreach ($this->_writers as $writer) { - $writer->write($event); - } - } + protected $logToDatabaseTableName = null; + protected $logToDatabaseColumnMapping = null; + protected $logToFileFilename = null; + protected $fileFormatter = null; + protected $screenFormatter = null; + protected $currentRequestKey; + + function __construct($logToFileFilename, + $fileFormatter, + $screenFormatter, + $logToDatabaseTableName, + $logToDatabaseColumnMapping) + { + parent::__construct(); + + $this->currentRequestKey = substr(Piwik_Common::generateUniqId(), 0, 8); + + $log_dir = Piwik_Config::getInstance()->log['logger_file_path']; + if ($log_dir[0] != '/' && $log_dir[0] != DIRECTORY_SEPARATOR) { + $log_dir = PIWIK_USER_PATH . '/' . $log_dir; + } + $this->logToFileFilename = $log_dir . '/' . $logToFileFilename; + + $this->fileFormatter = $fileFormatter; + $this->screenFormatter = $screenFormatter; + $this->logToDatabaseTableName = Piwik_Common::prefixTable($logToDatabaseTableName); + $this->logToDatabaseColumnMapping = $logToDatabaseColumnMapping; + } + + function addWriteToFile() + { + Piwik_Common::mkdir(dirname($this->logToFileFilename)); + $writerFile = new Zend_Log_Writer_Stream($this->logToFileFilename); + $writerFile->setFormatter($this->fileFormatter); + $this->addWriter($writerFile); + } + + function addWriteToNull() + { + $this->addWriter(new Zend_Log_Writer_Null); + } + + function addWriteToDatabase() + { + $writerDb = new Zend_Log_Writer_Db( + Zend_Registry::get('db'), + $this->logToDatabaseTableName, + $this->logToDatabaseColumnMapping); + + $this->addWriter($writerDb); + } + + function addWriteToScreen() + { + $writerScreen = new Zend_Log_Writer_Stream('php://output'); + $writerScreen->setFormatter($this->screenFormatter); + $this->addWriter($writerScreen); + } + + public function getWritersCount() + { + return count($this->_writers); + } + + /** + * Log an event + * @param string $event + * @param int $priority + * @param null $extras + * @throws Zend_Log_Exception + * @return void + */ + public function log($event, $priority, $extras = null) + { + // sanity checks + if (empty($this->_writers)) { + throw new Zend_Log_Exception('No writers were added'); + } + + $event['timestamp'] = date('Y-m-d H:i:s'); + $event['requestKey'] = $this->currentRequestKey; + // pack into event required by filters and writers + $event = array_merge($event, $this->_extras); + + // one message must stay on one line + if (isset($event['message'])) { + $event['message'] = str_replace(array(PHP_EOL, "\n"), " ", $event['message']); + } + + // Truncate the backtrace which can be too long to display in the browser + if (!empty($event['backtrace'])) { + $maxSizeOutputBytes = 1024 * 1024; // no more than 1M output please + $truncateBacktraceLineAfter = 1000; + $maxLines = ceil($maxSizeOutputBytes / $truncateBacktraceLineAfter); + $bt = explode("\n", $event['backtrace']); + foreach ($bt as $count => &$line) { + if (strlen($line) > $truncateBacktraceLineAfter) { + $line = substr($line, 0, $truncateBacktraceLineAfter) . '...'; + } + if ($count > $maxLines) { + $line .= "\nTruncated error message."; + break; + } + } + $event['backtrace'] = implode("\n", $bt); + } + // abort if rejected by the global filters + foreach ($this->_filters as $filter) { + if (!$filter->accept($event)) { + return; + } + } + + // send to each writer + foreach ($this->_writers as $writer) { + $writer->write($event); + } + } } /** - * + * * @package Piwik * @subpackage Piwik_Log */ class Piwik_Log_Formatter_FileFormatter implements Zend_Log_Formatter_Interface -{ - /** - * Formats data into a single line to be written by the writer. - * - * @param array $event event data - * @return string formatted line to write to the log - */ - public function format($event) - { - foreach($event as &$value) - { - $value = str_replace("\n", '\n', $value); - $value = '"'.$value.'"'; - } - $ts = $event['timestamp']; - unset($event['timestamp']); - return $ts . ' ' . implode(" ", $event) . "\n"; - } +{ + /** + * Formats data into a single line to be written by the writer. + * + * @param array $event event data + * @return string formatted line to write to the log + */ + public function format($event) + { + foreach ($event as &$value) { + $value = str_replace("\n", '\n', $value); + $value = '"' . $value . '"'; + } + $ts = $event['timestamp']; + unset($event['timestamp']); + return $ts . ' ' . implode(" ", $event) . "\n"; + } } /** - * + * * @package Piwik * @subpackage Piwik_Log */ class Piwik_Log_Formatter_ScreenFormatter implements Zend_Log_Formatter_Interface { - function formatEvent($event) - { - // no injection in error messages, backtrace when displayed on screen - return array_map(array('Piwik_Common','sanitizeInputValue'), $event); - } - - function format($string) - { - return self::getFormattedString($string); - } - - static public function getFormattedString($string) - { - if(!Piwik_Common::isPhpCliMode()) - { - @header('Content-Type: text/html; charset=utf-8'); - } - return $string; - } + function formatEvent($event) + { + // no injection in error messages, backtrace when displayed on screen + return array_map(array('Piwik_Common', 'sanitizeInputValue'), $event); + } + + function format($string) + { + return self::getFormattedString($string); + } + + static public function getFormattedString($string) + { + if (!Piwik_Common::isPhpCliMode()) { + @header('Content-Type: text/html; charset=utf-8'); + } + return $string; + } } diff --git a/core/Log/APICall.php b/core/Log/APICall.php index 53e40e55c3..07e5bb25ff 100644 --- a/core/Log/APICall.php +++ b/core/Log/APICall.php @@ -17,58 +17,58 @@ */ class Piwik_Log_APICall extends Piwik_Log { - const ID = 'logger_api_call'; + const ID = 'logger_api_call'; - /** - * Constructor - */ - function __construct() - { - $logToFileFilename = self::ID; - $logToDatabaseTableName = self::ID; - $logToDatabaseColumnMapping = array( - 'class_name' => 'class_name', - 'method_name' => 'method_name', - 'parameter_names_default_values' => 'parameter_names_default_values', - 'parameter_values' => 'parameter_values', - 'execution_time' => 'execution_time', - 'caller_ip' => 'caller_ip', - 'timestamp' => 'timestamp', - 'returned_value' => 'returned_value' - ); - $screenFormatter = new Piwik_Log_APICall_Formatter_ScreenFormatter(); - $fileFormatter = new Piwik_Log_Formatter_FileFormatter(); + /** + * Constructor + */ + function __construct() + { + $logToFileFilename = self::ID; + $logToDatabaseTableName = self::ID; + $logToDatabaseColumnMapping = array( + 'class_name' => 'class_name', + 'method_name' => 'method_name', + 'parameter_names_default_values' => 'parameter_names_default_values', + 'parameter_values' => 'parameter_values', + 'execution_time' => 'execution_time', + 'caller_ip' => 'caller_ip', + 'timestamp' => 'timestamp', + 'returned_value' => 'returned_value' + ); + $screenFormatter = new Piwik_Log_APICall_Formatter_ScreenFormatter(); + $fileFormatter = new Piwik_Log_Formatter_FileFormatter(); - parent::__construct($logToFileFilename, - $fileFormatter, - $screenFormatter, - $logToDatabaseTableName, - $logToDatabaseColumnMapping ); + parent::__construct($logToFileFilename, + $fileFormatter, + $screenFormatter, + $logToDatabaseTableName, + $logToDatabaseColumnMapping); - $this->setEventItem('caller_ip', Piwik_IP::P2N(Piwik_IP::getIpFromHeader()) ); - } + $this->setEventItem('caller_ip', Piwik_IP::P2N(Piwik_IP::getIpFromHeader())); + } - /** - * Logs the given api call event with the parameters - * - * @param string $className - * @param string $methodName - * @param array $parameterNames - * @param array $parameterValues - * @param number $executionTime - * @param mixed $returnedValue - */ - public function logEvent($className, $methodName, $parameterNames, $parameterValues, $executionTime, $returnedValue) - { - $event = array(); - $event['class_name'] = $className; - $event['method_name'] = $methodName; - $event['parameter_names_default_values'] = serialize($parameterNames); - $event['parameter_values'] = serialize($parameterValues); - $event['execution_time'] = $executionTime; - $event['returned_value'] = is_array($returnedValue) ? serialize($returnedValue) : $returnedValue; - parent::log($event, Piwik_Log::INFO, null); - } + /** + * Logs the given api call event with the parameters + * + * @param string $className + * @param string $methodName + * @param array $parameterNames + * @param array $parameterValues + * @param number $executionTime + * @param mixed $returnedValue + */ + public function logEvent($className, $methodName, $parameterNames, $parameterValues, $executionTime, $returnedValue) + { + $event = array(); + $event['class_name'] = $className; + $event['method_name'] = $methodName; + $event['parameter_names_default_values'] = serialize($parameterNames); + $event['parameter_values'] = serialize($parameterValues); + $event['execution_time'] = $executionTime; + $event['returned_value'] = is_array($returnedValue) ? serialize($returnedValue) : $returnedValue; + parent::log($event, Piwik_Log::INFO, null); + } } /** @@ -79,60 +79,53 @@ class Piwik_Log_APICall extends Piwik_Log */ class Piwik_Log_APICall_Formatter_ScreenFormatter extends Piwik_Log_Formatter_ScreenFormatter { - /** + /** * Formats data into a single line to be written by the writer. * - * @param array $event event data + * @param array $event event data * @return string formatted line to write to the log */ public function format($event) { - $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']); - $i = 0; - foreach($parameterNamesAndDefault as $pName => $pDefault) - { - if(isset($parameterValues[$i])) - { - $currentValue = $parameterValues[$i]; - } - else - { - $currentValue = $pDefault; - } - $currentValue = $this->formatValue($currentValue); - $str .= "$pName = $currentValue, "; + $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']); + $i = 0; + foreach ($parameterNamesAndDefault as $pName => $pDefault) { + if (isset($parameterValues[$i])) { + $currentValue = $parameterValues[$i]; + } else { + $currentValue = $pDefault; + } + $currentValue = $this->formatValue($currentValue); + $str .= "$pName = $currentValue, "; - $i++; - } - $str .= "\n<br /> "; - $str .= "\n<br /> "; - return parent::format($str); + $i++; + } + $str .= "\n<br /> "; + $str .= "\n<br /> "; + return parent::format($str); } - /** - * Converts the given value to a string - * - * @param mixed $value - * @return string - */ - private function formatValue( $value ) + /** + * Converts the given value to a string + * + * @param mixed $value + * @return string + */ + private function formatValue($value) { - if(is_string($value)) - { - $value = "'$value'"; - } - if(is_null($value)) - { - $value= 'null'; - } - if(is_array($value)) - { - $value = "array( ".implode(", ", $value). ")"; - } - return $value; + if (is_string($value)) { + $value = "'$value'"; + } + if (is_null($value)) { + $value = 'null'; + } + if (is_array($value)) { + $value = "array( " . implode(", ", $value) . ")"; + } + return $value; } } diff --git a/core/Log/Error.php b/core/Log/Error.php index a424691a23..bd54c8e6f0 100644 --- a/core/Log/Error.php +++ b/core/Log/Error.php @@ -1,141 +1,172 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Class used to log an error event. - * + * * @package Piwik * @subpackage Piwik_Log */ class Piwik_Log_Error extends Piwik_Log { - const ID = 'logger_error'; + const ID = 'logger_error'; - /** - * Constructor - */ - function __construct() - { - $logToFileFilename = self::ID; - $logToDatabaseTableName = self::ID; - $logToDatabaseColumnMapping = array( - 'timestamp' => 'timestamp', - 'message' => 'message', - 'errno' => 'errno', - 'errline' => 'errline', - 'errfile' => 'errfile', - 'backtrace' => 'backtrace' - ); - $screenFormatter = new Piwik_Log_Error_Formatter_ScreenFormatter(); - $fileFormatter = new Piwik_Log_Formatter_FileFormatter(); - parent::__construct($logToFileFilename, - $fileFormatter, - $screenFormatter, - $logToDatabaseTableName, - $logToDatabaseColumnMapping ); - } + /** + * Constructor + */ + function __construct() + { + $logToFileFilename = self::ID; + $logToDatabaseTableName = self::ID; + $logToDatabaseColumnMapping = array( + 'timestamp' => 'timestamp', + 'message' => 'message', + 'errno' => 'errno', + 'errline' => 'errline', + 'errfile' => 'errfile', + 'backtrace' => 'backtrace' + ); + $screenFormatter = new Piwik_Log_Error_Formatter_ScreenFormatter(); + $fileFormatter = new Piwik_Log_Formatter_FileFormatter(); + parent::__construct($logToFileFilename, + $fileFormatter, + $screenFormatter, + $logToDatabaseTableName, + $logToDatabaseColumnMapping); + } - /** - * Adds the writer - */ - function addWriteToScreen() - { - parent::addWriteToScreen(); - $writerScreen = new Zend_Log_Writer_Stream('php://stderr'); - $writerScreen->setFormatter( $this->screenFormatter ); - $this->addWriter($writerScreen); - } + /** + * Adds the writer + */ + function addWriteToScreen() + { + parent::addWriteToScreen(); + $writerScreen = new Zend_Log_Writer_Stream('php://stderr'); + $writerScreen->setFormatter($this->screenFormatter); + $this->addWriter($writerScreen); + } - /** - * Logs the given error event - * - * @param int $errno - * @param string $errstr - * @param string $errfile - * @param int $errline - * @param string $backtrace - */ - public function logEvent($errno, $errstr, $errfile, $errline, $backtrace) - { - $event = array(); - $event['errno'] = $errno; - $event['message'] = $errstr; - $event['errfile'] = $errfile; - $event['errline'] = $errline; - $event['backtrace'] = $backtrace; + /** + * Logs the given error event + * + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param int $errline + * @param string $backtrace + */ + public function logEvent($errno, $errstr, $errfile, $errline, $backtrace) + { + $event = array(); + $event['errno'] = $errno; + $event['message'] = $errstr; + $event['errfile'] = $errfile; + $event['errline'] = $errline; + $event['backtrace'] = $backtrace; - parent::log($event, Piwik_Log::ERR, null); - } + parent::log($event, Piwik_Log::ERR, null); + } } /** * Format an error event to be displayed on the screen. - * + * * @package Piwik * @subpackage Piwik_Log */ class Piwik_Log_Error_Formatter_ScreenFormatter extends Piwik_Log_Formatter_ScreenFormatter { - /** + /** * Formats data into a single line to be written by the writer. * - * @param array $event event data + * @param array $event event data * @return string formatted line to write to the log */ public function format($event) { - $event = parent::formatEvent($event); - - $errno = $event['errno'] ; - $errstr = $event['message'] ; - $errfile = $event['errfile'] ; - $errline = $event['errline'] ; - $backtrace = $event['backtrace'] ; - - $strReturned = ''; - $errno = $errno & error_reporting(); - - // problem when using error_reporting with the @ silent fail operator - // it gives an errno 0, and in this case the objective is to NOT display anything on the screen! - // is there any other case where the errno is zero at this point? - if($errno == 0) return ''; - $strReturned .= "\n<div style='word-wrap: break-word; border: 3px solid red; padding:4px; width:70%; background-color:#FFFF96;'> - <strong>There is an error. Please report the message (Piwik ". (class_exists('Piwik_Version') ? Piwik_Version::VERSION : '' ) .") + $event = parent::formatEvent($event); + + $errno = $event['errno']; + $errstr = $event['message']; + $errfile = $event['errfile']; + $errline = $event['errline']; + $backtrace = $event['backtrace']; + + $strReturned = ''; + $errno = $errno & error_reporting(); + + // problem when using error_reporting with the @ silent fail operator + // it gives an errno 0, and in this case the objective is to NOT display anything on the screen! + // is there any other case where the errno is zero at this point? + if ($errno == 0) return ''; + $strReturned .= "\n<div style='word-wrap: break-word; border: 3px solid red; padding:4px; width:70%; background-color:#FFFF96;'> + <strong>There is an error. Please report the message (Piwik " . (class_exists('Piwik_Version') ? Piwik_Version::VERSION : '') . ") and full backtrace in the <a href='?module=Proxy&action=redirect&url=http://forum.piwik.org' target='_blank'>Piwik forums</a> (please do a Search first as it might have been reported already!).<br /><br/> "; - switch($errno) - { - case E_ERROR: $strReturned .= "Error"; break; - case E_WARNING: $strReturned .= "Warning"; break; - case E_PARSE: $strReturned .= "Parse Error"; break; - case E_NOTICE: $strReturned .= "Notice"; break; - case E_CORE_ERROR: $strReturned .= "Core Error"; break; - case E_CORE_WARNING: $strReturned .= "Core Warning"; break; - case E_COMPILE_ERROR: $strReturned .= "Compile Error"; break; - case E_COMPILE_WARNING: $strReturned .= "Compile Warning"; break; - case E_USER_ERROR: $strReturned .= "User Error"; break; - case E_USER_WARNING: $strReturned .= "User Warning"; break; - case E_USER_NOTICE: $strReturned .= "User Notice"; break; - case E_STRICT: $strReturned .= "Strict Notice"; break; - case E_RECOVERABLE_ERROR: $strReturned .= "Recoverable Error"; break; - case E_DEPRECATED: $strReturned .= "Deprecated"; break; - case E_USER_DEPRECATED: $strReturned .= "User Deprecated"; break; - default: $strReturned .= "Unknown error ($errno)"; break; - } - $strReturned .= ":</strong> <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(array("\n",'#'), array("<br />\n","<br />\n#"), $backtrace); - $strReturned .= "</div><br />"; - $strReturned .= "\n </pre></div><br />"; - - return parent::format($strReturned); + switch ($errno) { + case E_ERROR: + $strReturned .= "Error"; + break; + case E_WARNING: + $strReturned .= "Warning"; + break; + case E_PARSE: + $strReturned .= "Parse Error"; + break; + case E_NOTICE: + $strReturned .= "Notice"; + break; + case E_CORE_ERROR: + $strReturned .= "Core Error"; + break; + case E_CORE_WARNING: + $strReturned .= "Core Warning"; + break; + case E_COMPILE_ERROR: + $strReturned .= "Compile Error"; + break; + case E_COMPILE_WARNING: + $strReturned .= "Compile Warning"; + break; + case E_USER_ERROR: + $strReturned .= "User Error"; + break; + case E_USER_WARNING: + $strReturned .= "User Warning"; + break; + case E_USER_NOTICE: + $strReturned .= "User Notice"; + break; + case E_STRICT: + $strReturned .= "Strict Notice"; + break; + case E_RECOVERABLE_ERROR: + $strReturned .= "Recoverable Error"; + break; + case E_DEPRECATED: + $strReturned .= "Deprecated"; + break; + case E_USER_DEPRECATED: + $strReturned .= "User Deprecated"; + break; + default: + $strReturned .= "Unknown error ($errno)"; + break; + } + $strReturned .= ":</strong> <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(array("\n", '#'), array("<br />\n", "<br />\n#"), $backtrace); + $strReturned .= "</div><br />"; + $strReturned .= "\n </pre></div><br />"; + + return parent::format($strReturned); } } diff --git a/core/Log/Exception.php b/core/Log/Exception.php index 5a2cb3e2ef..b18233b3e2 100644 --- a/core/Log/Exception.php +++ b/core/Log/Exception.php @@ -18,60 +18,60 @@ */ class Piwik_Log_Exception extends Piwik_Log { - const ID = 'logger_exception'; + const ID = 'logger_exception'; - /** - * Constructor - */ - function __construct() - { - $logToFileFilename = self::ID; - $logToDatabaseTableName = self::ID; - $logToDatabaseColumnMapping = array( - 'timestamp' => 'timestamp', - 'message' => 'message', - 'errno' => 'errno', - 'errline' => 'errline', - 'errfile' => 'errfile', - 'backtrace' => 'backtrace' - ); - $screenFormatter = new Piwik_Log_Exception_Formatter_ScreenFormatter(); - $fileFormatter = new Piwik_Log_Formatter_FileFormatter(); + /** + * Constructor + */ + function __construct() + { + $logToFileFilename = self::ID; + $logToDatabaseTableName = self::ID; + $logToDatabaseColumnMapping = array( + 'timestamp' => 'timestamp', + 'message' => 'message', + 'errno' => 'errno', + 'errline' => 'errline', + 'errfile' => 'errfile', + 'backtrace' => 'backtrace' + ); + $screenFormatter = new Piwik_Log_Exception_Formatter_ScreenFormatter(); + $fileFormatter = new Piwik_Log_Formatter_FileFormatter(); - parent::__construct($logToFileFilename, - $fileFormatter, - $screenFormatter, - $logToDatabaseTableName, - $logToDatabaseColumnMapping ); - } + parent::__construct($logToFileFilename, + $fileFormatter, + $screenFormatter, + $logToDatabaseTableName, + $logToDatabaseColumnMapping); + } - /** - * Adds the writer - */ - function addWriteToScreen() - { - parent::addWriteToScreen(); - $writerScreen = new Zend_Log_Writer_Stream('php://stderr'); - $writerScreen->setFormatter( $this->screenFormatter ); - $this->addWriter($writerScreen); - } + /** + * Adds the writer + */ + function addWriteToScreen() + { + parent::addWriteToScreen(); + $writerScreen = new Zend_Log_Writer_Stream('php://stderr'); + $writerScreen->setFormatter($this->screenFormatter); + $this->addWriter($writerScreen); + } - /** - * Logs the given exception event - * - * @param Exception $exception - */ - public function logEvent($exception) - { - $event = array(); - $event['errno'] = $exception->getCode(); - $event['message'] = $exception->getMessage(); - $event['errfile'] = $exception->getFile(); - $event['errline'] = $exception->getLine(); - $event['backtrace'] = $exception->getTraceAsString(); + /** + * Logs the given exception event + * + * @param Exception $exception + */ + public function logEvent($exception) + { + $event = array(); + $event['errno'] = $exception->getCode(); + $event['message'] = $exception->getMessage(); + $event['errfile'] = $exception->getFile(); + $event['errline'] = $exception->getLine(); + $event['backtrace'] = $exception->getTraceAsString(); - parent::log($event, Piwik_Log::CRIT, null); - } + parent::log($event, Piwik_Log::CRIT, null); + } } /** @@ -82,20 +82,20 @@ class Piwik_Log_Exception extends Piwik_Log */ class Piwik_Log_Exception_Formatter_ScreenFormatter extends Piwik_Log_Formatter_ScreenFormatter { - /** - * Formats data into a single line to be written by the writer. - * - * @param array $event event data - * @return string formatted line to write to the log - */ - public function format($event) - { - $event = parent::formatEvent($event); - $errstr = $event['message'] ; + /** + * Formats data into a single line to be written by the writer. + * + * @param array $event event data + * @return string formatted line to write to the log + */ + public function format($event) + { + $event = parent::formatEvent($event); + $errstr = $event['message']; - $outputFormat = strtolower(Piwik_Common::getRequestVar('format', 'html', 'string')); - $response = new Piwik_API_ResponseBuilder($outputFormat); - $message = $response->getResponseException(new Exception($errstr)); - return parent::format($message ); - } + $outputFormat = strtolower(Piwik_Common::getRequestVar('format', 'html', 'string')); + $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 23a6777e69..5e8c428e64 100644 --- a/core/Log/Message.php +++ b/core/Log/Message.php @@ -1,96 +1,91 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Class used to log a standard message event. - * + * * @package Piwik * @subpackage Piwik_Log */ class Piwik_Log_Message extends Piwik_Log { - const ID = 'logger_message'; + const ID = 'logger_message'; - /** - * Constructor - */ - function __construct() - { - $logToFileFilename = self::ID.".htm"; - $logToDatabaseTableName = self::ID; - $logToDatabaseColumnMapping = array( - 'message' => 'message', - 'timestamp' => 'timestamp' - ); - $screenFormatter = new Piwik_Log_Message_Formatter_ScreenFormatter(); - $fileFormatter = new Piwik_Log_Formatter_FileFormatter(); + /** + * Constructor + */ + function __construct() + { + $logToFileFilename = self::ID . ".htm"; + $logToDatabaseTableName = self::ID; + $logToDatabaseColumnMapping = array( + 'message' => 'message', + 'timestamp' => 'timestamp' + ); + $screenFormatter = new Piwik_Log_Message_Formatter_ScreenFormatter(); + $fileFormatter = new Piwik_Log_Formatter_FileFormatter(); - parent::__construct($logToFileFilename, - $fileFormatter, - $screenFormatter, - $logToDatabaseTableName, - $logToDatabaseColumnMapping ); - } + parent::__construct($logToFileFilename, + $fileFormatter, + $screenFormatter, + $logToDatabaseTableName, + $logToDatabaseColumnMapping); + } - /** - * Logs the given message - * - * @param string $message - */ - public function logEvent($message) - { - $event = array(); - $event['message'] = $message; - parent::log($event, Piwik_Log::INFO, null); - } + /** + * Logs the given message + * + * @param string $message + */ + public function logEvent($message) + { + $event = array(); + $event['message'] = $message; + parent::log($event, Piwik_Log::INFO, null); + } } /** * Format a standard message event to be displayed on the screen. * The message can be a PHP array or a string. - * + * * @package Piwik * @subpackage Piwik_Log */ -class Piwik_Log_Message_Formatter_ScreenFormatter extends Piwik_Log_Formatter_ScreenFormatter +class Piwik_Log_Message_Formatter_ScreenFormatter extends Piwik_Log_Formatter_ScreenFormatter { - /** + /** * Formats data into a single line to be written by the writer. * - * @param array $event event data + * @param array $event event data * @return string formatted line to write to the log */ public function format($event) { - if(is_array($event['message'])) - { - $message = "<pre>".var_export($event['message'], true)."</pre>"; - } - else - { - $message = $event['message']; - } - if(!Piwik_Common::isPhpCliMode()) - { - $message .= "<br/>"; - } - $message .= "\n"; - - $memory = ''; - // Hacky: let's hide the memory usage in CLI to hide from the archive.php output - if(!Piwik_Common::isPhpCliMode()) - { - $memory = '['.Piwik::getMemoryUsage(). '] '; - } - $message = '['. $event['timestamp'] . '] ['.$event['requestKey'].'] ' .$memory.$message; - return parent::format($message); + if (is_array($event['message'])) { + $message = "<pre>" . var_export($event['message'], true) . "</pre>"; + } else { + $message = $event['message']; + } + if (!Piwik_Common::isPhpCliMode()) { + $message .= "<br/>"; + } + $message .= "\n"; + + $memory = ''; + // Hacky: let's hide the memory usage in CLI to hide from the archive.php output + if (!Piwik_Common::isPhpCliMode()) { + $memory = '[' . Piwik::getMemoryUsage() . '] '; + } + $message = '[' . $event['timestamp'] . '] [' . $event['requestKey'] . '] ' . $memory . $message; + return parent::format($message); } } diff --git a/core/Mail.php b/core/Mail.php index 583266e870..833e01fdb6 100644 --- a/core/Mail.php +++ b/core/Mail.php @@ -4,78 +4,78 @@ * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * Class for sending mails, for more information see: + * Class for sending mails, for more information see: * * @package Piwik * @see Zend_Mail, libs/Zend/Mail.php - * @link http://framework.zend.com/manual/en/zend.mail.html + * @link http://framework.zend.com/manual/en/zend.mail.html */ class Piwik_Mail extends Zend_Mail { - /** - * Default charset utf-8 - * - * @param string $charset charset, defaults to utf-8 - */ - public function __construct($charset = 'utf-8') - { - parent::__construct($charset); - $this->initSmtpTransport(); - } + /** + * Default charset utf-8 + * + * @param string $charset charset, defaults to utf-8 + */ + public function __construct($charset = 'utf-8') + { + parent::__construct($charset); + $this->initSmtpTransport(); + } + + /** + * Sets the sender to use + * + * @param string $email + * @param null|string $name + */ + public function setFrom($email, $name = null) + { + $hostname = Piwik_Config::getInstance()->mail['defaultHostnameIfEmpty']; + $piwikHost = Piwik_Url::getCurrentHost($hostname); + + // If known Piwik URL, use it instead of "localhost" + $piwikUrl = Piwik::getPiwikUrl(); + $url = parse_url($piwikUrl); + if (isset($url['host']) + && $url['host'] != 'localhost' + && $url['host'] != '127.0.0.1' + ) { + $piwikHost = $url['host']; + } + $email = str_replace('{DOMAIN}', $piwikHost, $email); + parent::setFrom($email, $name); + } - /** - * Sets the sender to use - * - * @param string $email - * @param null|string $name - */ - public function setFrom($email, $name = null) - { - $hostname = Piwik_Config::getInstance()->mail['defaultHostnameIfEmpty']; - $piwikHost = Piwik_Url::getCurrentHost($hostname); - - // If known Piwik URL, use it instead of "localhost" - $piwikUrl = Piwik::getPiwikUrl(); - $url = parse_url($piwikUrl); - if(isset($url['host']) - && $url['host'] != 'localhost' - && $url['host'] != '127.0.0.1') - { - $piwikHost = $url['host']; - } - $email = str_replace('{DOMAIN}', $piwikHost, $email); - parent::setFrom($email, $name); - } + /** + * @return void + */ + private function initSmtpTransport() + { + $mailConfig = Piwik_Config::getInstance()->mail; + if (empty($mailConfig['host']) + || $mailConfig['transport'] != 'smtp' + ) { + return; + } + $smtpConfig = array(); + if (!empty($mailConfig['type'])) + $smtpConfig['auth'] = strtolower($mailConfig['type']); + if (!empty($mailConfig['username'])) + $smtpConfig['username'] = $mailConfig['username']; + if (!empty($mailConfig['password'])) + $smtpConfig['password'] = $mailConfig['password']; + if (!empty($mailConfig['encryption'])) + $smtpConfig['ssl'] = $mailConfig['encryption']; - /** - * @return void - */ - private function initSmtpTransport() - { - $mailConfig = Piwik_Config::getInstance()->mail; - if ( empty($mailConfig['host']) - || $mailConfig['transport'] != 'smtp') - { - return; - } - $smtpConfig = array(); - if (!empty($mailConfig['type'])) - $smtpConfig['auth'] = strtolower($mailConfig['type']); - if (!empty($mailConfig['username'])) - $smtpConfig['username'] = $mailConfig['username']; - if (!empty($mailConfig['password'])) - $smtpConfig['password'] = $mailConfig['password']; - if (!empty($mailConfig['encryption'])) - $smtpConfig['ssl'] = $mailConfig['encryption']; - - $tr = new Zend_Mail_Transport_Smtp($mailConfig['host'], $smtpConfig); - Piwik_Mail::setDefaultTransport($tr); - ini_set("smtp_port", $mailConfig['port']); - } + $tr = new Zend_Mail_Transport_Smtp($mailConfig['host'], $smtpConfig); + Piwik_Mail::setDefaultTransport($tr); + ini_set("smtp_port", $mailConfig['port']); + } } diff --git a/core/Menu/Abstract.php b/core/Menu/Abstract.php index 3a0b49c62b..553a34489e 100644 --- a/core/Menu/Abstract.php +++ b/core/Menu/Abstract.php @@ -15,231 +15,212 @@ abstract class Piwik_Menu_Abstract { - protected $menu = null; - protected $menuEntries = array(); - protected $edits = array(); - protected $renames = array(); - protected $orderingApplied = false; - - /* - * Can't enforce static function in 5.2. - */ - //abstract static public function getInstance(); - - /** - * Builds the menu, applies edits, renames - * and orders the entries. - * - * @return Array - */ - public function get() - { - $this->buildMenu(); - $this->applyEdits(); - $this->applyRenames(); - $this->applyOrdering(); - return $this->menu; - } - - /** - * Adds a new entry to the menu. - * - * @param string $menuName - * @param string $subMenuName - * @param string $url - * @param bool $displayedForCurrentUser - * @param int $order - * @param string $tooltip Tooltip to display. - */ - public function add($menuName, $subMenuName, $url, $displayedForCurrentUser, $order = 50, $tooltip = false) - { - if($displayedForCurrentUser) - { - // make sure the idSite value used is numeric (hack-y fix for #3426) - if (!is_numeric(Piwik_Common::getRequestVar('idSite', false))) - { - $idSites = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess(); - $url['idSite'] = reset($idSites); - } - - $this->menuEntries[] = array( - $menuName, - $subMenuName, - $url, - $order, - $tooltip - ); - } - } - - /** - * Builds a single menu item - * - * @param string $menuName - * @param string $subMenuName - * @param string $url - * @param int $order - * @param string $tooltip Tooltip to display. - */ - private function buildMenuItem($menuName, $subMenuName, $url, $order = 50, $tooltip = false) - { - if (!isset($this->menu[$menuName]) || empty($subMenuName)) - { - $this->menu[$menuName]['_url'] = $url; - if(empty($subMenuName)) { - $this->menu[$menuName]['_order'] = $order; - } - $this->menu[$menuName]['_name'] = $menuName; - $this->menu[$menuName]['_hasSubmenu'] = false; - $this->menu[$menuName]['_tooltip'] = $tooltip; - } - if (!empty($subMenuName)) - { - $this->menu[$menuName][$subMenuName]['_url'] = $url; - $this->menu[$menuName][$subMenuName]['_order'] = $order; - $this->menu[$menuName][$subMenuName]['_name'] = $subMenuName; - $this->menu[$menuName]['_hasSubmenu'] = true; - $this->menu[$menuName]['_tooltip'] = $tooltip; - } - } - - /** - * Builds the menu from the $this->menuEntries variable. - */ - private function buildMenu() - { - foreach ($this->menuEntries as $menuEntry) - { - $this->buildMenuItem($menuEntry[0], $menuEntry[1], $menuEntry[2], $menuEntry[3], $menuEntry[4]); - } - } - - /** - * Renames a single menu entry. - * - * @param $mainMenuOriginal - * @param $subMenuOriginal - * @param $mainMenuRenamed - * @param $subMenuRenamed - */ - public function rename($mainMenuOriginal, $subMenuOriginal, $mainMenuRenamed, $subMenuRenamed) - { - $this->renames[] = array($mainMenuOriginal, $subMenuOriginal, - $mainMenuRenamed, $subMenuRenamed); - } - - /** - * Edits a URL of an existing menu entry. - * - * @param $mainMenuToEdit - * @param $subMenuToEdit - * @param $newUrl - */ - public function editUrl($mainMenuToEdit, $subMenuToEdit, $newUrl) - { - $this->edits[] = array($mainMenuToEdit, $subMenuToEdit, $newUrl); - } - - /** - * Applies all edits to the menu. - */ - private function applyEdits() - { - foreach ($this->edits as $edit) - { - $mainMenuToEdit = $edit[0]; - $subMenuToEdit = $edit[1]; - $newUrl = $edit[2]; - if (!isset($this->menu[$mainMenuToEdit][$subMenuToEdit])) - { - $this->buildMenuItem($mainMenuToEdit, $subMenuToEdit, $newUrl); - } - else - { - $this->menu[$mainMenuToEdit][$subMenuToEdit]['_url'] = $newUrl; - } - } - } - - /** - * Applies renames to the menu. - */ - private function applyRenames() - { - foreach ($this->renames as $rename) - { - $mainMenuOriginal = $rename[0]; - $subMenuOriginal = $rename[1]; - $mainMenuRenamed = $rename[2]; - $subMenuRenamed = $rename[3]; - // Are we changing a submenu? - if (!empty($subMenuOriginal)) - { - if (isset($this->menu[$mainMenuOriginal][$subMenuOriginal])) - { - $save = $this->menu[$mainMenuOriginal][$subMenuOriginal]; - $save['_name'] = $subMenuRenamed; - unset($this->menu[$mainMenuOriginal][$subMenuOriginal]); - $this->menu[$mainMenuRenamed][$subMenuRenamed] = $save; - } - } - // Changing a first-level element - else if (isset($this->menu[$mainMenuOriginal])) - { - $save = $this->menu[$mainMenuOriginal]; - $save['_name'] = $mainMenuRenamed; - unset($this->menu[$mainMenuOriginal]); - $this->menu[$mainMenuRenamed] = $save; - } - } - } - - /** - * Orders the menu according to their order. - */ - private function applyOrdering() - { - if (empty($this->menu) - || $this->orderingApplied) - { - return; - } - - uasort($this->menu, array($this, 'menuCompare')); - foreach ($this->menu as $key => &$element) - { - if (is_null($element)) - { - unset($this->menu[$key]); - } - else if ($element['_hasSubmenu']) - { - uasort($element, array($this, 'menuCompare')); - } - } - - $this->orderingApplied = true; - } - - /** - * Compares two menu entries. Used for ordering. - * - * @param array $itemOne - * @param array $itemTwo - * @return boolean - */ - protected function menuCompare($itemOne, $itemTwo) - { - if (!is_array($itemOne) || !is_array($itemTwo) - || !isset($itemOne['_order']) || !isset($itemTwo['_order'])) - { - return 0; - } - - if ($itemOne['_order'] == $itemTwo['_order']) - { - return strcmp($itemOne['_name'], $itemTwo['_name']); - } - return ($itemOne['_order'] < $itemTwo['_order']) ? -1 : 1; - } + protected $menu = null; + protected $menuEntries = array(); + protected $edits = array(); + protected $renames = array(); + protected $orderingApplied = false; + + /* + * Can't enforce static function in 5.2. + */ + //abstract static public function getInstance(); + + /** + * Builds the menu, applies edits, renames + * and orders the entries. + * + * @return Array + */ + public function get() + { + $this->buildMenu(); + $this->applyEdits(); + $this->applyRenames(); + $this->applyOrdering(); + return $this->menu; + } + + /** + * Adds a new entry to the menu. + * + * @param string $menuName + * @param string $subMenuName + * @param string $url + * @param bool $displayedForCurrentUser + * @param int $order + * @param string $tooltip Tooltip to display. + */ + public function add($menuName, $subMenuName, $url, $displayedForCurrentUser, $order = 50, $tooltip = false) + { + if ($displayedForCurrentUser) { + // make sure the idSite value used is numeric (hack-y fix for #3426) + if (!is_numeric(Piwik_Common::getRequestVar('idSite', false))) { + $idSites = Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess(); + $url['idSite'] = reset($idSites); + } + + $this->menuEntries[] = array( + $menuName, + $subMenuName, + $url, + $order, + $tooltip + ); + } + } + + /** + * Builds a single menu item + * + * @param string $menuName + * @param string $subMenuName + * @param string $url + * @param int $order + * @param string $tooltip Tooltip to display. + */ + private function buildMenuItem($menuName, $subMenuName, $url, $order = 50, $tooltip = false) + { + if (!isset($this->menu[$menuName]) || empty($subMenuName)) { + $this->menu[$menuName]['_url'] = $url; + if (empty($subMenuName)) { + $this->menu[$menuName]['_order'] = $order; + } + $this->menu[$menuName]['_name'] = $menuName; + $this->menu[$menuName]['_hasSubmenu'] = false; + $this->menu[$menuName]['_tooltip'] = $tooltip; + } + if (!empty($subMenuName)) { + $this->menu[$menuName][$subMenuName]['_url'] = $url; + $this->menu[$menuName][$subMenuName]['_order'] = $order; + $this->menu[$menuName][$subMenuName]['_name'] = $subMenuName; + $this->menu[$menuName]['_hasSubmenu'] = true; + $this->menu[$menuName]['_tooltip'] = $tooltip; + } + } + + /** + * Builds the menu from the $this->menuEntries variable. + */ + private function buildMenu() + { + foreach ($this->menuEntries as $menuEntry) { + $this->buildMenuItem($menuEntry[0], $menuEntry[1], $menuEntry[2], $menuEntry[3], $menuEntry[4]); + } + } + + /** + * Renames a single menu entry. + * + * @param $mainMenuOriginal + * @param $subMenuOriginal + * @param $mainMenuRenamed + * @param $subMenuRenamed + */ + public function rename($mainMenuOriginal, $subMenuOriginal, $mainMenuRenamed, $subMenuRenamed) + { + $this->renames[] = array($mainMenuOriginal, $subMenuOriginal, + $mainMenuRenamed, $subMenuRenamed); + } + + /** + * Edits a URL of an existing menu entry. + * + * @param $mainMenuToEdit + * @param $subMenuToEdit + * @param $newUrl + */ + public function editUrl($mainMenuToEdit, $subMenuToEdit, $newUrl) + { + $this->edits[] = array($mainMenuToEdit, $subMenuToEdit, $newUrl); + } + + /** + * Applies all edits to the menu. + */ + private function applyEdits() + { + foreach ($this->edits as $edit) { + $mainMenuToEdit = $edit[0]; + $subMenuToEdit = $edit[1]; + $newUrl = $edit[2]; + if (!isset($this->menu[$mainMenuToEdit][$subMenuToEdit])) { + $this->buildMenuItem($mainMenuToEdit, $subMenuToEdit, $newUrl); + } else { + $this->menu[$mainMenuToEdit][$subMenuToEdit]['_url'] = $newUrl; + } + } + } + + /** + * Applies renames to the menu. + */ + private function applyRenames() + { + foreach ($this->renames as $rename) { + $mainMenuOriginal = $rename[0]; + $subMenuOriginal = $rename[1]; + $mainMenuRenamed = $rename[2]; + $subMenuRenamed = $rename[3]; + // Are we changing a submenu? + if (!empty($subMenuOriginal)) { + if (isset($this->menu[$mainMenuOriginal][$subMenuOriginal])) { + $save = $this->menu[$mainMenuOriginal][$subMenuOriginal]; + $save['_name'] = $subMenuRenamed; + unset($this->menu[$mainMenuOriginal][$subMenuOriginal]); + $this->menu[$mainMenuRenamed][$subMenuRenamed] = $save; + } + } // Changing a first-level element + else if (isset($this->menu[$mainMenuOriginal])) { + $save = $this->menu[$mainMenuOriginal]; + $save['_name'] = $mainMenuRenamed; + unset($this->menu[$mainMenuOriginal]); + $this->menu[$mainMenuRenamed] = $save; + } + } + } + + /** + * Orders the menu according to their order. + */ + private function applyOrdering() + { + if (empty($this->menu) + || $this->orderingApplied + ) { + return; + } + + uasort($this->menu, array($this, 'menuCompare')); + foreach ($this->menu as $key => &$element) { + if (is_null($element)) { + unset($this->menu[$key]); + } else if ($element['_hasSubmenu']) { + uasort($element, array($this, 'menuCompare')); + } + } + + $this->orderingApplied = true; + } + + /** + * Compares two menu entries. Used for ordering. + * + * @param array $itemOne + * @param array $itemTwo + * @return boolean + */ + protected function menuCompare($itemOne, $itemTwo) + { + if (!is_array($itemOne) || !is_array($itemTwo) + || !isset($itemOne['_order']) || !isset($itemTwo['_order']) + ) { + return 0; + } + + if ($itemOne['_order'] == $itemTwo['_order']) { + return strcmp($itemOne['_name'], $itemTwo['_name']); + } + return ($itemOne['_order'] < $itemTwo['_order']) ? -1 : 1; + } } diff --git a/core/Menu/Admin.php b/core/Menu/Admin.php index 9463e2291d..f85259811e 100644 --- a/core/Menu/Admin.php +++ b/core/Menu/Admin.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik_Menu */ @@ -14,33 +14,31 @@ */ class Piwik_Menu_Admin extends Piwik_Menu_Abstract { - static private $instance = null; + static private $instance = null; - /** - * @return Piwik_Menu_Admin - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } + /** + * @return Piwik_Menu_Admin + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } - /** - * Triggers the AdminMenu.add hook and returns the menu. - * - * @return Array - */ - public function get() - { - if(!$this->menu) - { - Piwik_PostEvent('AdminMenu.add'); - } - return parent::get(); - } + /** + * Triggers the AdminMenu.add hook and returns the menu. + * + * @return Array + */ + public function get() + { + if (!$this->menu) { + Piwik_PostEvent('AdminMenu.add'); + } + return parent::get(); + } } /** @@ -50,21 +48,20 @@ class Piwik_Menu_Admin extends Piwik_Menu_Abstract */ function Piwik_GetCurrentAdminMenuName() { - $menu = Piwik_GetAdminMenu(); - $currentModule = Piwik::getModule(); - $currentAction = Piwik::getAction(); - foreach($menu as $name => $submenu) - { - foreach($submenu as $subMenuName => $parameters) { - if(strpos($subMenuName, '_') !== 0 && - $parameters['_url']['module'] == $currentModule - && $parameters['_url']['action'] == $currentAction) - { - return $subMenuName; - } - } - } - return false; + $menu = Piwik_GetAdminMenu(); + $currentModule = Piwik::getModule(); + $currentAction = Piwik::getAction(); + foreach ($menu as $name => $submenu) { + foreach ($submenu as $subMenuName => $parameters) { + if (strpos($subMenuName, '_') !== 0 && + $parameters['_url']['module'] == $currentModule + && $parameters['_url']['action'] == $currentAction + ) { + return $subMenuName; + } + } + } + return false; } /** @@ -74,43 +71,43 @@ function Piwik_GetCurrentAdminMenuName() */ function Piwik_GetAdminMenu() { - return Piwik_Menu_Admin::getInstance()->get(); + return Piwik_Menu_Admin::getInstance()->get(); } /** * Adds a new AdminMenu entry. * - * @param string $adminMenuName - * @param string $url - * @param boolean $displayedForCurrentUser - * @param int $order + * @param string $adminMenuName + * @param string $url + * @param boolean $displayedForCurrentUser + * @param int $order */ -function Piwik_AddAdminMenu( $adminMenuName, $url, $displayedForCurrentUser = true, $order = 10 ) +function Piwik_AddAdminMenu($adminMenuName, $url, $displayedForCurrentUser = true, $order = 10) { - Piwik_Menu_Admin::getInstance()->add('General_Settings', $adminMenuName, $url, $displayedForCurrentUser, $order); + Piwik_Menu_Admin::getInstance()->add('General_Settings', $adminMenuName, $url, $displayedForCurrentUser, $order); } /** * Adds a new AdminMenu entry with a submenu. * - * @param string $adminMenuName - * @param string $adminSubMenuName - * @param string $url - * @param boolean $displayedForCurrentUser - * @param int $order + * @param string $adminMenuName + * @param string $adminSubMenuName + * @param string $url + * @param boolean $displayedForCurrentUser + * @param int $order */ -function Piwik_AddAdminSubMenu( $adminMenuName, $adminSubMenuName, $url, $displayedForCurrentUser = true, $order = 10 ) +function Piwik_AddAdminSubMenu($adminMenuName, $adminSubMenuName, $url, $displayedForCurrentUser = true, $order = 10) { - Piwik_Menu_Admin::getInstance()->add($adminMenuName, $adminSubMenuName, $url, $displayedForCurrentUser, $order); + Piwik_Menu_Admin::getInstance()->add($adminMenuName, $adminSubMenuName, $url, $displayedForCurrentUser, $order); } /** * Renames an AdminMenu entry. * - * @param string $adminMenuOriginal - * @param string $adminMenuRenamed + * @param string $adminMenuOriginal + * @param string $adminMenuRenamed */ function Piwik_RenameAdminMenuEntry($adminMenuOriginal, $adminMenuRenamed) { - Piwik_Menu_Admin::getInstance()->rename($adminMenuOriginal, null, $adminMenuRenamed, null); + Piwik_Menu_Admin::getInstance()->rename($adminMenuOriginal, null, $adminMenuRenamed, null); } diff --git a/core/Menu/Main.php b/core/Menu/Main.php index 66eb237c5d..fa21883a30 100644 --- a/core/Menu/Main.php +++ b/core/Menu/Main.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik_Menu */ @@ -14,69 +14,64 @@ */ class Piwik_Menu_Main extends Piwik_Menu_Abstract { - static private $instance = null; - - /** - * @return Piwik_Menu_Abstract - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } + static private $instance = null; + + /** + * @return Piwik_Menu_Abstract + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Returns if the URL was found in the menu. + * + * @param string $url + * @return boolean + */ + public function isUrlFound($url) + { + $menu = Piwik_Menu_Main::getInstance()->get(); - /** - * Returns if the URL was found in the menu. - * - * @param string $url - * @return boolean - */ - public function isUrlFound($url) - { - $menu = Piwik_Menu_Main::getInstance()->get(); - - foreach($menu as $mainMenuName => $subMenus) - { - foreach($subMenus as $subMenuName => $menuUrl) - { - if(strpos($subMenuName, '_') !== 0 && $menuUrl['_url'] == $url) - { - return true; - } - } - } - return false; - } + foreach ($menu as $mainMenuName => $subMenus) { + foreach ($subMenus as $subMenuName => $menuUrl) { + if (strpos($subMenuName, '_') !== 0 && $menuUrl['_url'] == $url) { + return true; + } + } + } + return false; + } + + /** + * Triggers the Menu.add hook and returns the menu. + * + * @return Array + */ + public function get() + { + // We trigger the Event only once! + if (!$this->menu) { + Piwik_PostEvent('Menu.add'); + } + return parent::get(); + } - /** - * Triggers the Menu.add hook and returns the menu. - * - * @return Array - */ - public function get() - { - // We trigger the Event only once! - if(!$this->menu) - { - Piwik_PostEvent('Menu.add'); - } - return parent::get(); - } - } /** * Checks if an entry uses the URL $url. - * - * @param string $url + * + * @param string $url * @return boolean */ function Piwik_IsMenuUrlFound($url) { - return Piwik_Menu_Main::getInstance()->isUrlFound($url); + return Piwik_Menu_Main::getInstance()->isUrlFound($url); } /** @@ -86,45 +81,45 @@ function Piwik_IsMenuUrlFound($url) */ function Piwik_GetMenu() { - return Piwik_Menu_Main::getInstance()->get(); + return Piwik_Menu_Main::getInstance()->get(); } /** * Adds a new entry to the MainMenu. * - * @param string $mainMenuName - * @param string $subMenuName - * @param string $url - * @param boolean $displayedForCurrentUser - * @param int $order + * @param string $mainMenuName + * @param string $subMenuName + * @param string $url + * @param boolean $displayedForCurrentUser + * @param int $order */ -function Piwik_AddMenu( $mainMenuName, $subMenuName, $url, $displayedForCurrentUser = true, $order = 10) +function Piwik_AddMenu($mainMenuName, $subMenuName, $url, $displayedForCurrentUser = true, $order = 10) { - Piwik_Menu_Main::getInstance()->add($mainMenuName, $subMenuName, $url, $displayedForCurrentUser, $order); + Piwik_Menu_Main::getInstance()->add($mainMenuName, $subMenuName, $url, $displayedForCurrentUser, $order); } /** * Renames a menu entry. - * - * @param string $mainMenuOriginal - * @param string $subMenuOriginal - * @param string $mainMenuRenamed - * @param string $subMenuRenamed + * + * @param string $mainMenuOriginal + * @param string $subMenuOriginal + * @param string $mainMenuRenamed + * @param string $subMenuRenamed */ -function Piwik_RenameMenuEntry($mainMenuOriginal, $subMenuOriginal, - $mainMenuRenamed, $subMenuRenamed) +function Piwik_RenameMenuEntry($mainMenuOriginal, $subMenuOriginal, + $mainMenuRenamed, $subMenuRenamed) { - Piwik_Menu_Main::getInstance()->rename($mainMenuOriginal, $subMenuOriginal, $mainMenuRenamed, $subMenuRenamed); + Piwik_Menu_Main::getInstance()->rename($mainMenuOriginal, $subMenuOriginal, $mainMenuRenamed, $subMenuRenamed); } /** * Edits the URL of a menu entry. - * - * @param string $mainMenuToEdit - * @param string $subMenuToEdit - * @param string $newUrl + * + * @param string $mainMenuToEdit + * @param string $subMenuToEdit + * @param string $newUrl */ -function Piwik_EditMenuUrl( $mainMenuToEdit, $subMenuToEdit, $newUrl ) +function Piwik_EditMenuUrl($mainMenuToEdit, $subMenuToEdit, $newUrl) { - Piwik_Menu_Main::getInstance()->editUrl($mainMenuToEdit, $subMenuToEdit, $newUrl); + Piwik_Menu_Main::getInstance()->editUrl($mainMenuToEdit, $subMenuToEdit, $newUrl); } diff --git a/core/Menu/Top.php b/core/Menu/Top.php index cbb768bb48..f24822ac26 100644 --- a/core/Menu/Top.php +++ b/core/Menu/Top.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik_Menu */ @@ -14,56 +14,52 @@ */ class Piwik_Menu_Top extends Piwik_Menu_Abstract { - static private $instance = null; + static private $instance = null; - /** - * @return Piwik_Menu_Top - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } + /** + * @return Piwik_Menu_Top + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } - /** - * Directly adds a menu entry containing html. - * - * @param string $menuName - * @param string $data - * @param boolean $displayedForCurrentUser - * @param int $order - * @param string $tooltip Tooltip to display. - */ - public function addHtml($menuName, $data, $displayedForCurrentUser, $order, $tooltip) - { - if($displayedForCurrentUser) - { - if(!isset($this->menu[$menuName])) - { - $this->menu[$menuName]['_html'] = $data; - $this->menu[$menuName]['_order'] = $order; - $this->menu[$menuName]['_hasSubmenu'] = false; - $this->menu[$menuName]['_tooltip'] = $tooltip; - } - } - } + /** + * Directly adds a menu entry containing html. + * + * @param string $menuName + * @param string $data + * @param boolean $displayedForCurrentUser + * @param int $order + * @param string $tooltip Tooltip to display. + */ + public function addHtml($menuName, $data, $displayedForCurrentUser, $order, $tooltip) + { + if ($displayedForCurrentUser) { + if (!isset($this->menu[$menuName])) { + $this->menu[$menuName]['_html'] = $data; + $this->menu[$menuName]['_order'] = $order; + $this->menu[$menuName]['_hasSubmenu'] = false; + $this->menu[$menuName]['_tooltip'] = $tooltip; + } + } + } - /** - * Triggers the TopMenu.add hook and returns the menu. - * - * @return Array - */ - public function get() - { - if(!$this->menu) - { - Piwik_PostEvent('TopMenu.add'); - } - return parent::get(); - } + /** + * Triggers the TopMenu.add hook and returns the menu. + * + * @return Array + */ + public function get() + { + if (!$this->menu) { + Piwik_PostEvent('TopMenu.add'); + } + return parent::get(); + } } /** @@ -73,39 +69,36 @@ class Piwik_Menu_Top extends Piwik_Menu_Abstract */ function Piwik_GetTopMenu() { - return Piwik_Menu_Top::getInstance()->get(); + return Piwik_Menu_Top::getInstance()->get(); } /** * Adds a new entry to the TopMenu. * - * @param string $topMenuName - * @param string $data - * @param boolean $displayedForCurrentUser - * @param int $order - * @param bool $isHTML - * @param string $tooltip Tooltip to display. + * @param string $topMenuName + * @param string $data + * @param boolean $displayedForCurrentUser + * @param int $order + * @param bool $isHTML + * @param string $tooltip Tooltip to display. */ -function Piwik_AddTopMenu( $topMenuName, $data, $displayedForCurrentUser = true, $order = 10, $isHTML = false, - $tooltip = false) +function Piwik_AddTopMenu($topMenuName, $data, $displayedForCurrentUser = true, $order = 10, $isHTML = false, + $tooltip = false) { - if($isHTML) - { - Piwik_Menu_Top::getInstance()->addHtml($topMenuName, $data, $displayedForCurrentUser, $order, $tooltip); - } - else - { - Piwik_Menu_Top::getInstance()->add($topMenuName, null, $data, $displayedForCurrentUser, $order, $tooltip); - } + if ($isHTML) { + Piwik_Menu_Top::getInstance()->addHtml($topMenuName, $data, $displayedForCurrentUser, $order, $tooltip); + } else { + Piwik_Menu_Top::getInstance()->add($topMenuName, null, $data, $displayedForCurrentUser, $order, $tooltip); + } } /** * Renames a entry of the TopMenu * - * @param string $topMenuOriginal - * @param string $topMenuRenamed + * @param string $topMenuOriginal + * @param string $topMenuRenamed */ function Piwik_RenameTopMenuEntry($topMenuOriginal, $topMenuRenamed) { - Piwik_Menu_Top::getInstance()->rename($topMenuOriginal, null, $topMenuRenamed, null); + Piwik_Menu_Top::getInstance()->rename($topMenuOriginal, null, $topMenuRenamed, null); } diff --git a/core/Nonce.php b/core/Nonce.php index b76cbd16e9..f7763ee15b 100644 --- a/core/Nonce.php +++ b/core/Nonce.php @@ -24,128 +24,121 @@ */ 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 Piwik_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 Piwik_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::isLocalUrl($referer)) - { - return false; - } - - // validate origin - $origin = self::getOrigin(); - if(!empty($origin) && - ($origin == 'null' - || !in_array($origin, self::getAcceptableOrigins()))) - { - return false; - } - - return true; - } - - /** - * Discard nonce ("now" as opposed to waiting for garbage collection) - * - * @param string $id Unique id - */ - static public function discardNonce($id) - { - $ns = new Piwik_Session_Namespace($id); - $ns->unsetAll(); - } - - /** - * Get ORIGIN header, false if not found - * - * @return string|false - */ - static public function getOrigin() - { - if(!empty($_SERVER['HTTP_ORIGIN'])) - { - return $_SERVER['HTTP_ORIGIN']; - } - return false; - } - - /** - * Returns acceptable origins (not simply scheme://host) that - * should handle a variety of proxy and web server (mis)configurations,. - * - * @return array - */ - static public function getAcceptableOrigins() - { - $host = Piwik_Url::getCurrentHost(null); - $port = ''; - - // parse host:port - if(preg_match('/^([^:]+):([0-9]+)$/D', $host, $matches)) - { - $host = $matches[1]; - $port = $matches[2]; - } - - if(empty($host)) - { - return array(); - } - - // standard ports - $origins[] = 'http://'.$host; - $origins[] = 'https://'.$host; - - // non-standard ports - if(!empty($port) && $port != 80 && $port != 443) - { - $origins[] = 'http://'.$host.':'.$port; - $origins[] = 'https://'.$host.':'.$port; - } - - return $origins; - } + /** + * 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 Piwik_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 Piwik_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::isLocalUrl($referer)) { + return false; + } + + // validate origin + $origin = self::getOrigin(); + if (!empty($origin) && + ($origin == 'null' + || !in_array($origin, self::getAcceptableOrigins())) + ) { + return false; + } + + return true; + } + + /** + * Discard nonce ("now" as opposed to waiting for garbage collection) + * + * @param string $id Unique id + */ + static public function discardNonce($id) + { + $ns = new Piwik_Session_Namespace($id); + $ns->unsetAll(); + } + + /** + * Get ORIGIN header, false if not found + * + * @return string|false + */ + static public function getOrigin() + { + if (!empty($_SERVER['HTTP_ORIGIN'])) { + return $_SERVER['HTTP_ORIGIN']; + } + return false; + } + + /** + * Returns acceptable origins (not simply scheme://host) that + * should handle a variety of proxy and web server (mis)configurations,. + * + * @return array + */ + static public function getAcceptableOrigins() + { + $host = Piwik_Url::getCurrentHost(null); + $port = ''; + + // parse host:port + if (preg_match('/^([^:]+):([0-9]+)$/D', $host, $matches)) { + $host = $matches[1]; + $port = $matches[2]; + } + + if (empty($host)) { + return array(); + } + + // standard ports + $origins[] = 'http://' . $host; + $origins[] = 'https://' . $host; + + // non-standard ports + if (!empty($port) && $port != 80 && $port != 443) { + $origins[] = 'http://' . $host . ':' . $port; + $origins[] = 'https://' . $host . ':' . $port; + } + + return $origins; + } } diff --git a/core/Option.php b/core/Option.php index 7fa0b2b42c..4275b8afa1 100644 --- a/core/Option.php +++ b/core/Option.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,189 +12,184 @@ /** * Piwik_Option provides a very simple mechanism to save/retrieve key-values pair * from the database (persistent key-value datastore). - * + * * This is useful to save Piwik-wide preferences, configuration values. - * + * * @package Piwik */ class Piwik_Option { - /** - * @var array - */ - private $all = array(); - - /** - * @var bool - */ - private $loaded = false; - - /** - * Singleton instance - * @var self - */ - static private $instance = null; - - /** - * Returns Singleton instance - * - * @return Piwik_Option - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - /** - * Private Constructor - */ - private function __construct() {} - - /** - * Returns the option value for the requested option $name, fetching from database, if not in cache. - * - * @param string $name Key - * @return string|false Value or false, if not found - */ - public function get($name) - { - $this->autoload(); - if(isset($this->all[$name])) - { - return $this->all[$name]; - } - $value = Piwik_FetchOne( 'SELECT option_value '. - 'FROM `' . Piwik_Common::prefixTable('option') . '`'. - 'WHERE option_name = ?', $name); - if($value === false) - { - return false; - } - $this->all[$name] = $value; - return $value; - } - - /** - * Sets the option value in the database and cache - * - * @param string $name - * @param string $value - * @param int $autoload if set to 1, this option value will be automatically loaded; should be set to 1 for options that will always be used in the Piwik request. - */ - public function set($name, $value, $autoload = 0) - { - $autoload = (int)$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)); - $this->all[$name] = $value; - } - - /** - * Delete key-value pair from database and reload cache. - * - * @param string $name Key to match exactly - * @param string $value Optional value - */ - public function delete($name, $value = null) - { - $sql = 'DELETE FROM `'. Piwik_Common::prefixTable('option') . '` WHERE option_name = ?'; - $bind[] = $name; - - if(isset($value)) - { - $sql .= ' AND option_value = ?'; - $bind[] = $value; - } - - Piwik_Query($sql, $bind); - - $this->clearCache(); - } - - /** - * Delete key-value pair(s) from database and reload cache. - * The supplied pattern should use '%' as wildcards, and literal '_' should be escaped. - * - * @param string $name Pattern of key to match. - * @param string $value Optional value - */ - public function deleteLike($name, $value = null) - { - $sql = 'DELETE FROM `'. Piwik_Common::prefixTable('option') . '` WHERE option_name LIKE ?'; - $bind[] = $name; - - if(isset($value)) - { - $sql .= ' AND option_value = ?'; - $bind[] = $value; - } - - Piwik_Query($sql, $bind); - - $this->clearCache(); - } - - /** - * Initialize cache with autoload settings. - * - * @return void - */ - private function autoload() - { - if($this->loaded) - { - return; - } - - $all = Piwik_FetchAll('SELECT option_value, option_name - FROM `'. Piwik_Common::prefixTable('option') . '` + /** + * @var array + */ + private $all = array(); + + /** + * @var bool + */ + private $loaded = false; + + /** + * Singleton instance + * @var self + */ + static private $instance = null; + + /** + * Returns Singleton instance + * + * @return Piwik_Option + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + /** + * Private Constructor + */ + private function __construct() + { + } + + /** + * Returns the option value for the requested option $name, fetching from database, if not in cache. + * + * @param string $name Key + * @return string|false Value or false, if not found + */ + public function get($name) + { + $this->autoload(); + if (isset($this->all[$name])) { + return $this->all[$name]; + } + $value = Piwik_FetchOne('SELECT option_value ' . + 'FROM `' . Piwik_Common::prefixTable('option') . '`' . + 'WHERE option_name = ?', $name); + if ($value === false) { + return false; + } + $this->all[$name] = $value; + return $value; + } + + /** + * Sets the option value in the database and cache + * + * @param string $name + * @param string $value + * @param int $autoload if set to 1, this option value will be automatically loaded; should be set to 1 for options that will always be used in the Piwik request. + */ + public function set($name, $value, $autoload = 0) + { + $autoload = (int)$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)); + $this->all[$name] = $value; + } + + /** + * Delete key-value pair from database and reload cache. + * + * @param string $name Key to match exactly + * @param string $value Optional value + */ + public function delete($name, $value = null) + { + $sql = 'DELETE FROM `' . Piwik_Common::prefixTable('option') . '` WHERE option_name = ?'; + $bind[] = $name; + + if (isset($value)) { + $sql .= ' AND option_value = ?'; + $bind[] = $value; + } + + Piwik_Query($sql, $bind); + + $this->clearCache(); + } + + /** + * Delete key-value pair(s) from database and reload cache. + * The supplied pattern should use '%' as wildcards, and literal '_' should be escaped. + * + * @param string $name Pattern of key to match. + * @param string $value Optional value + */ + public function deleteLike($name, $value = null) + { + $sql = 'DELETE FROM `' . Piwik_Common::prefixTable('option') . '` WHERE option_name LIKE ?'; + $bind[] = $name; + + if (isset($value)) { + $sql .= ' AND option_value = ?'; + $bind[] = $value; + } + + Piwik_Query($sql, $bind); + + $this->clearCache(); + } + + /** + * Initialize cache with autoload settings. + * + * @return void + */ + private function autoload() + { + if ($this->loaded) { + return; + } + + $all = Piwik_FetchAll('SELECT option_value, option_name + FROM `' . Piwik_Common::prefixTable('option') . '` WHERE autoload = 1'); - foreach($all as $option) - { - $this->all[$option['option_name']] = $option['option_value']; - } - - $this->loaded = true; - } - - /** - * Clears the cache - * Used in unit tests to reset the state of the object between tests - * - * @return void - */ - public function clearCache() - { - $this->loaded = false; - $this->all = array(); - } + foreach ($all as $option) { + $this->all[$option['option_name']] = $option['option_value']; + } + + $this->loaded = true; + } + + /** + * Clears the cache + * Used in unit tests to reset the state of the object between tests + * + * @return void + */ + public function clearCache() + { + $this->loaded = false; + $this->all = array(); + } } /** * Returns the option value for the requested option $name * - * @param string $name Key + * @param string $name Key * @return string|false Value or false, if not found */ function Piwik_GetOption($name) { - return Piwik_Option::getInstance()->get($name); + return Piwik_Option::getInstance()->get($name); } /** * Sets the option value in the database * - * @param string $name - * @param string $value - * @param int $autoload if set to 1, this option value will be automatically loaded; should be set to 1 for options that will always be used in the Piwik request. + * @param string $name + * @param string $value + * @param int $autoload if set to 1, this option value will be automatically loaded; should be set to 1 for options that will always be used in the Piwik request. */ function Piwik_SetOption($name, $value, $autoload = 0) { - Piwik_Option::getInstance()->set($name, $value, $autoload); + Piwik_Option::getInstance()->set($name, $value, $autoload); } diff --git a/core/Period.php b/core/Period.php index 81ab037539..6e6d4cf4d6 100644 --- a/core/Period.php +++ b/core/Period.php @@ -1,24 +1,24 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * Creating a new Piwik_Period subclass: - * + * Creating a new Piwik_Period subclass: + * * Every overloaded method must start with the code - * if(!$this->subperiodsProcessed) - * { - * $this->generate(); - * } - * that checks whether the subperiods have already been computed. - * This is for performance improvements, computing the subperiods is done a per demand basis. + * if(!$this->subperiodsProcessed) + * { + * $this->generate(); + * } + * that checks whether the subperiods have already been computed. + * This is for performance improvements, computing the subperiods is done a per demand basis. * * @package Piwik * @subpackage Piwik_Period @@ -30,151 +30,145 @@ abstract class Piwik_Period * @var Piwik_Period[] */ protected $subperiods = array(); - protected $subperiodsProcessed = false; + protected $subperiodsProcessed = false; /** * @var string */ protected $label = null; - /** - * @var Piwik_Date - */ - protected $date = null; - static protected $errorAvailablePeriods = 'day, week, month, year, range'; + /** + * @var Piwik_Date + */ + protected $date = null; + static protected $errorAvailablePeriods = 'day, week, month, year, range'; /** * Constructor * @param Piwik_Date $date */ - public function __construct( $date ) - { - $this->checkInputDate( $date ); - $this->date = clone $date; - } + public function __construct($date) + { + $this->checkInputDate($date); + $this->date = clone $date; + } + + /** + * @param string $strPeriod "day", "week", "month", "year" + * @param Piwik_Date $date Piwik_Date object + * @throws Exception + * @return Piwik_Period + */ + static public function factory($strPeriod, Piwik_Date $date) + { + switch ($strPeriod) { + case 'day': + return new Piwik_Period_Day($date); + break; + + case 'week': + return new Piwik_Period_Week($date); + break; + + case 'month': + return new Piwik_Period_Month($date); + break; + + case 'year': + return new Piwik_Period_Year($date); + break; + + default: + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidPeriod', array($strPeriod, self::$errorAvailablePeriods))); + break; + } + } + + /** + * The advanced factory method is easier to use from the API than the factory + * method above. It doesn't require an instance of Piwik_Date and works for + * period=range. Generally speaking, anything that can be passed as period + * and range to the API methods can directly be forwarded to this factory + * method in order to get a suitable instance of Piwik_Period. + * + * @param string $strPeriod "day", "week", "month", "year", "range" + * @param string $strDate + * @return Piwik_Period + */ + static public function advancedFactory($strPeriod, $strDate) + { + if (Piwik_Archive::isMultiplePeriod($strDate, $strPeriod) || $strPeriod == 'range') { + return new Piwik_Period_Range($strPeriod, $strDate); + } + return self::factory($strPeriod, Piwik_Date::factory($strDate)); + } + + + /** + * Returns the first day of the period + * + * @return Piwik_Date First day of the period + */ + public function getDateStart() + { + if (!$this->subperiodsProcessed) { + $this->generate(); + } + if (count($this->subperiods) == 0) { + return $this->getDate(); + } + $periods = $this->getSubperiods(); + $currentPeriod = $periods[0]; + while ($currentPeriod->getNumberOfSubperiods() > 0) { + $periods = $currentPeriod->getSubperiods(); + $currentPeriod = $periods[0]; + } + return $currentPeriod->getDate(); + } - /** - * @param string $strPeriod "day", "week", "month", "year" - * @param Piwik_Date $date Piwik_Date object - * @throws Exception - * @return Piwik_Period - */ - static public function factory($strPeriod, Piwik_Date $date) - { - switch ($strPeriod) { - case 'day': - return new Piwik_Period_Day($date); - break; - - case 'week': - return new Piwik_Period_Week($date); - break; - - case 'month': - return new Piwik_Period_Month($date); - break; - - case 'year': - return new Piwik_Period_Year($date); - break; - - default: - throw new Exception(Piwik_TranslateException('General_ExceptionInvalidPeriod', array($strPeriod, self::$errorAvailablePeriods))); - break; - } - } - - /** - * The advanced factory method is easier to use from the API than the factory - * method above. It doesn't require an instance of Piwik_Date and works for - * period=range. Generally speaking, anything that can be passed as period - * and range to the API methods can directly be forwarded to this factory - * method in order to get a suitable instance of Piwik_Period. - * - * @param string $strPeriod "day", "week", "month", "year", "range" - * @param string $strDate - * @return Piwik_Period - */ - static public function advancedFactory($strPeriod, $strDate) { - if (Piwik_Archive::isMultiplePeriod($strDate, $strPeriod) || $strPeriod == 'range') - { - return new Piwik_Period_Range($strPeriod, $strDate); - } - return self::factory($strPeriod, Piwik_Date::factory($strDate)); - } + /** + * Returns the last day of the period ; can be a date in the future + * + * @return Piwik_Date Last day of the period + */ + public function getDateEnd() + { + if (!$this->subperiodsProcessed) { + $this->generate(); + } + if (count($this->subperiods) == 0) { + return $this->getDate(); + } + $periods = $this->getSubperiods(); + $currentPeriod = $periods[count($periods) - 1]; + while ($currentPeriod->getNumberOfSubperiods() > 0) { + $periods = $currentPeriod->getSubperiods(); + $currentPeriod = $periods[count($periods) - 1]; + } + return $currentPeriod->getDate(); + } - - /** - * Returns the first day of the period - * - * @return Piwik_Date First day of the period - */ - public function getDateStart() - { - if(!$this->subperiodsProcessed) - { - $this->generate(); - } - if(count($this->subperiods) == 0) - { - return $this->getDate(); - } - $periods = $this->getSubperiods(); - $currentPeriod = $periods[0]; - while( $currentPeriod->getNumberOfSubperiods() > 0 ) - { - $periods = $currentPeriod->getSubperiods(); - $currentPeriod = $periods[0]; - } - return $currentPeriod->getDate(); - } - - /** - * Returns the last day of the period ; can be a date in the future - * - * @return Piwik_Date Last day of the period - */ - public function getDateEnd() - { - if(!$this->subperiodsProcessed) - { - $this->generate(); - } - if(count($this->subperiods) == 0) - { - return $this->getDate(); - } - $periods = $this->getSubperiods(); - $currentPeriod = $periods[count($periods)-1]; - while( $currentPeriod->getNumberOfSubperiods() > 0 ) - { - $periods = $currentPeriod->getSubperiods(); - $currentPeriod = $periods[count($periods)-1]; - } - return $currentPeriod->getDate(); - } - - public function getId() - { - return Piwik::$idPeriods[$this->getLabel()]; - } + public function getId() + { + return Piwik::$idPeriods[$this->getLabel()]; + } /** * Returns the label for the current period * @return string */ public function getLabel() - { - return $this->label; - } - - /** - * @return Piwik_Date - */ - protected function getDate() - { - return $this->date; - } + { + return $this->label; + } + + /** + * @return Piwik_Date + */ + protected function getDate() + { + return $this->date; + } /** * Checks if the given date is an instance of Piwik_Date @@ -184,57 +178,54 @@ abstract class Piwik_Period * @throws Exception */ protected function checkInputDate($date) - { - if( !($date instanceof Piwik_Date)) - { - throw new Exception("The date must be a Piwik_Date object. " . var_export($date,true)); - } - } - - protected function generate() - { - $this->subperiodsProcessed = true; - } + { + if (!($date instanceof Piwik_Date)) { + throw new Exception("The date must be a Piwik_Date object. " . var_export($date, true)); + } + } + + protected function generate() + { + $this->subperiodsProcessed = true; + } /** * Returns the number of available subperiods * @return int */ public function getNumberOfSubperiods() - { - if(!$this->subperiodsProcessed) - { - $this->generate(); - } - return count($this->subperiods); - } - - /** - * Returns Period_Day for a period made of days (week, month), - * Period_Month for a period made of months (year) - * - * @return array Piwik_Period - */ - public function getSubperiods() - { - if(!$this->subperiodsProcessed) - { - $this->generate(); - } - return $this->subperiods; - } + { + if (!$this->subperiodsProcessed) { + $this->generate(); + } + return count($this->subperiods); + } - /** - * Add a date to the period. - * - * Protected because it not yet supported to add periods after the initialization - * - * @param Piwik_Period $period Valid Piwik_Period object - */ - protected function addSubperiod( $period ) - { - $this->subperiods[] = $period; - } + /** + * Returns Period_Day for a period made of days (week, month), + * Period_Month for a period made of months (year) + * + * @return array Piwik_Period + */ + public function getSubperiods() + { + if (!$this->subperiodsProcessed) { + $this->generate(); + } + return $this->subperiods; + } + + /** + * Add a date to the period. + * + * Protected because it not yet supported to add periods after the initialization + * + * @param Piwik_Period $period Valid Piwik_Period object + */ + protected function addSubperiod($period) + { + $this->subperiods[] = $period; + } /** * Returns a string representing the current period @@ -245,34 +236,33 @@ abstract class Piwik_Period * @return array */ public function toString($format = "Y-m-d") - { - if(!$this->subperiodsProcessed) - { - $this->generate(); - } - $dateString = array(); - foreach($this->subperiods as $period) - { - $dateString[] = $period->toString($format); - } - return $dateString; - } - - public function __toString() - { - return implode(",", $this->toString()); - } - - public function get( $part= null ) - { - if(!$this->subperiodsProcessed) - { - $this->generate(); - } - return $this->date->toString($part); - } - - abstract public function getPrettyString(); - abstract public function getLocalizedShortString(); - abstract public function getLocalizedLongString(); + { + if (!$this->subperiodsProcessed) { + $this->generate(); + } + $dateString = array(); + foreach ($this->subperiods as $period) { + $dateString[] = $period->toString($format); + } + return $dateString; + } + + public function __toString() + { + return implode(",", $this->toString()); + } + + public function get($part = null) + { + if (!$this->subperiodsProcessed) { + $this->generate(); + } + return $this->date->toString($part); + } + + abstract public function getPrettyString(); + + abstract public function getLocalizedShortString(); + + abstract public function getLocalizedLongString(); } diff --git a/core/Period/Day.php b/core/Period/Day.php index b5439d8edb..14cdd16bd7 100644 --- a/core/Period/Day.php +++ b/core/Period/Day.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,87 +15,87 @@ */ class Piwik_Period_Day extends Piwik_Period { - protected $label = 'day'; + protected $label = 'day'; - /** - * Returns the day of the period as a string - * - * @return string - */ - public function getPrettyString() - { - $out = $this->getDateStart()->toString() ; - return $out; - } + /** + * Returns the day of the period as a string + * + * @return string + */ + public function getPrettyString() + { + $out = $this->getDateStart()->toString(); + return $out; + } - /** - * Returns the day of the period as a localized short string - * - * @return string - */ - public function getLocalizedShortString() - { - //"Mon 15 Aug" - $date = $this->getDateStart(); - $out = $date->getLocalized(Piwik_Translate('CoreHome_ShortDateFormat')); - return $out; - } + /** + * Returns the day of the period as a localized short string + * + * @return string + */ + public function getLocalizedShortString() + { + //"Mon 15 Aug" + $date = $this->getDateStart(); + $out = $date->getLocalized(Piwik_Translate('CoreHome_ShortDateFormat')); + return $out; + } - /** - * Returns the day of the period as a localized long string - * - * @return string - */ - public function getLocalizedLongString() - { - //"Mon 15 Aug" - $date = $this->getDateStart(); - $template = Piwik_Translate('CoreHome_DateFormat'); - $out = $date->getLocalized($template); - return $out; - } + /** + * Returns the day of the period as a localized long string + * + * @return string + */ + public function getLocalizedLongString() + { + //"Mon 15 Aug" + $date = $this->getDateStart(); + $template = Piwik_Translate('CoreHome_DateFormat'); + $out = $date->getLocalized($template); + return $out; + } - /** - * Returns the number of subperiods - * Always 0, in that case - * - * @return int - */ - public function getNumberOfSubperiods() - { - return 0; - } + /** + * Returns the number of subperiods + * Always 0, in that case + * + * @return int + */ + public function getNumberOfSubperiods() + { + return 0; + } - /** - * Adds a subperiod - * Not supported for day periods - * - * @param $date - * @throws Exception - */ - public function addSubperiod( $date ) - { - throw new Exception("Adding a subperiod is not supported for Piwik_Period_Day"); - } + /** + * Adds a subperiod + * Not supported for day periods + * + * @param $date + * @throws Exception + */ + public function addSubperiod($date) + { + throw new Exception("Adding a subperiod is not supported for Piwik_Period_Day"); + } - /** - * Returns the day of the period in the given format - * - * @param string $format - * @return string - */ - public function toString($format = "Y-m-d") - { - return $this->date->toString($format); - } + /** + * Returns the day of the period in the given format + * + * @param string $format + * @return string + */ + public function toString($format = "Y-m-d") + { + return $this->date->toString($format); + } - /** - * Returns the current period as a string - * - * @return string - */ - public function __toString() - { - return $this->toString(); - } + /** + * Returns the current period as a string + * + * @return string + */ + public function __toString() + { + return $this->toString(); + } } diff --git a/core/Period/Month.php b/core/Period/Month.php index f7b21cdda8..fdb8a78dbc 100644 --- a/core/Period/Month.php +++ b/core/Period/Month.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,62 +15,60 @@ */ class Piwik_Period_Month extends Piwik_Period { - protected $label = 'month'; + protected $label = 'month'; - /** - * Returns the current period as a localized short string - * - * @return string - */ - public function getLocalizedShortString() - { - //"Aug 09" - $out = $this->getDateStart()->getLocalized(Piwik_Translate('CoreHome_ShortMonthFormat')); - return $out; - } + /** + * Returns the current period as a localized short string + * + * @return string + */ + public function getLocalizedShortString() + { + //"Aug 09" + $out = $this->getDateStart()->getLocalized(Piwik_Translate('CoreHome_ShortMonthFormat')); + return $out; + } - /** - * Returns the current period as a localized long string - * - * @return string - */ - public function getLocalizedLongString() - { - //"August 2009" - $out = $this->getDateStart()->getLocalized(Piwik_Translate('CoreHome_LongMonthFormat')); - return $out; - } + /** + * Returns the current period as a localized long string + * + * @return string + */ + public function getLocalizedLongString() + { + //"August 2009" + $out = $this->getDateStart()->getLocalized(Piwik_Translate('CoreHome_LongMonthFormat')); + return $out; + } - /** - * Returns the current period as a string - * - * @return string - */ - public function getPrettyString() - { - $out = $this->getDateStart()->toString('Y-m'); - return $out; - } + /** + * Returns the current period as a string + * + * @return string + */ + public function getPrettyString() + { + $out = $this->getDateStart()->toString('Y-m'); + return $out; + } - /** - * Generates the subperiods (one for each day in the month) - */ - protected function generate() - { - if($this->subperiodsProcessed) - { - return; - } - parent::generate(); - - $date = $this->date; - - $startMonth = $date->setDay(1); - $currentDay = clone $startMonth; - while($currentDay->compareMonth($startMonth) == 0) - { - $this->addSubperiod(new Piwik_Period_Day($currentDay)); - $currentDay = $currentDay->addDay(1); - } - } + /** + * Generates the subperiods (one for each day in the month) + */ + protected function generate() + { + if ($this->subperiodsProcessed) { + return; + } + parent::generate(); + + $date = $this->date; + + $startMonth = $date->setDay(1); + $currentDay = clone $startMonth; + while ($currentDay->compareMonth($startMonth) == 0) { + $this->addSubperiod(new Piwik_Period_Day($currentDay)); + $currentDay = $currentDay->addDay(1); + } + } } diff --git a/core/Period/Range.php b/core/Period/Range.php index b77ffec9fa..38dd9b350e 100644 --- a/core/Period/Range.php +++ b/core/Period/Range.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -17,402 +17,362 @@ */ class Piwik_Period_Range extends Piwik_Period { - protected $label = 'range'; - - public function __construct( $strPeriod, $strDate, $timezone = 'UTC', $today = false ) - { - $this->strPeriod = $strPeriod; - $this->strDate = $strDate; - $this->defaultEndDate = null; - $this->timezone = $timezone; - if($today === false) - { - $today = Piwik_Date::factory('today', $this->timezone); - } - $this->today = $today; - } - - /** - * Returns the current period as a localized short string - * - * @return string - */ - public function getLocalizedShortString() - { - //"30 Dec 08 - 26 Feb 09" - $dateStart = $this->getDateStart(); - $dateEnd = $this->getDateEnd(); - $template = Piwik_Translate('CoreHome_ShortDateFormatWithYear'); - $shortDateStart = $dateStart->getLocalized($template); - $shortDateEnd = $dateEnd->getLocalized($template); - $out = "$shortDateStart - $shortDateEnd"; - return $out; - } - - /** - * Returns the current period as a localized long string - * - * @return string - */ - public function getLocalizedLongString() - { - return $this->getLocalizedShortString(); - } - - /** - * Returns the start date of the period - * - * @return Piwik_Date - * @throws Exception - */ - public function getDateStart() - { - $dateStart = parent::getDateStart(); - if(empty($dateStart)) - { - throw new Exception("Specified date range is invalid."); - } - return $dateStart; - } - - /** - * Returns the current period as a string - * - * @return string - */ - public function getPrettyString() - { - $out = Piwik_Translate('General_DateRangeFromTo', array($this->getDateStart()->toString(), $this->getDateEnd()->toString())); - return $out; - } - - /** - * - * @param string $period - * @param Piwik_Date $date - * @param int $n - * @throws Exception - * @return Piwik_Date - */ - static public function removePeriod( $period, Piwik_Date $date, $n ) - { - switch($period) - { - case 'day': - $startDate = $date->subDay( $n ); - break; - - case 'week': - $startDate = $date->subDay( $n * 7 ); - break; - - case 'month': - $startDate = $date->subMonth( $n ); - break; - - case 'year': - $startDate = $date->subMonth( 12 * $n ); - break; - default: - throw new Exception('The period parameter is invalid'); - break; - } - return $startDate; - } - - protected function getMaxN($lastN) - { - switch($this->strPeriod) - { - case 'day': - $lastN = min( $lastN, 5*365 ); - break; - - case 'week': - $lastN = min( $lastN, 10*52 ); - break; - - case 'month': - $lastN = min( $lastN, 10*12 ); - break; - - case 'year': - $lastN = min( $lastN, 10 ); - break; - } - return $lastN; - } - - /** - * Sets the default end date of the period - * - * @param Piwik_Date $oDate - */ - public function setDefaultEndDate( Piwik_Date $oDate) - { - $this->defaultEndDate = $oDate; - } - - /** - * Generates the subperiods - * - * @throws Exception - */ - protected function generate() - { - if($this->subperiodsProcessed) - { - return; - } - parent::generate(); - - if(preg_match('/(last|previous)([0-9]*)/', $this->strDate, $regs)) - { - $lastN = $regs[2]; - $lastOrPrevious = $regs[1]; - if(!is_null($this->defaultEndDate)) - { - $defaultEndDate = $this->defaultEndDate; - } - else - { - $defaultEndDate = Piwik_Date::factory('now', $this->timezone); - } - - $period = $this->strPeriod; - if($period == 'range') - { - $period = 'day'; - } - - if($lastOrPrevious == 'last') - { - $endDate = $defaultEndDate; - } - elseif($lastOrPrevious == 'previous') - { - $endDate = self::removePeriod($period, $defaultEndDate, 1); - } - - $lastN = $this->getMaxN($lastN); - - // last1 means only one result ; last2 means 2 results so we remove only 1 to the days/weeks/etc - $lastN--; - $lastN = abs($lastN); - - $startDate = self::removePeriod($period, $endDate, $lastN); - } - elseif( $dateRange = Piwik_Period_Range::parseDateRange($this->strDate) ) - { - $strDateStart = $dateRange[1]; - $strDateEnd = $dateRange[2]; - $startDate = Piwik_Date::factory($strDateStart); - - if($strDateEnd == 'today') - { - $strDateEnd = 'now'; - } - elseif($strDateEnd == 'yesterday') - { - $strDateEnd = 'yesterdaySameTime'; - } - // we set the timezone in the Date object only if the date is relative eg. 'today', 'yesterday', 'now' - $timezone = null; - if(strpos($strDateEnd, '-') === false) - { - $timezone = $this->timezone; - } - $endDate = Piwik_Date::factory($strDateEnd, $timezone); - } - else - { - throw new Exception(Piwik_TranslateException('General_ExceptionInvalidDateRange', array($this->strDate, ' \'lastN\', \'previousN\', \'YYYY-MM-DD,YYYY-MM-DD\''))); - } - if($this->strPeriod != 'range') - { - $this->fillArraySubPeriods($startDate, $endDate, $this->strPeriod); - return; - } - $this->processOptimalSubperiods($startDate, $endDate); - // When period=range, we want End Date to be the actual specified end date, - // rather than the end of the month / week / whatever is used for processing this range - $this->endDate = $endDate; - } - - /** - * Given a date string, returns false if not a date range, - * or returns the array containing date start, date end - * - * @param string $dateString - * @return mixed array(1 => dateStartString, 2 => dateEndString ) or false if the input was not a date range - */ - static public function parseDateRange($dateString) - { - $matched = preg_match('/^([0-9]{4}-[0-9]{1,2}-[0-9]{1,2}),(([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})|today|now|yesterday)$/D', trim($dateString), $regs); - if(empty($matched)) - { - return false; - } - return $regs; - } - - protected $endDate = null; - - /** - * Returns the end date of the period - * - * @return null|Piwik_Date - */ - public function getDateEnd() - { - if(!is_null($this->endDate)) - { - return $this->endDate; - } - return parent::getDateEnd(); - } - - /** - * Determine which kind of period is best to use - * See Range.test.php - * - * @param $startDate - * @param $endDate - */ - protected function processOptimalSubperiods($startDate, $endDate) - { - while($startDate->isEarlier($endDate) - || $startDate == $endDate) - { - $endOfPeriod = null; - - $month = new Piwik_Period_Month($startDate); - $endOfMonth = $month->getDateEnd(); - $startOfMonth = $month->getDateStart(); - if($startDate == $startOfMonth - && ($endOfMonth->isEarlier($endDate) - || $endOfMonth == $endDate - || $endOfMonth->isLater($this->today) - ) - // We don't use the month if - // the end day is in this month, is before today, and month not finished - && !($endDate->isEarlier($this->today) - && $this->today->toString('Y') == $endOfMonth->toString('Y') - && $this->today->compareMonth($endOfMonth) == 0) - ) - { - $this->addSubperiod($month); - $endOfPeriod = $endOfMonth; - } - else - { - // From start date, - // Process end of week - $week = new Piwik_Period_Week($startDate); - $startOfWeek = $week->getDateStart(); - $endOfWeek = $week->getDateEnd(); - - $useMonthsNextIteration = $startDate->addPeriod(2, 'month')->setDay(1)->isEarlier($endDate); - if($useMonthsNextIteration - && $endOfWeek->isLater($endOfMonth)) - { - $this->fillArraySubPeriods($startDate, $endOfMonth, 'day'); - $endOfPeriod = $endOfMonth; - } - // If end of this week is later than end date, we use days - elseif($endOfWeek->isLater($endDate) - && ($endOfWeek->isEarlier($this->today) - || $endDate->isEarlier($this->today)) - ) - { - $this->fillArraySubPeriods($startDate, $endDate, 'day'); - break 1; - } - elseif($startOfWeek->isEarlier($startDate) - && $endOfWeek->isEarlier($this->today)) - { - $this->fillArraySubPeriods($startDate, $endOfWeek, 'day'); - $endOfPeriod = $endOfWeek; - } - else - { - $this->addSubperiod($week); - $endOfPeriod = $endOfWeek; - } - } - $startDate = $endOfPeriod->addDay(1); - } - } - - /** - * Adds new subperiods - * - * @param Piwik_Date $startDate - * @param Piwik_Date $endDate - * @param string $period - */ - protected function fillArraySubPeriods($startDate, $endDate, $period) - { - $arrayPeriods= array(); - $endSubperiod = Piwik_Period::factory($period, $endDate); - $arrayPeriods[] = $endSubperiod; - - // set end date to start of end period since we're comparing against start date. - $endDate = $endSubperiod->getDateStart(); - while($endDate->isLater($startDate) ) - { - $endDate = self::removePeriod($period, $endDate, 1); - $subPeriod = Piwik_Period::factory($period, $endDate); - $arrayPeriods[] = $subPeriod; - } - $arrayPeriods = array_reverse($arrayPeriods); - foreach($arrayPeriods as $period) - { - $this->addSubperiod($period); - } - } - - /** - * Returns the date that is one period before the supplied date. - * - * @param string $date The date to get the last date of. - * @param string $period The period to use (either 'day', 'week', 'month', 'year'); - * @return array An array with two elements, a string for the date before $date and - * a Piwik_Period instance for the period before $date. - */ - public static function getLastDate( $date = false, $period = false ) - { - if ($date === false) - { - $date = Piwik_Common::getRequestVar('date'); - } - - if ($period === false) - { - $period = Piwik_Common::getRequestVar('period'); - } - - // can't get the last date for range periods & dates that use lastN/previousN - $strLastDate = false; - $lastPeriod = false; - if ($period != 'range' && !preg_match('/(last|previous)([0-9]*)/', $date, $regs)) - { - if (strpos($date, ',')) // date in the form of 2011-01-01,2011-02-02 - { - $rangePeriod = new Piwik_Period_Range($period, $date); - - $lastStartDate = Piwik_Period_Range::removePeriod($period, $rangePeriod->getDateStart(), $n = 1); - $lastEndDate = Piwik_Period_Range::removePeriod($period, $rangePeriod->getDateEnd(), $n = 1); - - $strLastDate = "$lastStartDate,$lastEndDate"; - } - else - { - $lastPeriod = Piwik_Period_Range::removePeriod($period, Piwik_Date::factory($date), $n = 1); - $strLastDate = $lastPeriod->toString(); - } - } - - return array($strLastDate, $lastPeriod); - } + protected $label = 'range'; + + public function __construct($strPeriod, $strDate, $timezone = 'UTC', $today = false) + { + $this->strPeriod = $strPeriod; + $this->strDate = $strDate; + $this->defaultEndDate = null; + $this->timezone = $timezone; + if ($today === false) { + $today = Piwik_Date::factory('today', $this->timezone); + } + $this->today = $today; + } + + /** + * Returns the current period as a localized short string + * + * @return string + */ + public function getLocalizedShortString() + { + //"30 Dec 08 - 26 Feb 09" + $dateStart = $this->getDateStart(); + $dateEnd = $this->getDateEnd(); + $template = Piwik_Translate('CoreHome_ShortDateFormatWithYear'); + $shortDateStart = $dateStart->getLocalized($template); + $shortDateEnd = $dateEnd->getLocalized($template); + $out = "$shortDateStart - $shortDateEnd"; + return $out; + } + + /** + * Returns the current period as a localized long string + * + * @return string + */ + public function getLocalizedLongString() + { + return $this->getLocalizedShortString(); + } + + /** + * Returns the start date of the period + * + * @return Piwik_Date + * @throws Exception + */ + public function getDateStart() + { + $dateStart = parent::getDateStart(); + if (empty($dateStart)) { + throw new Exception("Specified date range is invalid."); + } + return $dateStart; + } + + /** + * Returns the current period as a string + * + * @return string + */ + public function getPrettyString() + { + $out = Piwik_Translate('General_DateRangeFromTo', array($this->getDateStart()->toString(), $this->getDateEnd()->toString())); + return $out; + } + + /** + * + * @param string $period + * @param Piwik_Date $date + * @param int $n + * @throws Exception + * @return Piwik_Date + */ + static public function removePeriod($period, Piwik_Date $date, $n) + { + switch ($period) { + case 'day': + $startDate = $date->subDay($n); + break; + + case 'week': + $startDate = $date->subDay($n * 7); + break; + + case 'month': + $startDate = $date->subMonth($n); + break; + + case 'year': + $startDate = $date->subMonth(12 * $n); + break; + default: + throw new Exception('The period parameter is invalid'); + break; + } + return $startDate; + } + + protected function getMaxN($lastN) + { + switch ($this->strPeriod) { + case 'day': + $lastN = min($lastN, 5 * 365); + break; + + case 'week': + $lastN = min($lastN, 10 * 52); + break; + + case 'month': + $lastN = min($lastN, 10 * 12); + break; + + case 'year': + $lastN = min($lastN, 10); + break; + } + return $lastN; + } + + /** + * Sets the default end date of the period + * + * @param Piwik_Date $oDate + */ + public function setDefaultEndDate(Piwik_Date $oDate) + { + $this->defaultEndDate = $oDate; + } + + /** + * Generates the subperiods + * + * @throws Exception + */ + protected function generate() + { + if ($this->subperiodsProcessed) { + return; + } + parent::generate(); + + if (preg_match('/(last|previous)([0-9]*)/', $this->strDate, $regs)) { + $lastN = $regs[2]; + $lastOrPrevious = $regs[1]; + if (!is_null($this->defaultEndDate)) { + $defaultEndDate = $this->defaultEndDate; + } else { + $defaultEndDate = Piwik_Date::factory('now', $this->timezone); + } + + $period = $this->strPeriod; + if ($period == 'range') { + $period = 'day'; + } + + if ($lastOrPrevious == 'last') { + $endDate = $defaultEndDate; + } elseif ($lastOrPrevious == 'previous') { + $endDate = self::removePeriod($period, $defaultEndDate, 1); + } + + $lastN = $this->getMaxN($lastN); + + // last1 means only one result ; last2 means 2 results so we remove only 1 to the days/weeks/etc + $lastN--; + $lastN = abs($lastN); + + $startDate = self::removePeriod($period, $endDate, $lastN); + } elseif ($dateRange = Piwik_Period_Range::parseDateRange($this->strDate)) { + $strDateStart = $dateRange[1]; + $strDateEnd = $dateRange[2]; + $startDate = Piwik_Date::factory($strDateStart); + + if ($strDateEnd == 'today') { + $strDateEnd = 'now'; + } elseif ($strDateEnd == 'yesterday') { + $strDateEnd = 'yesterdaySameTime'; + } + // we set the timezone in the Date object only if the date is relative eg. 'today', 'yesterday', 'now' + $timezone = null; + if (strpos($strDateEnd, '-') === false) { + $timezone = $this->timezone; + } + $endDate = Piwik_Date::factory($strDateEnd, $timezone); + } else { + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidDateRange', array($this->strDate, ' \'lastN\', \'previousN\', \'YYYY-MM-DD,YYYY-MM-DD\''))); + } + if ($this->strPeriod != 'range') { + $this->fillArraySubPeriods($startDate, $endDate, $this->strPeriod); + return; + } + $this->processOptimalSubperiods($startDate, $endDate); + // When period=range, we want End Date to be the actual specified end date, + // rather than the end of the month / week / whatever is used for processing this range + $this->endDate = $endDate; + } + + /** + * Given a date string, returns false if not a date range, + * or returns the array containing date start, date end + * + * @param string $dateString + * @return mixed array(1 => dateStartString, 2 => dateEndString ) or false if the input was not a date range + */ + static public function parseDateRange($dateString) + { + $matched = preg_match('/^([0-9]{4}-[0-9]{1,2}-[0-9]{1,2}),(([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})|today|now|yesterday)$/D', trim($dateString), $regs); + if (empty($matched)) { + return false; + } + return $regs; + } + + protected $endDate = null; + + /** + * Returns the end date of the period + * + * @return null|Piwik_Date + */ + public function getDateEnd() + { + if (!is_null($this->endDate)) { + return $this->endDate; + } + return parent::getDateEnd(); + } + + /** + * Determine which kind of period is best to use + * See Range.test.php + * + * @param $startDate + * @param $endDate + */ + protected function processOptimalSubperiods($startDate, $endDate) + { + while ($startDate->isEarlier($endDate) + || $startDate == $endDate) { + $endOfPeriod = null; + + $month = new Piwik_Period_Month($startDate); + $endOfMonth = $month->getDateEnd(); + $startOfMonth = $month->getDateStart(); + if ($startDate == $startOfMonth + && ($endOfMonth->isEarlier($endDate) + || $endOfMonth == $endDate + || $endOfMonth->isLater($this->today) + ) + // We don't use the month if + // the end day is in this month, is before today, and month not finished + && !($endDate->isEarlier($this->today) + && $this->today->toString('Y') == $endOfMonth->toString('Y') + && $this->today->compareMonth($endOfMonth) == 0) + ) { + $this->addSubperiod($month); + $endOfPeriod = $endOfMonth; + } else { + // From start date, + // Process end of week + $week = new Piwik_Period_Week($startDate); + $startOfWeek = $week->getDateStart(); + $endOfWeek = $week->getDateEnd(); + + $useMonthsNextIteration = $startDate->addPeriod(2, 'month')->setDay(1)->isEarlier($endDate); + if ($useMonthsNextIteration + && $endOfWeek->isLater($endOfMonth) + ) { + $this->fillArraySubPeriods($startDate, $endOfMonth, 'day'); + $endOfPeriod = $endOfMonth; + } // If end of this week is later than end date, we use days + elseif ($endOfWeek->isLater($endDate) + && ($endOfWeek->isEarlier($this->today) + || $endDate->isEarlier($this->today)) + ) { + $this->fillArraySubPeriods($startDate, $endDate, 'day'); + break 1; + } elseif ($startOfWeek->isEarlier($startDate) + && $endOfWeek->isEarlier($this->today) + ) { + $this->fillArraySubPeriods($startDate, $endOfWeek, 'day'); + $endOfPeriod = $endOfWeek; + } else { + $this->addSubperiod($week); + $endOfPeriod = $endOfWeek; + } + } + $startDate = $endOfPeriod->addDay(1); + } + } + + /** + * Adds new subperiods + * + * @param Piwik_Date $startDate + * @param Piwik_Date $endDate + * @param string $period + */ + protected function fillArraySubPeriods($startDate, $endDate, $period) + { + $arrayPeriods = array(); + $endSubperiod = Piwik_Period::factory($period, $endDate); + $arrayPeriods[] = $endSubperiod; + + // set end date to start of end period since we're comparing against start date. + $endDate = $endSubperiod->getDateStart(); + while ($endDate->isLater($startDate)) { + $endDate = self::removePeriod($period, $endDate, 1); + $subPeriod = Piwik_Period::factory($period, $endDate); + $arrayPeriods[] = $subPeriod; + } + $arrayPeriods = array_reverse($arrayPeriods); + foreach ($arrayPeriods as $period) { + $this->addSubperiod($period); + } + } + + /** + * Returns the date that is one period before the supplied date. + * + * @param string $date The date to get the last date of. + * @param string $period The period to use (either 'day', 'week', 'month', 'year'); + * @return array An array with two elements, a string for the date before $date and + * a Piwik_Period instance for the period before $date. + */ + public static function getLastDate($date = false, $period = false) + { + if ($date === false) { + $date = Piwik_Common::getRequestVar('date'); + } + + if ($period === false) { + $period = Piwik_Common::getRequestVar('period'); + } + + // can't get the last date for range periods & dates that use lastN/previousN + $strLastDate = false; + $lastPeriod = false; + if ($period != 'range' && !preg_match('/(last|previous)([0-9]*)/', $date, $regs)) { + if (strpos($date, ',')) // date in the form of 2011-01-01,2011-02-02 + { + $rangePeriod = new Piwik_Period_Range($period, $date); + + $lastStartDate = Piwik_Period_Range::removePeriod($period, $rangePeriod->getDateStart(), $n = 1); + $lastEndDate = Piwik_Period_Range::removePeriod($period, $rangePeriod->getDateEnd(), $n = 1); + + $strLastDate = "$lastStartDate,$lastEndDate"; + } else { + $lastPeriod = Piwik_Period_Range::removePeriod($period, Piwik_Date::factory($date), $n = 1); + $strLastDate = $lastPeriod->toString(); + } + } + + return array($strLastDate, $lastPeriod); + } } diff --git a/core/Period/Week.php b/core/Period/Week.php index f5dd546764..9b84f05b11 100644 --- a/core/Period/Week.php +++ b/core/Period/Week.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -15,83 +15,80 @@ */ class Piwik_Period_Week extends Piwik_Period { - protected $label = 'week'; + protected $label = 'week'; - /** - * Returns the current period as a localized short string - * - * @return string - */ - public function getLocalizedShortString() - { - //"30 Dec - 6 Jan 09" - $dateStart = $this->getDateStart(); - $dateEnd = $this->getDateEnd(); - - $string = Piwik_Translate('CoreHome_ShortWeekFormat'); - $string = self::getTranslatedRange($string, $dateStart, $dateEnd); - return $string; - } + /** + * Returns the current period as a localized short string + * + * @return string + */ + public function getLocalizedShortString() + { + //"30 Dec - 6 Jan 09" + $dateStart = $this->getDateStart(); + $dateEnd = $this->getDateEnd(); - /** - * Returns the current period as a localized long string - * - * @return string - */ - public function getLocalizedLongString() - { - $format = Piwik_Translate('CoreHome_LongWeekFormat'); - $string = self::getTranslatedRange($format, $this->getDateStart(), $this->getDateEnd()); - return Piwik_Translate('CoreHome_PeriodWeek') . " " . $string; - } + $string = Piwik_Translate('CoreHome_ShortWeekFormat'); + $string = self::getTranslatedRange($string, $dateStart, $dateEnd); + return $string; + } - static protected function getTranslatedRange($format, $dateStart, $dateEnd) - { - $string = str_replace('From%', '%', $format); - $string = $dateStart->getLocalized($string); - $string = str_replace('To%', '%', $string); - $string = $dateEnd->getLocalized($string); - return $string; - } - - /** - * Returns the current period as a string - * - * @return string - */ - public function getPrettyString() - { - $out = Piwik_Translate('General_DateRangeFromTo', - array($this->getDateStart()->toString(), - $this->getDateEnd()->toString()) + /** + * Returns the current period as a localized long string + * + * @return string + */ + public function getLocalizedLongString() + { + $format = Piwik_Translate('CoreHome_LongWeekFormat'); + $string = self::getTranslatedRange($format, $this->getDateStart(), $this->getDateEnd()); + return Piwik_Translate('CoreHome_PeriodWeek') . " " . $string; + } + + static protected function getTranslatedRange($format, $dateStart, $dateEnd) + { + $string = str_replace('From%', '%', $format); + $string = $dateStart->getLocalized($string); + $string = str_replace('To%', '%', $string); + $string = $dateEnd->getLocalized($string); + return $string; + } + + /** + * Returns the current period as a string + * + * @return string + */ + public function getPrettyString() + { + $out = Piwik_Translate('General_DateRangeFromTo', + array($this->getDateStart()->toString(), + $this->getDateEnd()->toString()) ); - return $out; - } + return $out; + } + + /** + * Generates the subperiods - one for each day in the week + */ + protected function generate() + { + if ($this->subperiodsProcessed) { + return; + } + parent::generate(); + $date = $this->date; + + if ($date->toString('N') > 1) { + $date = $date->subDay($date->toString('N') - 1); + } + + $startWeek = $date; - /** - * Generates the subperiods - one for each day in the week - */ - protected function generate() - { - if($this->subperiodsProcessed) - { - return; - } - parent::generate(); - $date = $this->date; - - if( $date->toString('N') > 1) - { - $date = $date->subDay($date->toString('N')-1); - } - - $startWeek = $date; - - $currentDay = clone $startWeek; - while($currentDay->compareWeek($startWeek) == 0) - { - $this->addSubperiod(new Piwik_Period_Day($currentDay) ); - $currentDay = $currentDay->addDay(1); - } - } + $currentDay = clone $startWeek; + while ($currentDay->compareWeek($startWeek) == 0) { + $this->addSubperiod(new Piwik_Period_Day($currentDay)); + $currentDay = $currentDay->addDay(1); + } + } } diff --git a/core/Period/Year.php b/core/Period/Year.php index 64c6cc4acb..7ab5efbabe 100644 --- a/core/Period/Year.php +++ b/core/Period/Year.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -14,80 +14,76 @@ * @subpackage Piwik_Period */ class Piwik_Period_Year extends Piwik_Period -{ - protected $label = 'year'; +{ + protected $label = 'year'; - /** - * Returns the current period as a localized short string - * - * @return string - */ - public function getLocalizedShortString() - { - return $this->getLocalizedLongString(); - } + /** + * Returns the current period as a localized short string + * + * @return string + */ + public function getLocalizedShortString() + { + return $this->getLocalizedLongString(); + } - /** - * Returns the current period as a localized long string - * - * @return string - */ - public function getLocalizedLongString() - { - //"2009" - $out = $this->getDateStart()->getLocalized("%longYear%"); - return $out; - } + /** + * Returns the current period as a localized long string + * + * @return string + */ + public function getLocalizedLongString() + { + //"2009" + $out = $this->getDateStart()->getLocalized("%longYear%"); + return $out; + } - /** - * Returns the current period as a string - * - * @return string - */ - public function getPrettyString() - { - $out = $this->getDateStart()->toString('Y'); - return $out; - } + /** + * Returns the current period as a string + * + * @return string + */ + public function getPrettyString() + { + $out = $this->getDateStart()->toString('Y'); + return $out; + } - /** - * Generates the subperiods (one for each month of the year) - */ - protected function generate() - { - if($this->subperiodsProcessed) - { - return; - } - parent::generate(); - - $year = $this->date->toString("Y"); - for($i=1; $i<=12; $i++) - { - $this->addSubperiod( new Piwik_Period_Month( - Piwik_Date::factory("$year-$i-01") - ) - ); - } - } + /** + * Generates the subperiods (one for each month of the year) + */ + protected function generate() + { + if ($this->subperiodsProcessed) { + return; + } + parent::generate(); - /** - * Returns the current period as a string - * - * @param string $format - * @return array - */ - function toString($format = 'ignored') - { - if(!$this->subperiodsProcessed) - { - $this->generate(); - } - $stringMonth = array(); - foreach($this->subperiods as $month) - { - $stringMonth[] = $month->get("Y")."-".$month->get("m")."-01"; - } - return $stringMonth; - } + $year = $this->date->toString("Y"); + for ($i = 1; $i <= 12; $i++) { + $this->addSubperiod(new Piwik_Period_Month( + Piwik_Date::factory("$year-$i-01") + ) + ); + } + } + + /** + * Returns the current period as a string + * + * @param string $format + * @return array + */ + function toString($format = 'ignored') + { + if (!$this->subperiodsProcessed) { + $this->generate(); + } + $stringMonth = array(); + foreach ($this->subperiods as $month) { + $stringMonth[] = $month->get("Y") . "-" . $month->get("m") . "-01"; + } + return $stringMonth; + } } diff --git a/core/Piwik.php b/core/Piwik.php index 4a694b7b28..a1ffc1df27 100644 --- a/core/Piwik.php +++ b/core/Piwik.php @@ -22,591 +22,547 @@ require_once PIWIK_INCLUDE_PATH . '/core/Translate.php'; */ class Piwik { - const CLASSES_PREFIX = 'Piwik_'; - const COMPRESSED_FILE_LOCATION = '/tmp/assets/'; - - /** - * Piwik periods - * @var array - */ - public static $idPeriods = array( - 'day' => 1, - 'week' => 2, - 'month' => 3, - 'year' => 4, - 'range' => 5, - ); - - /** - * Should we process and display Unique Visitors? - * -> Always process for day/week/month periods - * For Year and Range, only process if it was enabled in the config file, - * - * @param string $periodLabel Period label (e.g., 'day') - * @return bool - */ - static public function isUniqueVisitorsEnabled($periodLabel) - { - $generalSettings = Piwik_Config::getInstance()->General; - - $settingName = "enable_processing_unique_visitors_$periodLabel"; - $result = !empty($generalSettings[$settingName]) && $generalSettings[$settingName] == 1; - - // check enable_processing_unique_visitors_year_and_range for backwards compatibility - if (($periodLabel == 'year' || $periodLabel == 'range') - && isset($generalSettings['enable_processing_unique_visitors_year_and_range'])) - { - $result |= $generalSettings['enable_processing_unique_visitors_year_and_range'] == 1; - } - - return $result; - } - - /** - * Prefix class name (if needed) - * - * @param string $class - * @return string - */ - static public function prefixClass( $class ) - { - if(!strncmp($class, Piwik::CLASSES_PREFIX, strlen(Piwik::CLASSES_PREFIX))) - { - 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(!strncmp($class, Piwik::CLASSES_PREFIX, $lenPrefix)) - { - return substr($class, $lenPrefix); - } - return $class; - } - - /** - * Installation helper - */ - static public function install() - { - Piwik_Common::mkdir(PIWIK_USER_PATH . '/' . Piwik_Config::getInstance()->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() - { - try { - return Piwik_Db_Schema::getInstance()->hasTables(); - } catch(Exception $e) { - return false; - } - } - - /** - * Called on Core install, update, plugin enable/disable - * Will clear all cache that could be affected by the change in configuration being made - */ - static public function deleteAllCacheOnUpdate() - { - Piwik_AssetManager::removeMergedAssets(); - Piwik_View::clearCompiledTemplates(); - Piwik_Tracker_Cache::deleteTrackerCache(); - } - - /** - * Cache for result of getPiwikUrl. - * Can be overwritten for testing purposes only. - * - * @var string - */ - static public $piwikUrlCache = null; - - /** - * Returns the cached the Piwik URL, eg. http://demo.piwik.org/ or http://example.org/piwik/ - * If not found, then tries to cache it and returns the value. - * - * If the Piwik URL changes (eg. Piwik moved to new server), the value will automatically be refreshed in the cache. - * - * @return string - */ - static public function getPiwikUrl() - { - // Only set in tests - if (self::$piwikUrlCache !== null) - { - return self::$piwikUrlCache; - } - - $key = 'piwikUrl'; - $url = Piwik_GetOption($key); - if(Piwik_Common::isPhpCliMode() - // in case archive.php is triggered with domain localhost - || Piwik_Common::isArchivePhpTriggered() - || defined('PIWIK_MODE_ARCHIVE')) - { - return $url; - } - - $currentUrl = Piwik_Common::sanitizeInputValue(Piwik_Url::getCurrentUrlWithoutFileName()); - - if(empty($url) - // if URL changes, always update the cache - || $currentUrl != $url) - { - if(strlen($currentUrl) >= strlen('http://a/')) - { - Piwik_SetOption($key, $currentUrl, $autoload = true); - } - $url = $currentUrl; - } - return $url; - } - - /** - * Returns true if this appears to be a secure HTTPS connection - * - * @return bool - */ - static public function isHttps() - { - return Piwik_Url::getCurrentScheme() === 'https'; - } - - /** - * Set response header, e.g., HTTP/1.0 200 Ok - * - * @param string $status Status - * @return bool - */ - static public function setHttpStatus($status) - { - if(substr_compare(PHP_SAPI, '-fcgi', -5)) - { - @header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status); - } - else - { - // FastCGI - @header('Status: ' . $status); - } - } - - /** - * Workaround IE bug when downloading certain document types over SSL and - * cache control headers are present, e.g., - * - * Cache-Control: no-cache - * Cache-Control: no-store,max-age=0,must-revalidate - * Pragma: no-cache - * - * @see http://support.microsoft.com/kb/316431/ - * @see RFC2616 - * - * @param string $override One of "public", "private", "no-cache", or "no-store". (optional) - */ - static public function overrideCacheControlHeaders($override = null) - { - if($override || self::isHttps()) - { - @header('Pragma: '); - @header('Expires: '); - if(in_array($override, array('public', 'private', 'no-cache', 'no-store'))) - { - @header("Cache-Control: $override, must-revalidate"); - } - else - { - @header('Cache-Control: must-revalidate'); - } - } - } - -/* + const CLASSES_PREFIX = 'Piwik_'; + const COMPRESSED_FILE_LOCATION = '/tmp/assets/'; + + /** + * Piwik periods + * @var array + */ + public static $idPeriods = array( + 'day' => 1, + 'week' => 2, + 'month' => 3, + 'year' => 4, + 'range' => 5, + ); + + /** + * Should we process and display Unique Visitors? + * -> Always process for day/week/month periods + * For Year and Range, only process if it was enabled in the config file, + * + * @param string $periodLabel Period label (e.g., 'day') + * @return bool + */ + static public function isUniqueVisitorsEnabled($periodLabel) + { + $generalSettings = Piwik_Config::getInstance()->General; + + $settingName = "enable_processing_unique_visitors_$periodLabel"; + $result = !empty($generalSettings[$settingName]) && $generalSettings[$settingName] == 1; + + // check enable_processing_unique_visitors_year_and_range for backwards compatibility + if (($periodLabel == 'year' || $periodLabel == 'range') + && isset($generalSettings['enable_processing_unique_visitors_year_and_range']) + ) { + $result |= $generalSettings['enable_processing_unique_visitors_year_and_range'] == 1; + } + + return $result; + } + + /** + * Prefix class name (if needed) + * + * @param string $class + * @return string + */ + static public function prefixClass($class) + { + if (!strncmp($class, Piwik::CLASSES_PREFIX, strlen(Piwik::CLASSES_PREFIX))) { + 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 (!strncmp($class, Piwik::CLASSES_PREFIX, $lenPrefix)) { + return substr($class, $lenPrefix); + } + return $class; + } + + /** + * Installation helper + */ + static public function install() + { + Piwik_Common::mkdir(PIWIK_USER_PATH . '/' . Piwik_Config::getInstance()->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() + { + try { + return Piwik_Db_Schema::getInstance()->hasTables(); + } catch (Exception $e) { + return false; + } + } + + /** + * Called on Core install, update, plugin enable/disable + * Will clear all cache that could be affected by the change in configuration being made + */ + static public function deleteAllCacheOnUpdate() + { + Piwik_AssetManager::removeMergedAssets(); + Piwik_View::clearCompiledTemplates(); + Piwik_Tracker_Cache::deleteTrackerCache(); + } + + /** + * Cache for result of getPiwikUrl. + * Can be overwritten for testing purposes only. + * + * @var string + */ + static public $piwikUrlCache = null; + + /** + * Returns the cached the Piwik URL, eg. http://demo.piwik.org/ or http://example.org/piwik/ + * If not found, then tries to cache it and returns the value. + * + * If the Piwik URL changes (eg. Piwik moved to new server), the value will automatically be refreshed in the cache. + * + * @return string + */ + static public function getPiwikUrl() + { + // Only set in tests + if (self::$piwikUrlCache !== null) { + return self::$piwikUrlCache; + } + + $key = 'piwikUrl'; + $url = Piwik_GetOption($key); + if (Piwik_Common::isPhpCliMode() + // in case archive.php is triggered with domain localhost + || Piwik_Common::isArchivePhpTriggered() + || defined('PIWIK_MODE_ARCHIVE') + ) { + return $url; + } + + $currentUrl = Piwik_Common::sanitizeInputValue(Piwik_Url::getCurrentUrlWithoutFileName()); + + if (empty($url) + // if URL changes, always update the cache + || $currentUrl != $url + ) { + if (strlen($currentUrl) >= strlen('http://a/')) { + Piwik_SetOption($key, $currentUrl, $autoload = true); + } + $url = $currentUrl; + } + return $url; + } + + /** + * Returns true if this appears to be a secure HTTPS connection + * + * @return bool + */ + static public function isHttps() + { + return Piwik_Url::getCurrentScheme() === 'https'; + } + + /** + * Set response header, e.g., HTTP/1.0 200 Ok + * + * @param string $status Status + * @return bool + */ + static public function setHttpStatus($status) + { + if (substr_compare(PHP_SAPI, '-fcgi', -5)) { + @header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status); + } else { + // FastCGI + @header('Status: ' . $status); + } + } + + /** + * Workaround IE bug when downloading certain document types over SSL and + * cache control headers are present, e.g., + * + * Cache-Control: no-cache + * Cache-Control: no-store,max-age=0,must-revalidate + * Pragma: no-cache + * + * @see http://support.microsoft.com/kb/316431/ + * @see RFC2616 + * + * @param string $override One of "public", "private", "no-cache", or "no-store". (optional) + */ + static public function overrideCacheControlHeaders($override = null) + { + if ($override || self::isHttps()) { + @header('Pragma: '); + @header('Expires: '); + if (in_array($override, array('public', 'private', 'no-cache', 'no-store'))) { + @header("Cache-Control: $override, must-revalidate"); + } else { + @header('Cache-Control: must-revalidate'); + } + } + } + + /* * 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 ) ) - { - Piwik_Common::mkdir( $target, false ); - $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 $dest eg. './index.php' - * @param bool $excludePhp - * @throws Exception - * @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 )) - { - $message = "Error while creating/copying file to <code>$dest</code>. <br />" - . self::getErrorMessageMissingPermissions(Piwik_Common::getPathToPiwikRoot()); - throw new Exception($message); - } - } - return true; - } - - /** - * Returns friendly error message explaining how to fix permissions - * - * @param string $path to the directory missing permissions - * @return string Error message - */ - static public function getErrorMessageMissingPermissions($path) - { - $message = "Please check that the web server has enough permission to write to these files/directories:<br />"; - - if(Piwik_Common::isWindows()) - { - $message .= "On Windows, check that the folder is not read only and is writable. + /** + * 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)) { + Piwik_Common::mkdir($target, false); + $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 $dest eg. './index.php' + * @param bool $excludePhp + * @throws Exception + * @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)) { + $message = "Error while creating/copying file to <code>$dest</code>. <br />" + . self::getErrorMessageMissingPermissions(Piwik_Common::getPathToPiwikRoot()); + throw new Exception($message); + } + } + return true; + } + + /** + * Returns friendly error message explaining how to fix permissions + * + * @param string $path to the directory missing permissions + * @return string Error message + */ + static public function getErrorMessageMissingPermissions($path) + { + $message = "Please check that the web server has enough permission to write to these files/directories:<br />"; + + if (Piwik_Common::isWindows()) { + $message .= "On Windows, check that the folder is not read only and is writable. You can try to execute:<br />"; - } - else - { - $message .= "For example, on a Linux server if your Apache httpd user + } else { + $message .= "For example, on a Linux server if your Apache httpd user is www-data, you can try to execute:<br />" - . "<code>chown -R www-data:www-data ".$path."</code><br />"; - } - - $message .= self::getMakeWritableCommand($path); - - return $message; - } - - /** - * 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; - } - - /** - * Returns the help text displayed to suggest which command to run to give writable access to a file or directory - * - * @param string $realpath - * @return string - */ - static private function getMakeWritableCommand($realpath) - { - if(Piwik_Common::isWindows()) - { - return "<code>cacls $realpath /t /g ".get_current_user().":f</code><br />"; - } - return "<code>chmod -R 0755 $realpath</code><br />"; - } - - /** - * 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 ) - { - return; - } - - $directoryList = ''; - foreach($resultCheck as $dir => $bool) - { - $realpath = Piwik_Common::realpath($dir); - if(!empty($realpath) && $bool === false) - { - $directoryList .= self::getMakeWritableCommand($realpath); - } - } - - // Also give the chown since the chmod is only 755 - if(!Piwik_Common::isWindows()) - { - $realpath = Piwik_Common::realpath(PIWIK_INCLUDE_PATH . '/'); - $directoryList = "<code>chown -R www-data:www-data ".$realpath."</code><br/>" . $directoryList; - } - - // The error message mentions chmod 777 in case users can't chown - $directoryMessage = "<p><b>Piwik couldn't write to some directories</b>.</p> + . "<code>chown -R www-data:www-data " . $path . "</code><br />"; + } + + $message .= self::getMakeWritableCommand($path); + + return $message; + } + + /** + * 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; + } + + /** + * Returns the help text displayed to suggest which command to run to give writable access to a file or directory + * + * @param string $realpath + * @return string + */ + static private function getMakeWritableCommand($realpath) + { + if (Piwik_Common::isWindows()) { + return "<code>cacls $realpath /t /g " . get_current_user() . ":f</code><br />"; + } + return "<code>chmod -R 0755 $realpath</code><br />"; + } + + /** + * 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) { + return; + } + + $directoryList = ''; + foreach ($resultCheck as $dir => $bool) { + $realpath = Piwik_Common::realpath($dir); + if (!empty($realpath) && $bool === false) { + $directoryList .= self::getMakeWritableCommand($realpath); + } + } + + // Also give the chown since the chmod is only 755 + if (!Piwik_Common::isWindows()) { + $realpath = Piwik_Common::realpath(PIWIK_INCLUDE_PATH . '/'); + $directoryList = "<code>chown -R www-data:www-data " . $realpath . "</code><br/>" . $directoryList; + } + + // The error message mentions chmod 777 in case users can't chown + $directoryMessage = "<p><b>Piwik couldn't write to some directories</b>.</p> <p>Try to Execute the following commands on your server, to allow Write access on these directories:</p>" - . "<blockquote>$directoryList</blockquote>" - . "<p>If this doesn't work, you can try to create the directories with your FTP software, and set the CHMOD to 0755 (or 0777 if 0755 is not enough). To do so with your FTP software, right click on the directories then click permissions.</p>" - . "<p>After applying the modifications, you can <a href='index.php'>refresh the page</a>.</p>" - . "<p>If you need more help, try <a href='?module=Proxy&action=redirect&url=http://piwik.org'>Piwik.org</a>.</p>"; - - 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 directory name => true|false (is writable) - */ - static public function checkDirectoriesWritable($directoriesToCheck = null) - { - if( $directoriesToCheck == null ) - { - $directoriesToCheck = array( - '/config/', - '/tmp/', - '/tmp/templates_c/', - '/tmp/cache/', - '/tmp/assets/', - '/tmp/latest/', - '/tmp/tcpdf/', - '/tmp/sessions/', - ); - } - - $resultCheck = array(); - foreach($directoriesToCheck as $directoryToCheck) - { - if( !preg_match('/^'.preg_quote(PIWIK_USER_PATH, '/').'/', $directoryToCheck) ) - { - $directoryToCheck = PIWIK_USER_PATH . $directoryToCheck; - } - - if(!file_exists($directoryToCheck)) - { - Piwik_Common::mkdir($directoryToCheck); - } - - $directory = Piwik_Common::realpath($directoryToCheck); - $resultCheck[$directory] = false; - if($directory !== false // realpath() returns FALSE on failure - && is_writable($directoryToCheck)) - { - $resultCheck[$directory] = true; - } - } - return $resultCheck; - } - - /** - * Check if this installation can be auto-updated. - * For performance, we look for clues rather than an exhaustive test. - * - * @return bool - */ - static public function canAutoUpdate() - { - if(!is_writable(PIWIK_INCLUDE_PATH . '/') || - !is_writable(PIWIK_DOCUMENT_ROOT . '/index.php') || - !is_writable(PIWIK_INCLUDE_PATH . '/core') || - !is_writable(PIWIK_USER_PATH . '/config/global.ini.php')) - { - return false; - } - return true; - } - - /** - * Returns the help message when the auto update can't run because of missing permissions - * - * @return string - */ - static public function getAutoUpdateMakeWritableMessage() - { - $realpath = Piwik_Common::realpath(PIWIK_INCLUDE_PATH . '/'); - $message = ''; - $message .= "<code>chown -R www-data:www-data ".$realpath."</code><br />"; - $message .= "<code>chmod -R 0755 ".$realpath."</code><br />"; - $message .= 'After you execute these commands (or change permissions via your FTP software), refresh the page and you should be able to use the "Automatic Update" feature.'; - return $message; - } - - /** - * Generate default robots.txt, favicon.ico, etc to suppress - * 404 (Not Found) errors in the web server logs, if Piwik - * is installed in the web root (or top level of subdomain). - * - * @see misc/crossdomain.xml - */ - static public function createWebRootFiles() - { - $filesToCreate = array( - '/robots.txt', - '/favicon.ico', - ); - foreach($filesToCreate as $file) - { - @file_put_contents(PIWIK_DOCUMENT_ROOT . $file, ''); - } - } - - /** - * Generate Apache .htaccess files to restrict access - */ - static public function createHtAccessFiles() - { - // deny access to these folders - $directoriesToProtect = array( - '/config', - '/core', - '/lang', - '/tmp', - ); - foreach($directoriesToProtect as $directoryToProtect) - { - Piwik_Common::createHtAccess(PIWIK_INCLUDE_PATH . $directoryToProtect, $overwrite = true); - } - - // Allow/Deny lives in different modules depending on the Apache version - $allow = "<IfModule mod_access.c>\nAllow from all\n</IfModule>\n<IfModule !mod_access_compat>\n<IfModule mod_authz_host.c>\nAllow from all\n</IfModule>\n</IfModule>\n<IfModule mod_access_compat>\nAllow from all\n</IfModule>\n"; - $deny = "<IfModule mod_access.c>\nDeny from all\n</IfModule>\n<IfModule !mod_access_compat>\n<IfModule mod_authz_host.c>\nDeny from all\n</IfModule>\n</IfModule>\n<IfModule mod_access_compat>\nDeny from all\n</IfModule>\n"; - - // more selective allow/deny filters - $allowAny = "<Files \"*\">\n".$allow."Satisfy any\n</Files>\n"; - $allowStaticAssets = "<Files ~ \"\\.(test\.php|gif|ico|jpg|png|svg|js|css|swf)$\">\n".$allow."Satisfy any\n</Files>\n"; - $denyDirectPhp = "<Files ~ \"\\.(php|php4|php5|inc|tpl|in)$\">\n".$deny."</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, $overwrite = true, $content); - } - } - - /** - * Generate IIS web.config files to restrict access - * - * Note: for IIS 7 and above - */ - static public function createWebConfigFiles() - { - @file_put_contents(PIWIK_INCLUDE_PATH . '/web.config', -'<?xml version="1.0" encoding="UTF-8"?> + . "<blockquote>$directoryList</blockquote>" + . "<p>If this doesn't work, you can try to create the directories with your FTP software, and set the CHMOD to 0755 (or 0777 if 0755 is not enough). To do so with your FTP software, right click on the directories then click permissions.</p>" + . "<p>After applying the modifications, you can <a href='index.php'>refresh the page</a>.</p>" + . "<p>If you need more help, try <a href='?module=Proxy&action=redirect&url=http://piwik.org'>Piwik.org</a>.</p>"; + + 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 directory name => true|false (is writable) + */ + static public function checkDirectoriesWritable($directoriesToCheck = null) + { + if ($directoriesToCheck == null) { + $directoriesToCheck = array( + '/config/', + '/tmp/', + '/tmp/templates_c/', + '/tmp/cache/', + '/tmp/assets/', + '/tmp/latest/', + '/tmp/tcpdf/', + '/tmp/sessions/', + ); + } + + $resultCheck = array(); + foreach ($directoriesToCheck as $directoryToCheck) { + if (!preg_match('/^' . preg_quote(PIWIK_USER_PATH, '/') . '/', $directoryToCheck)) { + $directoryToCheck = PIWIK_USER_PATH . $directoryToCheck; + } + + if (!file_exists($directoryToCheck)) { + Piwik_Common::mkdir($directoryToCheck); + } + + $directory = Piwik_Common::realpath($directoryToCheck); + $resultCheck[$directory] = false; + if ($directory !== false // realpath() returns FALSE on failure + && is_writable($directoryToCheck) + ) { + $resultCheck[$directory] = true; + } + } + return $resultCheck; + } + + /** + * Check if this installation can be auto-updated. + * For performance, we look for clues rather than an exhaustive test. + * + * @return bool + */ + static public function canAutoUpdate() + { + if (!is_writable(PIWIK_INCLUDE_PATH . '/') || + !is_writable(PIWIK_DOCUMENT_ROOT . '/index.php') || + !is_writable(PIWIK_INCLUDE_PATH . '/core') || + !is_writable(PIWIK_USER_PATH . '/config/global.ini.php') + ) { + return false; + } + return true; + } + + /** + * Returns the help message when the auto update can't run because of missing permissions + * + * @return string + */ + static public function getAutoUpdateMakeWritableMessage() + { + $realpath = Piwik_Common::realpath(PIWIK_INCLUDE_PATH . '/'); + $message = ''; + $message .= "<code>chown -R www-data:www-data " . $realpath . "</code><br />"; + $message .= "<code>chmod -R 0755 " . $realpath . "</code><br />"; + $message .= 'After you execute these commands (or change permissions via your FTP software), refresh the page and you should be able to use the "Automatic Update" feature.'; + return $message; + } + + /** + * Generate default robots.txt, favicon.ico, etc to suppress + * 404 (Not Found) errors in the web server logs, if Piwik + * is installed in the web root (or top level of subdomain). + * + * @see misc/crossdomain.xml + */ + static public function createWebRootFiles() + { + $filesToCreate = array( + '/robots.txt', + '/favicon.ico', + ); + foreach ($filesToCreate as $file) { + @file_put_contents(PIWIK_DOCUMENT_ROOT . $file, ''); + } + } + + /** + * Generate Apache .htaccess files to restrict access + */ + static public function createHtAccessFiles() + { + // deny access to these folders + $directoriesToProtect = array( + '/config', + '/core', + '/lang', + '/tmp', + ); + foreach ($directoriesToProtect as $directoryToProtect) { + Piwik_Common::createHtAccess(PIWIK_INCLUDE_PATH . $directoryToProtect, $overwrite = true); + } + + // Allow/Deny lives in different modules depending on the Apache version + $allow = "<IfModule mod_access.c>\nAllow from all\n</IfModule>\n<IfModule !mod_access_compat>\n<IfModule mod_authz_host.c>\nAllow from all\n</IfModule>\n</IfModule>\n<IfModule mod_access_compat>\nAllow from all\n</IfModule>\n"; + $deny = "<IfModule mod_access.c>\nDeny from all\n</IfModule>\n<IfModule !mod_access_compat>\n<IfModule mod_authz_host.c>\nDeny from all\n</IfModule>\n</IfModule>\n<IfModule mod_access_compat>\nDeny from all\n</IfModule>\n"; + + // more selective allow/deny filters + $allowAny = "<Files \"*\">\n" . $allow . "Satisfy any\n</Files>\n"; + $allowStaticAssets = "<Files ~ \"\\.(test\.php|gif|ico|jpg|png|svg|js|css|swf)$\">\n" . $allow . "Satisfy any\n</Files>\n"; + $denyDirectPhp = "<Files ~ \"\\.(php|php4|php5|inc|tpl|in)$\">\n" . $deny . "</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, $overwrite = true, $content); + } + } + + /** + * Generate IIS web.config files to restrict access + * + * Note: for IIS 7 and above + */ + static public function createWebConfigFiles() + { + @file_put_contents(PIWIK_INCLUDE_PATH . '/web.config', + '<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <security> @@ -636,15 +592,14 @@ class Piwik </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"?> + // 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> @@ -656,2124 +611,1987 @@ class Piwik </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() - { - $messages = array(); - $messages[] = true; - - // ignore dev environments - if(file_exists(PIWIK_INCLUDE_PATH . '/.git')) - { - $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'); - $hasMd5 = function_exists('md5'); - foreach($files as $path => $props) - { - $file = PIWIK_INCLUDE_PATH . '/' . $path; - - if(!file_exists($file)) - { - $messages[] = Piwik_Translate('General_ExceptionMissingFile', $file); - } - else if(filesize($file) != $props[0]) - { - if(!$hasMd5 || in_array(substr($path, -4), array('.gif', '.ico', '.jpg', '.png', '.swf'))) - { - // files that contain binary data (e.g., images) must match the file size - $messages[] = Piwik_Translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file))); - } - else - { - // convert end-of-line characters and re-test text files - $content = @file_get_contents($file); - $content = str_replace("\r\n", "\n", $content); - if((strlen($content) != $props[0]) - || (@md5($content) !== $props[1])) - { - $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; - } - - /** - * Test if php output is compressed - * - * @return bool True if php output is (or suspected/likely) to be compressed - */ - static public function isPhpOutputCompressed() - { - // Off = ''; On = '1'; otherwise, it's a buffer size - $zlibOutputCompression = ini_get('zlib.output_compression'); - - // could be ob_gzhandler, ob_deflatehandler, etc - $outputHandler = ini_get('output_handler'); - - // output handlers can be stacked - $obHandlers = array_filter( ob_list_handlers(), create_function('$var', 'return $var !== "default output handler";') ); - - // user defined handler via wrapper - $autoPrependFile = ini_get('auto_prepend_file'); - $autoAppendFile = ini_get('auto_append_file'); - - return !empty($zlibOutputCompression) || - !empty($outputHandler) || - !empty($obHandlers) || - !empty($autoPrependFile) || - !empty($autoAppendFile); - } - - /** - * Serve static files through php proxy. - * - * It performs the following actions: - * - Checks the file is readable or returns "HTTP/1.0 404 Not Found" - * - Returns "HTTP/1.1 304 Not Modified" after comparing the HTTP_IF_MODIFIED_SINCE - * with the modification date of the static file - * - Will try to compress the static file according to HTTP_ACCEPT_ENCODING. Compressed files are store in - * the /tmp directory. If compressing extensions are not available, a manually gzip compressed file - * can be provided in the /tmp directory. It has to bear the same name with an added .gz extension. - * Using manually compressed static files requires you to manually update the compressed file when - * the static file is updated. - * - Overrides server cache control config to allow caching - * - Sends Very Accept-Encoding to tell proxies to store different version of the static file according - * to users encoding capacities. - * - * Warning: - * Compressed filed are stored in the /tmp directory. - * If this method is used with two files bearing the same name but located in different locations, - * there is a risk of conflict. One file could be served with the content of the other. - * A future upgrade of this method would be to recreate the directory structure of the static file - * within a /tmp/compressed-static-files directory. - * - * @param string $file The location of the static file to serve - * @param string $contentType The content type of the static file. - * @param bool $expireFarFuture If set to true, will set Expires: header in far future. - * Should be set to false for files that don't have a cache buster (eg. piwik.js) - */ - static public function serveStaticFile($file, $contentType, $expireFarFuture = true) - { - if (file_exists($file)) - { - // conditional GET - $modifiedSince = ''; - if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) - { - $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE']; - - // strip any trailing data appended to header - if (false !== ($semicolon = strpos($modifiedSince, ';'))) - { - $modifiedSince = substr($modifiedSince, 0, $semicolon); - } - } - - $fileModifiedTime = @filemtime($file); - $lastModified = gmdate('D, d M Y H:i:s', $fileModifiedTime) . ' GMT'; - - // set HTTP response headers - self::overrideCacheControlHeaders('public'); - @header('Vary: Accept-Encoding'); - @header('Content-Disposition: inline; filename='.basename($file)); - - if($expireFarFuture) - { - // Required by proxy caches potentially in between the browser and server to cache the request indeed - @header("Expires: ".gmdate('D, d M Y H:i:s', time() + 86400 * 100) . ' GMT'); - } - - // Returns 304 if not modified since - if ($modifiedSince === $lastModified) - { - self::setHttpStatus('304 Not Modified'); - } - else - { - // optional compression - $compressed = false; - $encoding = ''; - $compressedFileLocation = PIWIK_USER_PATH . self::COMPRESSED_FILE_LOCATION . basename($file); - - $phpOutputCompressionEnabled = self::isPhpOutputCompressed(); - if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled) - { - $acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING']; - - if (extension_loaded('zlib') && function_exists('file_get_contents') && function_exists('file_put_contents')) - { - if (preg_match('/(?:^|, ?)(deflate)(?:,|$)/', $acceptEncoding, $matches)) - { - $encoding = 'deflate'; - $filegz = $compressedFileLocation .'.deflate'; - } - else if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches)) - { - $encoding = $matches[1]; - $filegz = $compressedFileLocation .'.gz'; - } - - if (!empty($encoding)) - { - // compress-on-demand and use cache - if(!file_exists($filegz) || ($fileModifiedTime > @filemtime($filegz))) - { - $data = file_get_contents($file); - - if ($encoding == 'deflate') - { - $data = gzdeflate($data, 9); - } - else if ($encoding == 'gzip' || $encoding == 'x-gzip') - { - $data = gzencode($data, 9); - } - - file_put_contents($filegz, $data); - } - - $compressed = true; - $file = $filegz; - } - } - else - { - // manually compressed - $filegz = $compressedFileLocation .'.gz'; - if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches) && file_exists($filegz) && ($fileModifiedTime < @filemtime($filegz))) - { - $encoding = $matches[1]; - $compressed = true; - $file = $filegz; - } - } - } - - @header('Last-Modified: ' . $lastModified); - - if(!$phpOutputCompressionEnabled) - { - @header('Content-Length: ' . filesize($file)); - } - - if(!empty($contentType)) - { - @header('Content-Type: '.$contentType); - } - - if($compressed) - { - @header('Content-Encoding: ' . $encoding); - } - - if(!_readfile($file)) - { - self::setHttpStatus('505 Internal server error'); - } - } - } - else - { - self::setHttpStatus('404 Not Found'); - } - } - - /** - * Create CSV (or other delimited) files - * - * @param string $filePath filename to create - * @param array $fileSpec File specifications (delimiter, line terminator, etc) - * @param array $rows Array of array corresponding to rows of values - * @throws Exception if unable to create or write to file - */ - static public function createCSVFile($filePath, $fileSpec, $rows) - { - // Set up CSV delimiters, quotes, etc - $delim = $fileSpec['delim']; - $quote = $fileSpec['quote']; - $eol = $fileSpec['eol']; - $null = $fileSpec['null']; - $escapespecial_cb = $fileSpec['escapespecial_cb']; - - $fp = @fopen($filePath, 'wb'); - if (!$fp) - { - throw new Exception('Error creating the tmp file '.$filePath.', please check that the webserver has write permission to write this file.'); - } - - foreach ($rows as $row) - { - $output = ''; - foreach($row as $value) - { - if(!isset($value) || is_null($value) || $value === false) - { - $output .= $null.$delim; - } - else - { - $output .= $quote.$escapespecial_cb($value).$quote.$delim; - } - } - - // Replace delim with eol - $output = substr_replace($output, $eol, -1); - - $ret = fwrite($fp, $output); - if (!$ret) { - fclose($fp); - throw new Exception('Error writing to the tmp file '.$filePath); - } - } - fclose($fp); - - @chmod($filePath, 0777); - } - -/* + } + } + + /** + * 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() + { + $messages = array(); + $messages[] = true; + + // ignore dev environments + if (file_exists(PIWIK_INCLUDE_PATH . '/.git')) { + $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'); + $hasMd5 = function_exists('md5'); + foreach ($files as $path => $props) { + $file = PIWIK_INCLUDE_PATH . '/' . $path; + + if (!file_exists($file)) { + $messages[] = Piwik_Translate('General_ExceptionMissingFile', $file); + } else if (filesize($file) != $props[0]) { + if (!$hasMd5 || in_array(substr($path, -4), array('.gif', '.ico', '.jpg', '.png', '.swf'))) { + // files that contain binary data (e.g., images) must match the file size + $messages[] = Piwik_Translate('General_ExceptionFilesizeMismatch', array($file, $props[0], filesize($file))); + } else { + // convert end-of-line characters and re-test text files + $content = @file_get_contents($file); + $content = str_replace("\r\n", "\n", $content); + if ((strlen($content) != $props[0]) + || (@md5($content) !== $props[1]) + ) { + $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; + } + + /** + * Test if php output is compressed + * + * @return bool True if php output is (or suspected/likely) to be compressed + */ + static public function isPhpOutputCompressed() + { + // Off = ''; On = '1'; otherwise, it's a buffer size + $zlibOutputCompression = ini_get('zlib.output_compression'); + + // could be ob_gzhandler, ob_deflatehandler, etc + $outputHandler = ini_get('output_handler'); + + // output handlers can be stacked + $obHandlers = array_filter(ob_list_handlers(), create_function('$var', 'return $var !== "default output handler";')); + + // user defined handler via wrapper + $autoPrependFile = ini_get('auto_prepend_file'); + $autoAppendFile = ini_get('auto_append_file'); + + return !empty($zlibOutputCompression) || + !empty($outputHandler) || + !empty($obHandlers) || + !empty($autoPrependFile) || + !empty($autoAppendFile); + } + + /** + * Serve static files through php proxy. + * + * It performs the following actions: + * - Checks the file is readable or returns "HTTP/1.0 404 Not Found" + * - Returns "HTTP/1.1 304 Not Modified" after comparing the HTTP_IF_MODIFIED_SINCE + * with the modification date of the static file + * - Will try to compress the static file according to HTTP_ACCEPT_ENCODING. Compressed files are store in + * the /tmp directory. If compressing extensions are not available, a manually gzip compressed file + * can be provided in the /tmp directory. It has to bear the same name with an added .gz extension. + * Using manually compressed static files requires you to manually update the compressed file when + * the static file is updated. + * - Overrides server cache control config to allow caching + * - Sends Very Accept-Encoding to tell proxies to store different version of the static file according + * to users encoding capacities. + * + * Warning: + * Compressed filed are stored in the /tmp directory. + * If this method is used with two files bearing the same name but located in different locations, + * there is a risk of conflict. One file could be served with the content of the other. + * A future upgrade of this method would be to recreate the directory structure of the static file + * within a /tmp/compressed-static-files directory. + * + * @param string $file The location of the static file to serve + * @param string $contentType The content type of the static file. + * @param bool $expireFarFuture If set to true, will set Expires: header in far future. + * Should be set to false for files that don't have a cache buster (eg. piwik.js) + */ + static public function serveStaticFile($file, $contentType, $expireFarFuture = true) + { + if (file_exists($file)) { + // conditional GET + $modifiedSince = ''; + if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { + $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE']; + + // strip any trailing data appended to header + if (false !== ($semicolon = strpos($modifiedSince, ';'))) { + $modifiedSince = substr($modifiedSince, 0, $semicolon); + } + } + + $fileModifiedTime = @filemtime($file); + $lastModified = gmdate('D, d M Y H:i:s', $fileModifiedTime) . ' GMT'; + + // set HTTP response headers + self::overrideCacheControlHeaders('public'); + @header('Vary: Accept-Encoding'); + @header('Content-Disposition: inline; filename=' . basename($file)); + + if ($expireFarFuture) { + // Required by proxy caches potentially in between the browser and server to cache the request indeed + @header("Expires: " . gmdate('D, d M Y H:i:s', time() + 86400 * 100) . ' GMT'); + } + + // Returns 304 if not modified since + if ($modifiedSince === $lastModified) { + self::setHttpStatus('304 Not Modified'); + } else { + // optional compression + $compressed = false; + $encoding = ''; + $compressedFileLocation = PIWIK_USER_PATH . self::COMPRESSED_FILE_LOCATION . basename($file); + + $phpOutputCompressionEnabled = self::isPhpOutputCompressed(); + if (isset($_SERVER['HTTP_ACCEPT_ENCODING']) && !$phpOutputCompressionEnabled) { + $acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING']; + + if (extension_loaded('zlib') && function_exists('file_get_contents') && function_exists('file_put_contents')) { + if (preg_match('/(?:^|, ?)(deflate)(?:,|$)/', $acceptEncoding, $matches)) { + $encoding = 'deflate'; + $filegz = $compressedFileLocation . '.deflate'; + } else if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches)) { + $encoding = $matches[1]; + $filegz = $compressedFileLocation . '.gz'; + } + + if (!empty($encoding)) { + // compress-on-demand and use cache + if (!file_exists($filegz) || ($fileModifiedTime > @filemtime($filegz))) { + $data = file_get_contents($file); + + if ($encoding == 'deflate') { + $data = gzdeflate($data, 9); + } else if ($encoding == 'gzip' || $encoding == 'x-gzip') { + $data = gzencode($data, 9); + } + + file_put_contents($filegz, $data); + } + + $compressed = true; + $file = $filegz; + } + } else { + // manually compressed + $filegz = $compressedFileLocation . '.gz'; + if (preg_match('/(?:^|, ?)((x-)?gzip)(?:,|$)/', $acceptEncoding, $matches) && file_exists($filegz) && ($fileModifiedTime < @filemtime($filegz))) { + $encoding = $matches[1]; + $compressed = true; + $file = $filegz; + } + } + } + + @header('Last-Modified: ' . $lastModified); + + if (!$phpOutputCompressionEnabled) { + @header('Content-Length: ' . filesize($file)); + } + + if (!empty($contentType)) { + @header('Content-Type: ' . $contentType); + } + + if ($compressed) { + @header('Content-Encoding: ' . $encoding); + } + + if (!_readfile($file)) { + self::setHttpStatus('505 Internal server error'); + } + } + } else { + self::setHttpStatus('404 Not Found'); + } + } + + /** + * Create CSV (or other delimited) files + * + * @param string $filePath filename to create + * @param array $fileSpec File specifications (delimiter, line terminator, etc) + * @param array $rows Array of array corresponding to rows of values + * @throws Exception if unable to create or write to file + */ + static public function createCSVFile($filePath, $fileSpec, $rows) + { + // Set up CSV delimiters, quotes, etc + $delim = $fileSpec['delim']; + $quote = $fileSpec['quote']; + $eol = $fileSpec['eol']; + $null = $fileSpec['null']; + $escapespecial_cb = $fileSpec['escapespecial_cb']; + + $fp = @fopen($filePath, 'wb'); + if (!$fp) { + throw new Exception('Error creating the tmp file ' . $filePath . ', please check that the webserver has write permission to write this file.'); + } + + foreach ($rows as $row) { + $output = ''; + foreach ($row as $value) { + if (!isset($value) || is_null($value) || $value === false) { + $output .= $null . $delim; + } else { + $output .= $quote . $escapespecial_cb($value) . $quote . $delim; + } + } + + // Replace delim with eol + $output = substr_replace($output, $eol, -1); + + $ret = fwrite($fp, $output); + if (!$ret) { + fclose($fp); + throw new Exception('Error writing to the tmp file ' . $filePath); + } + } + fclose($fp); + + @chmod($filePath, 0777); + } + + /* * PHP environment settings */ - /** - * Set maximum script execution time. - * - * @param int $executionTime max execution time in seconds (0 = no limit) - */ - static public function setMaxExecutionTime($executionTime) - { - // in the event one or the other is disabled... - @ini_set('max_execution_time', $executionTime); - @set_time_limit($executionTime); - } - - /** - * Get php memory_limit (in Megabytes) - * - * 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|false memory limit in megabytes, or false if there is no limit - */ - static public function getMemoryLimitValue() - { - if(($memory = ini_get('memory_limit')) > 0) - { - // 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; - } - - // no memory limit - 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) - { - // in Megabytes - $currentValue = self::getMemoryLimitValue(); - if( $currentValue === false - || ($currentValue < $minimumMemoryLimit && @ini_set('memory_limit', $minimumMemoryLimit.'M'))) - { - return true; - } - return false; - } - - /** - * Raise PHP memory limit if below the minimum required - * - * @return bool true if set; false otherwise - */ - static public function raiseMemoryLimitIfNecessary() - { - $memoryLimit = self::getMemoryLimitValue(); - if($memoryLimit === false) - { - return false; - } - $minimumMemoryLimit = Piwik_Config::getInstance()->General['minimum_memory_limit']; - - if(Piwik_Common::isArchivePhpTriggered() - && Piwik::isUserIsSuperUser()) - { - // archive.php: no time limit, high memory limit - self::setMaxExecutionTime(0); - $minimumMemoryLimitWhenArchiving = Piwik_Config::getInstance()->General['minimum_memory_limit_when_archiving']; - if($memoryLimit < $minimumMemoryLimitWhenArchiving) - { - return self::setMemoryLimit($minimumMemoryLimitWhenArchiving); - } - return false; - } - if($memoryLimit < $minimumMemoryLimit) - { - return self::setMemoryLimit($minimumMemoryLimit); - } - return false; - } - -/* + /** + * Set maximum script execution time. + * + * @param int $executionTime max execution time in seconds (0 = no limit) + */ + static public function setMaxExecutionTime($executionTime) + { + // in the event one or the other is disabled... + @ini_set('max_execution_time', $executionTime); + @set_time_limit($executionTime); + } + + /** + * Get php memory_limit (in Megabytes) + * + * 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|false memory limit in megabytes, or false if there is no limit + */ + static public function getMemoryLimitValue() + { + if (($memory = ini_get('memory_limit')) > 0) { + // 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; + } + + // no memory limit + 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) + { + // in Megabytes + $currentValue = self::getMemoryLimitValue(); + if ($currentValue === false + || ($currentValue < $minimumMemoryLimit && @ini_set('memory_limit', $minimumMemoryLimit . 'M')) + ) { + return true; + } + return false; + } + + /** + * Raise PHP memory limit if below the minimum required + * + * @return bool true if set; false otherwise + */ + static public function raiseMemoryLimitIfNecessary() + { + $memoryLimit = self::getMemoryLimitValue(); + if ($memoryLimit === false) { + return false; + } + $minimumMemoryLimit = Piwik_Config::getInstance()->General['minimum_memory_limit']; + + if (Piwik_Common::isArchivePhpTriggered() + && Piwik::isUserIsSuperUser() + ) { + // archive.php: no time limit, high memory limit + self::setMaxExecutionTime(0); + $minimumMemoryLimitWhenArchiving = Piwik_Config::getInstance()->General['minimum_memory_limit_when_archiving']; + if ($memoryLimit < $minimumMemoryLimitWhenArchiving) { + return self::setMemoryLimit($minimumMemoryLimitWhenArchiving); + } + return false; + } + if ($memoryLimit < $minimumMemoryLimit) { + return self::setMemoryLimit($minimumMemoryLimit); + } + return false; + } + + /* * Logging and error handling */ - public static $shouldLog = null; - - /** - * Log a message - * - * @param string $message - */ - static public function log($message = '') - { - if(is_null(self::$shouldLog)) - { - self::$shouldLog = self::shouldLoggerLog(); - // It is possible that the logger is not setup: - // - Tracker request, and debug disabled, - // - and some scheduled tasks call code that tries and log something - try { - Zend_Registry::get('logger_message'); - } catch(Exception $e) { - self::$shouldLog = false; - } - } - if(self::$shouldLog) - { - Zend_Registry::get('logger_message')->logEvent($message); - } - } - - /** - * Returns if logging should work - * @return bool - */ - static public function shouldLoggerLog() - { - try { - $shouldLog = (Piwik_Common::isPhpCliMode() - || Piwik_Config::getInstance()->log['log_only_when_cli'] == 0) - && - ( Piwik_Config::getInstance()->log['log_only_when_debug_parameter'] == 0 - || isset($_REQUEST['debug'])) - ; - } catch(Exception $e) { - $shouldLog = false; - } - return $shouldLog; - } - - /** - * Trigger E_USER_ERROR with optional message - * - * @param string $message - */ - 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 - * - * @param string $message - */ - static public function exitWithErrorMessage( $message ) - { - $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' />". - $message. - "</p></div>"; - print(Piwik_Log_Formatter_ScreenFormatter::getFormattedString($output)); - exit; - } - -/* + public static $shouldLog = null; + + /** + * Log a message + * + * @param string $message + */ + static public function log($message = '') + { + if (is_null(self::$shouldLog)) { + self::$shouldLog = self::shouldLoggerLog(); + // It is possible that the logger is not setup: + // - Tracker request, and debug disabled, + // - and some scheduled tasks call code that tries and log something + try { + Zend_Registry::get('logger_message'); + } catch (Exception $e) { + self::$shouldLog = false; + } + } + if (self::$shouldLog) { + Zend_Registry::get('logger_message')->logEvent($message); + } + } + + /** + * Returns if logging should work + * @return bool + */ + static public function shouldLoggerLog() + { + try { + $shouldLog = (Piwik_Common::isPhpCliMode() + || Piwik_Config::getInstance()->log['log_only_when_cli'] == 0) + && + (Piwik_Config::getInstance()->log['log_only_when_debug_parameter'] == 0 + || isset($_REQUEST['debug'])); + } catch (Exception $e) { + $shouldLog = false; + } + return $shouldLog; + } + + /** + * Trigger E_USER_ERROR with optional message + * + * @param string $message + */ + 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 + * + * @param string $message + */ + static public function exitWithErrorMessage($message) + { + $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' />" . + $message . + "</p></div>"; + print(Piwik_Log_Formatter_ScreenFormatter::getFormattedString($output)); + exit; + } + + /* * Profiling */ - /** - * Get total number of queries - * - * @return int number of queries - */ - 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(sprintf("Total queries = %d (total sql time = %.2fs)", $queryCount, $totalTime)); - } - - /** - * 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')) - { - function maxSumMsFirst($a,$b) - { - 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) - { - return; - } - uasort($all, 'maxSumMsFirst'); - - $infoIndexedByQuery = array(); - foreach($all as $infoQuery) - { - $query = $infoQuery['query']; - $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 - * It is automatically called when enabling the SQL profiling in the config file enable_sql_profiler - * @throws Exception - */ - 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) - { - if(isset($infoIndexedByQuery[$query->getQuery()])) - { - $existing = $infoIndexedByQuery[$query->getQuery()]; - } - else - { - $existing = array( 'count' => 0, 'sumTimeMs' => 0); - } - $new = array( 'count' => $existing['count'] + 1, - 'sumTimeMs' => $existing['count'] + $query->getElapsedSecs() * 1000); - $infoIndexedByQuery[$query->getQuery()] = $new; - } - - if(!function_exists('sortTimeDesc')) - { - function sortTimeDesc($a,$b) - { - return $a['sumTimeMs'] < $b['sumTimeMs']; - } - } - uasort( $infoIndexedByQuery, 'sortTimeDesc'); - - $str = '<hr /><b>SQL Profiler</b><hr /><b>Summary</b><br/>'; - $totalTime = $profiler->getTotalElapsedSecs(); - $queryCount = $profiler->getTotalNumQueries(); - $longestTime = 0; - $longestQuery = null; - foreach ($profiler->getQueryProfiles() as $query) { - if ($query->getElapsedSecs() > $longestTime) { - $longestTime = $query->getElapsedSecs(); - $longestQuery = $query->getQuery(); - } - } - $str .= 'Executed ' . $queryCount . ' queries in ' . round($totalTime,3) . ' seconds'; - $str .= '(Average query length: ' . round($totalTime / $queryCount,3) . ' seconds)'; - $str .= '<br />Queries per second: ' . round($queryCount / $totalTime,1) ; - $str .= '<br />Longest query length: ' . round($longestTime,3) . " seconds (<code>$longestQuery</code>)"; - Piwik::log($str); - Piwik::getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery); - } - - /** - * Log a breakdown by query - * - * @param array $infoIndexedByQuery - */ - static private function getSqlProfilingQueryBreakdownOutput( $infoIndexedByQuery ) - { - $output = '<hr /><b>Breakdown by query</b><br/>'; - foreach($infoIndexedByQuery as $query => $queryInfo) - { - $timeMs = round($queryInfo['sumTimeMs'],1); - $count = $queryInfo['count']; - $avgTimeString = ''; - if($count > 1) - { - $avgTimeMs = $timeMs / $count; - $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() - { - Piwik::log(Zend_Registry::get('timer')); - } - - /** - * 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 - * - * @return string - */ - static public function getMemoryUsage() - { - $memory = false; - if(function_exists('xdebug_memory_usage')) - { - $memory = xdebug_memory_usage(); - } - elseif(function_exists('memory_get_usage')) - { - $memory = memory_get_usage(); - } - if($memory === false) - { - return "Memory usage function not found."; - } - $usage = number_format( round($memory / 1024 / 1024, 2), 2); - return "$usage Mb"; - } - -/* + /** + * Get total number of queries + * + * @return int number of queries + */ + 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(sprintf("Total queries = %d (total sql time = %.2fs)", $queryCount, $totalTime)); + } + + /** + * 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')) { + function maxSumMsFirst($a, $b) + { + 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) { + return; + } + uasort($all, 'maxSumMsFirst'); + + $infoIndexedByQuery = array(); + foreach ($all as $infoQuery) { + $query = $infoQuery['query']; + $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 + * It is automatically called when enabling the SQL profiling in the config file enable_sql_profiler + * @throws Exception + */ + 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) { + if (isset($infoIndexedByQuery[$query->getQuery()])) { + $existing = $infoIndexedByQuery[$query->getQuery()]; + } else { + $existing = array('count' => 0, 'sumTimeMs' => 0); + } + $new = array('count' => $existing['count'] + 1, + 'sumTimeMs' => $existing['count'] + $query->getElapsedSecs() * 1000); + $infoIndexedByQuery[$query->getQuery()] = $new; + } + + if (!function_exists('sortTimeDesc')) { + function sortTimeDesc($a, $b) + { + return $a['sumTimeMs'] < $b['sumTimeMs']; + } + } + uasort($infoIndexedByQuery, 'sortTimeDesc'); + + $str = '<hr /><b>SQL Profiler</b><hr /><b>Summary</b><br/>'; + $totalTime = $profiler->getTotalElapsedSecs(); + $queryCount = $profiler->getTotalNumQueries(); + $longestTime = 0; + $longestQuery = null; + foreach ($profiler->getQueryProfiles() as $query) { + if ($query->getElapsedSecs() > $longestTime) { + $longestTime = $query->getElapsedSecs(); + $longestQuery = $query->getQuery(); + } + } + $str .= 'Executed ' . $queryCount . ' queries in ' . round($totalTime, 3) . ' seconds'; + $str .= '(Average query length: ' . round($totalTime / $queryCount, 3) . ' seconds)'; + $str .= '<br />Queries per second: ' . round($queryCount / $totalTime, 1); + $str .= '<br />Longest query length: ' . round($longestTime, 3) . " seconds (<code>$longestQuery</code>)"; + Piwik::log($str); + Piwik::getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery); + } + + /** + * Log a breakdown by query + * + * @param array $infoIndexedByQuery + */ + static private function getSqlProfilingQueryBreakdownOutput($infoIndexedByQuery) + { + $output = '<hr /><b>Breakdown by query</b><br/>'; + foreach ($infoIndexedByQuery as $query => $queryInfo) { + $timeMs = round($queryInfo['sumTimeMs'], 1); + $count = $queryInfo['count']; + $avgTimeString = ''; + if ($count > 1) { + $avgTimeMs = $timeMs / $count; + $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() + { + Piwik::log(Zend_Registry::get('timer')); + } + + /** + * 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 + * + * @return string + */ + static public function getMemoryUsage() + { + $memory = false; + if (function_exists('xdebug_memory_usage')) { + $memory = xdebug_memory_usage(); + } elseif (function_exists('memory_get_usage')) { + $memory = memory_get_usage(); + } + if ($memory === false) { + return "Memory usage function not found."; + } + $usage = number_format(round($memory / 1024 / 1024, 2), 2); + return "$usage Mb"; + } + + /* * Amounts, Percentages, Currency, Time, Math Operations, and Pretty Printing */ - /** - * Returns a list of currency symbols - * - * @return array array( currencyCode => symbol, ... ) - */ - static public function getCurrencyList() - { - static $currenciesList = null; - if(is_null($currenciesList)) - { - require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Currencies.php'; - $currenciesList = $GLOBALS['Piwik_CurrencyList']; - } - return $currenciesList; - } - - /** - * 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 number $i1 - * @param number $i2 - * @return number The result of the division or zero - */ - static public function secureDiv( $i1, $i2 ) - { - if ( is_numeric($i1) && is_numeric($i2) && floatval($i2) != 0) - { - return $i1 / $i2; - } - return 0; - } - - /** - * Safely compute a percentage. Return 0 to avoid division by zero. - * - * @param number $dividend - * @param number $divisor - * @param int $precision - * @return number - */ - static public function getPercentageSafe($dividend, $divisor, $precision = 0) - { - if($divisor == 0) - { - return 0; - } - return round(100 * $dividend / $divisor, $precision); - } - - /** - * Get currency symbol for a site - * - * @param int $idSite - * @return string - */ - static public function getCurrency($idSite) - { - $symbols = self::getCurrencyList(); - $site = new Piwik_Site($idSite); - $currency = $site->getCurrency(); - if(isset($symbols[$currency])) - { - return $symbols[$currency][0]; - } - - return ''; - } - - /** - * For the given value, based on the column name, will apply: pretty time, pretty money - * @param int $idSite - * @param string $columnName - * @param mixed $value - * @param bool $htmlAllowed - * @param string $timeAsSentence - * @return string - */ - static public function getPrettyValue($idSite, $columnName, $value, $htmlAllowed, $timeAsSentence) - { - // Display time in human readable - if(strpos($columnName, 'time') !== false) - { - return Piwik::getPrettyTimeFromSeconds($value, $timeAsSentence); - } - // Add revenue symbol to revenues - if(strpos($columnName, 'revenue') !== false && strpos($columnName, 'evolution') === false) - { - return Piwik::getPrettyMoney($value, $idSite, $htmlAllowed); - } - // Add % symbol to rates - if(strpos($columnName, '_rate') !== false) - { - if(strpos($value, "%") === false) - { - return $value . "%"; - } - } - return $value; - } - - /** - * Pretty format monetary value for a site - * - * @param int|string $value - * @param int $idSite - * @param bool $htmlAllowed - * @return string - */ - static public function getPrettyMoney($value, $idSite, $htmlAllowed = true) - { - $currencyBefore = self::getCurrency($idSite); - - $space = ' '; - if($htmlAllowed) - { - $space = ' '; - } - - $currencyAfter = ''; - // manually put the currency symbol after the amount for euro - // (maybe more currencies prefer this notation?) - if(in_array($currencyBefore,array('€', 'kr'))) - { - $currencyAfter = $space.$currencyBefore; - $currencyBefore = ''; - } - - // if the input is a number (it could be a string or INPUT form), - // and if this number is not an int, we round to precision 2 - if(is_numeric($value)) - { - if($value == round($value)) - { - // 0.0 => 0 - $value = round($value); - } - else - { - $precision = Piwik_Tracker_GoalManager::REVENUE_PRECISION; - $value = sprintf( "%01.".$precision."f", $value); - } - } - $prettyMoney = $currencyBefore . $space . $value . $currencyAfter; - return $prettyMoney; - } - - /** - * Pretty format a memory size value - * - * @param number $size size in bytes - * @param string $unit The specific unit to use, if any. If null, the unit is determined by $size. - * @param int $precision The precision to use when rounding. - * @return string - */ - static public function getPrettySizeFromBytes( $size, $unit = null, $precision = 1 ) - { - if ($size == 0) - { - return '0 M'; - } - - $units = array('B','K','M','G','T'); - foreach($units as $currentUnit) - { - if ($size >= 1024 && $unit != $currentUnit) - { - $size = $size / 1024; - } - else - { - break; - } - } - return round($size, $precision)." ".$currentUnit; - } - - /** - * Pretty format a time - * - * @param int $numberOfSeconds - * @param bool $displayTimeAsSentence If set to true, will output "5min 17s", if false "00:05:17" - * @param bool $isHtml - * @param bool $round to the full seconds - * @return string - */ - static public function getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence = true, $isHtml = true, $round = false) - { - $numberOfSeconds = $round ? (int)$numberOfSeconds : (float)$numberOfSeconds; - - // Display 01:45:17 time format - if($displayTimeAsSentence === false) - { - $hours = floor( $numberOfSeconds / 3600); - $minutes = floor( ($reminder = ($numberOfSeconds - $hours * 3600)) / 60 ); - $seconds = floor( $reminder - $minutes * 60 ); - $time = sprintf("%02s", $hours) . ':' . sprintf("%02s", $minutes) .':'. sprintf("%02s", $seconds); - $milliSeconds = ($numberOfSeconds * 1000) % 1000; - if ($milliSeconds) { - $time .= '.' . $milliSeconds; - } - return $time; - } - $secondsInYear = 86400 * 365.25; - $years = floor($numberOfSeconds / $secondsInYear); - $minusYears = $numberOfSeconds - $years * $secondsInYear; - $days = floor($minusYears / 86400); - - $minusDays = $numberOfSeconds - $days * 86400; - $hours = floor($minusDays / 3600); - - $minusDaysAndHours = $minusDays - $hours * 3600; - $minutes = floor($minusDaysAndHours / 60 ); - - $seconds = $minusDaysAndHours - $minutes * 60; - - if($years > 0) - { - $return = sprintf(Piwik_Translate('General_YearsDays'), $years, $days); - } - elseif($days > 0) - { - $return = sprintf(Piwik_Translate('General_DaysHours'), $days, $hours); - } - elseif($hours > 0) - { - $return = sprintf(Piwik_Translate('General_HoursMinutes'), $hours, $minutes); - } - elseif($minutes > 0) - { - $return = sprintf(Piwik_Translate('General_MinutesSeconds'), $minutes, $seconds); - } - else - { - $return = sprintf(Piwik_Translate('General_Seconds'), $seconds); - } - if($isHtml) - { - return str_replace(' ', ' ', $return); - } - return $return; - } - - /** - * Gets a prettified string representation of a number. The result will have - * thousands separators and a decimal point specific to the current locale. - * - * @param number $value - * @return string - */ - static public function getPrettyNumber( $value ) - { - $locale = localeconv(); - - $decimalPoint = $locale['decimal_point']; - $thousandsSeparator = $locale['thousands_sep']; - - return number_format($value, 0, $decimalPoint, $thousandsSeparator); - } - - /** - * Returns the Javascript code to be inserted on every page to track - * - * @param int $idSite - * @param string $piwikUrl http://path/to/piwik/directory/ - * @return string - */ - static public function getJavascriptCode($idSite, $piwikUrl) - { - $jsCode = file_get_contents( PIWIK_INCLUDE_PATH . "/core/Tracker/javascriptCode.tpl"); - $jsCode = htmlentities($jsCode); - preg_match('~^(http|https)://(.*)$~D', $piwikUrl, $matches); - $piwikUrl = @$matches[2]; - $jsCode = str_replace('{$idSite}', $idSite, $jsCode); - $jsCode = str_replace('{$piwikUrl}', Piwik_Common::sanitizeInputValue($piwikUrl), $jsCode); - return $jsCode; - } - - /** - * Generate a title for image tags - * - * @return string - */ - static public function getRandomTitle() - { - static $titles = array( - 'Web analytics', - 'Real Time Web Analytics', - 'Analytics', - 'Real Time Analytics', - 'Analytics in Real time', - 'Open Source Analytics', - 'Open Source Web Analytics', - 'Free Website Analytics', - 'Free Web Analytics', - 'Analytics Platform', - ); - $id = abs(intval(md5(Piwik_Url::getCurrentHost()))); - $title = $titles[ $id % count($titles)]; - return $title; - } - - /** - * Number of websites to show in the Website selector - * - * @return int - */ - static public function getWebsitesCountToDisplay() - { - $count = max(Piwik_Config::getInstance()->General['site_selector_max_sites'], - Piwik_Config::getInstance()->General['autocomplete_min_sites']); - return (int)$count; - } - - /** - * Segments to pre-process - * - * @return string - */ - static public function getKnownSegmentsToArchive() - { - static $cachedResult = null; - - if (is_null($cachedResult)) - { - $segments = Piwik_Config::getInstance()->Segments; - $cachedResult = isset($segments['Segments']) ? $segments['Segments'] : ''; - } - - return $cachedResult; - } - -/* + /** + * Returns a list of currency symbols + * + * @return array array( currencyCode => symbol, ... ) + */ + static public function getCurrencyList() + { + static $currenciesList = null; + if (is_null($currenciesList)) { + require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Currencies.php'; + $currenciesList = $GLOBALS['Piwik_CurrencyList']; + } + return $currenciesList; + } + + /** + * 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 number $i1 + * @param number $i2 + * @return number The result of the division or zero + */ + static public function secureDiv($i1, $i2) + { + if (is_numeric($i1) && is_numeric($i2) && floatval($i2) != 0) { + return $i1 / $i2; + } + return 0; + } + + /** + * Safely compute a percentage. Return 0 to avoid division by zero. + * + * @param number $dividend + * @param number $divisor + * @param int $precision + * @return number + */ + static public function getPercentageSafe($dividend, $divisor, $precision = 0) + { + if ($divisor == 0) { + return 0; + } + return round(100 * $dividend / $divisor, $precision); + } + + /** + * Get currency symbol for a site + * + * @param int $idSite + * @return string + */ + static public function getCurrency($idSite) + { + $symbols = self::getCurrencyList(); + $site = new Piwik_Site($idSite); + $currency = $site->getCurrency(); + if (isset($symbols[$currency])) { + return $symbols[$currency][0]; + } + + return ''; + } + + /** + * For the given value, based on the column name, will apply: pretty time, pretty money + * @param int $idSite + * @param string $columnName + * @param mixed $value + * @param bool $htmlAllowed + * @param string $timeAsSentence + * @return string + */ + static public function getPrettyValue($idSite, $columnName, $value, $htmlAllowed, $timeAsSentence) + { + // Display time in human readable + if (strpos($columnName, 'time') !== false) { + return Piwik::getPrettyTimeFromSeconds($value, $timeAsSentence); + } + // Add revenue symbol to revenues + if (strpos($columnName, 'revenue') !== false && strpos($columnName, 'evolution') === false) { + return Piwik::getPrettyMoney($value, $idSite, $htmlAllowed); + } + // Add % symbol to rates + if (strpos($columnName, '_rate') !== false) { + if (strpos($value, "%") === false) { + return $value . "%"; + } + } + return $value; + } + + /** + * Pretty format monetary value for a site + * + * @param int|string $value + * @param int $idSite + * @param bool $htmlAllowed + * @return string + */ + static public function getPrettyMoney($value, $idSite, $htmlAllowed = true) + { + $currencyBefore = self::getCurrency($idSite); + + $space = ' '; + if ($htmlAllowed) { + $space = ' '; + } + + $currencyAfter = ''; + // manually put the currency symbol after the amount for euro + // (maybe more currencies prefer this notation?) + if (in_array($currencyBefore, array('€', 'kr'))) { + $currencyAfter = $space . $currencyBefore; + $currencyBefore = ''; + } + + // if the input is a number (it could be a string or INPUT form), + // and if this number is not an int, we round to precision 2 + if (is_numeric($value)) { + if ($value == round($value)) { + // 0.0 => 0 + $value = round($value); + } else { + $precision = Piwik_Tracker_GoalManager::REVENUE_PRECISION; + $value = sprintf("%01." . $precision . "f", $value); + } + } + $prettyMoney = $currencyBefore . $space . $value . $currencyAfter; + return $prettyMoney; + } + + /** + * Pretty format a memory size value + * + * @param number $size size in bytes + * @param string $unit The specific unit to use, if any. If null, the unit is determined by $size. + * @param int $precision The precision to use when rounding. + * @return string + */ + static public function getPrettySizeFromBytes($size, $unit = null, $precision = 1) + { + if ($size == 0) { + return '0 M'; + } + + $units = array('B', 'K', 'M', 'G', 'T'); + foreach ($units as $currentUnit) { + if ($size >= 1024 && $unit != $currentUnit) { + $size = $size / 1024; + } else { + break; + } + } + return round($size, $precision) . " " . $currentUnit; + } + + /** + * Pretty format a time + * + * @param int $numberOfSeconds + * @param bool $displayTimeAsSentence If set to true, will output "5min 17s", if false "00:05:17" + * @param bool $isHtml + * @param bool $round to the full seconds + * @return string + */ + static public function getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence = true, $isHtml = true, $round = false) + { + $numberOfSeconds = $round ? (int)$numberOfSeconds : (float)$numberOfSeconds; + + // Display 01:45:17 time format + if ($displayTimeAsSentence === false) { + $hours = floor($numberOfSeconds / 3600); + $minutes = floor(($reminder = ($numberOfSeconds - $hours * 3600)) / 60); + $seconds = floor($reminder - $minutes * 60); + $time = sprintf("%02s", $hours) . ':' . sprintf("%02s", $minutes) . ':' . sprintf("%02s", $seconds); + $milliSeconds = ($numberOfSeconds * 1000) % 1000; + if ($milliSeconds) { + $time .= '.' . $milliSeconds; + } + return $time; + } + $secondsInYear = 86400 * 365.25; + $years = floor($numberOfSeconds / $secondsInYear); + $minusYears = $numberOfSeconds - $years * $secondsInYear; + $days = floor($minusYears / 86400); + + $minusDays = $numberOfSeconds - $days * 86400; + $hours = floor($minusDays / 3600); + + $minusDaysAndHours = $minusDays - $hours * 3600; + $minutes = floor($minusDaysAndHours / 60); + + $seconds = $minusDaysAndHours - $minutes * 60; + + if ($years > 0) { + $return = sprintf(Piwik_Translate('General_YearsDays'), $years, $days); + } elseif ($days > 0) { + $return = sprintf(Piwik_Translate('General_DaysHours'), $days, $hours); + } elseif ($hours > 0) { + $return = sprintf(Piwik_Translate('General_HoursMinutes'), $hours, $minutes); + } elseif ($minutes > 0) { + $return = sprintf(Piwik_Translate('General_MinutesSeconds'), $minutes, $seconds); + } else { + $return = sprintf(Piwik_Translate('General_Seconds'), $seconds); + } + if ($isHtml) { + return str_replace(' ', ' ', $return); + } + return $return; + } + + /** + * Gets a prettified string representation of a number. The result will have + * thousands separators and a decimal point specific to the current locale. + * + * @param number $value + * @return string + */ + static public function getPrettyNumber($value) + { + $locale = localeconv(); + + $decimalPoint = $locale['decimal_point']; + $thousandsSeparator = $locale['thousands_sep']; + + return number_format($value, 0, $decimalPoint, $thousandsSeparator); + } + + /** + * Returns the Javascript code to be inserted on every page to track + * + * @param int $idSite + * @param string $piwikUrl http://path/to/piwik/directory/ + * @return string + */ + static public function getJavascriptCode($idSite, $piwikUrl) + { + $jsCode = file_get_contents(PIWIK_INCLUDE_PATH . "/core/Tracker/javascriptCode.tpl"); + $jsCode = htmlentities($jsCode); + preg_match('~^(http|https)://(.*)$~D', $piwikUrl, $matches); + $piwikUrl = @$matches[2]; + $jsCode = str_replace('{$idSite}', $idSite, $jsCode); + $jsCode = str_replace('{$piwikUrl}', Piwik_Common::sanitizeInputValue($piwikUrl), $jsCode); + return $jsCode; + } + + /** + * Generate a title for image tags + * + * @return string + */ + static public function getRandomTitle() + { + static $titles = array( + 'Web analytics', + 'Real Time Web Analytics', + 'Analytics', + 'Real Time Analytics', + 'Analytics in Real time', + 'Open Source Analytics', + 'Open Source Web Analytics', + 'Free Website Analytics', + 'Free Web Analytics', + 'Analytics Platform', + ); + $id = abs(intval(md5(Piwik_Url::getCurrentHost()))); + $title = $titles[$id % count($titles)]; + return $title; + } + + /** + * Number of websites to show in the Website selector + * + * @return int + */ + static public function getWebsitesCountToDisplay() + { + $count = max(Piwik_Config::getInstance()->General['site_selector_max_sites'], + Piwik_Config::getInstance()->General['autocomplete_min_sites']); + return (int)$count; + } + + /** + * Segments to pre-process + * + * @return string + */ + static public function getKnownSegmentsToArchive() + { + static $cachedResult = null; + + if (is_null($cachedResult)) { + $segments = Piwik_Config::getInstance()->Segments; + $cachedResult = isset($segments['Segments']) ? $segments['Segments'] : ''; + } + + return $cachedResult; + } + + /* * Access */ - /** - * Get current user email address - * - * @return string - */ - static public function getCurrentUserEmail() - { - if(!Piwik::isUserIsSuperUser()) - { - $user = Piwik_UsersManager_API::getInstance()->getUser(Piwik::getCurrentUserLogin()); - return $user['email']; - } - return self::getSuperUserEmail(); - } - - /** - * Returns Super User login - * - * @return string - */ - static public function getSuperUserLogin() - { - $superuser = Piwik_Config::getInstance()->superuser; - return $superuser['login']; - } - - /** - * Returns Super User email - * - * @return string - */ - static public function getSuperUserEmail() - { - $superuser = Piwik_Config::getInstance()->superuser; - return $superuser['email']; - } - - /** - * Get current user login - * - * @return string login ID - */ - static public function getCurrentUserLogin() - { - return Zend_Registry::get('access')->getLogin(); - } - - /** - * Get current user's token auth - * - * @return string Token auth - */ - static public function getCurrentUserTokenAuth() - { - 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 - */ - static public function isUserIsSuperUserOrTheUser( $theUser ) - { - try{ - self::checkUserIsSuperUserOrTheUser( $theUser ); - return true; - } catch( Exception $e){ - return false; - } - } - - /** - * Check that current user is either the specified user or the superuser - * - * @param string $theUser - * @throws Piwik_Access_NoAccessException if the user is neither the super user nor the user $theUser - */ - static public function checkUserIsSuperUserOrTheUser( $theUser ) - { - try{ - if( Piwik::getCurrentUserLogin() !== $theUser) - { - // or to the super user - Piwik::checkUserIsSuperUser(); - } - } catch( Piwik_Access_NoAccessException $e){ - throw new Piwik_Access_NoAccessException(Piwik_Translate('General_ExceptionCheckUserIsSuperUserOrTheUser', array($theUser))); - } - } - - /** - * Returns true if the current user is the Super User - * - * @return bool - */ - static public function isUserIsSuperUser() - { - try{ - self::checkUserIsSuperUser(); - return true; - } catch( Exception $e){ - return false; - } - } - - /** - * Is user the anonymous user? - * - * @return bool True if anonymouse; false otherwise - */ - static public function isUserIsAnonymous() - { - return Piwik::getCurrentUserLogin() == 'anonymous'; - } - - /** - * Checks if user is not the anonymous user. - * - * @throws Piwik_Access_NoAccessException if user is anonymous. - */ - static public function checkUserIsNotAnonymous() - { - if(self::isUserIsAnonymous()) - { - throw new Piwik_Access_NoAccessException(Piwik_Translate('General_YouMustBeLoggedIn')); - } - } - - /** - * Helper method user to set the current as Super User. - * This should be used with great care as this gives the user all permissions. - * - * @param bool $bool true to set current user as super user - */ - static public function setUserIsSuperUser( $bool = true ) - { - 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{ - self::checkUserHasAdminAccess( $idSites ); - return true; - } catch( Exception $e){ - 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{ - self::checkUserHasSomeAdminAccess(); - return true; - } catch( Exception $e){ - 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(); - } - - /** - * Returns true if the user has view access to the sites - * - * @param mixed $idSites - * @return bool - */ - static public function isUserHasViewAccess( $idSites ) - { - try{ - self::checkUserHasViewAccess( $idSites ); - return true; - } catch( Exception $e){ - return false; - } - } - - /** - * Check user has view access to the sites - * - * @param mixed $idSites - * @throws Exception if user doesn't have view access to sites - */ - static public function checkUserHasViewAccess( $idSites ) - { - Zend_Registry::get('access')->checkUserHasViewAccess( $idSites ); - } - - /** - * Returns true if the user has view access to any sites - * - * @return bool - */ - static public function isUserHasSomeViewAccess() - { - try{ - self::checkUserHasSomeViewAccess(); - return true; - } catch( Exception $e){ - return false; - } - } - - /** - * Check user has view access to any sites - * - * @throws Exception if user doesn't have view access to any sites - */ - static public function checkUserHasSomeViewAccess() - { - Zend_Registry::get('access')->checkUserHasSomeViewAccess(); - } - -/* + /** + * Get current user email address + * + * @return string + */ + static public function getCurrentUserEmail() + { + if (!Piwik::isUserIsSuperUser()) { + $user = Piwik_UsersManager_API::getInstance()->getUser(Piwik::getCurrentUserLogin()); + return $user['email']; + } + return self::getSuperUserEmail(); + } + + /** + * Returns Super User login + * + * @return string + */ + static public function getSuperUserLogin() + { + $superuser = Piwik_Config::getInstance()->superuser; + return $superuser['login']; + } + + /** + * Returns Super User email + * + * @return string + */ + static public function getSuperUserEmail() + { + $superuser = Piwik_Config::getInstance()->superuser; + return $superuser['email']; + } + + /** + * Get current user login + * + * @return string login ID + */ + static public function getCurrentUserLogin() + { + return Zend_Registry::get('access')->getLogin(); + } + + /** + * Get current user's token auth + * + * @return string Token auth + */ + static public function getCurrentUserTokenAuth() + { + 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 + */ + static public function isUserIsSuperUserOrTheUser($theUser) + { + try { + self::checkUserIsSuperUserOrTheUser($theUser); + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Check that current user is either the specified user or the superuser + * + * @param string $theUser + * @throws Piwik_Access_NoAccessException if the user is neither the super user nor the user $theUser + */ + static public function checkUserIsSuperUserOrTheUser($theUser) + { + try { + if (Piwik::getCurrentUserLogin() !== $theUser) { + // or to the super user + Piwik::checkUserIsSuperUser(); + } + } catch (Piwik_Access_NoAccessException $e) { + throw new Piwik_Access_NoAccessException(Piwik_Translate('General_ExceptionCheckUserIsSuperUserOrTheUser', array($theUser))); + } + } + + /** + * Returns true if the current user is the Super User + * + * @return bool + */ + static public function isUserIsSuperUser() + { + try { + self::checkUserIsSuperUser(); + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Is user the anonymous user? + * + * @return bool True if anonymouse; false otherwise + */ + static public function isUserIsAnonymous() + { + return Piwik::getCurrentUserLogin() == 'anonymous'; + } + + /** + * Checks if user is not the anonymous user. + * + * @throws Piwik_Access_NoAccessException if user is anonymous. + */ + static public function checkUserIsNotAnonymous() + { + if (self::isUserIsAnonymous()) { + throw new Piwik_Access_NoAccessException(Piwik_Translate('General_YouMustBeLoggedIn')); + } + } + + /** + * Helper method user to set the current as Super User. + * This should be used with great care as this gives the user all permissions. + * + * @param bool $bool true to set current user as super user + */ + static public function setUserIsSuperUser($bool = true) + { + 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 { + self::checkUserHasAdminAccess($idSites); + return true; + } catch (Exception $e) { + 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 { + self::checkUserHasSomeAdminAccess(); + return true; + } catch (Exception $e) { + 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(); + } + + /** + * Returns true if the user has view access to the sites + * + * @param mixed $idSites + * @return bool + */ + static public function isUserHasViewAccess($idSites) + { + try { + self::checkUserHasViewAccess($idSites); + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Check user has view access to the sites + * + * @param mixed $idSites + * @throws Exception if user doesn't have view access to sites + */ + static public function checkUserHasViewAccess($idSites) + { + Zend_Registry::get('access')->checkUserHasViewAccess($idSites); + } + + /** + * Returns true if the user has view access to any sites + * + * @return bool + */ + static public function isUserHasSomeViewAccess() + { + try { + self::checkUserHasSomeViewAccess(); + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Check user has view access to any sites + * + * @throws Exception if user doesn't have view access to any sites + */ + static public function checkUserHasSomeViewAccess() + { + Zend_Registry::get('access')->checkUserHasSomeViewAccess(); + } + + /* * Current module, action, plugin */ - /** - * 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 getLoginPluginName() - { - return Zend_Registry::get('auth')->getName(); - } - - /** - * Returns the plugin currently being used to display the page - * - * @return Piwik_Plugin - */ - static public function getCurrentPlugin() - { - return Piwik_PluginsManager::getInstance()->getLoadedPlugin(Piwik::getModule()); - } - - /** - * 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 - * - * @return string - */ - static public function getAction() - { - return Piwik_Common::getRequestVar('action', '', 'string'); - } - - /** - * Helper method used in API function to introduce array elements in API parameters. - * Array elements can be passed by comma separated values, or using the notation - * array[]=value1&array[]=value2 in the URL. - * This function will handle both cases and return the array. - * - * @param array|string $columns - * @return array - */ - static public function getArrayFromApiParameter($columns) - { - return $columns === false - ? array() - : (is_array($columns) - ? $columns - : explode(',', $columns) - ); - } - - /** - * Redirect to module (and action) - * - * @param string $newModule Target module - * @param string $newAction Target action - * @param array $parameters Parameters to modify in the URL - * @return bool false if the URL to redirect to is already this URL - */ - static public function redirectToModule( $newModule, $newAction = '', $parameters = array() ) - { - $newUrl = 'index.php' . Piwik_Url::getCurrentQueryStringWithParametersModified( - array('module' => $newModule, 'action' => $newAction) - + $parameters - ); - Piwik_Url::redirectToUrl($newUrl); - } - -/* + /** + * 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 getLoginPluginName() + { + return Zend_Registry::get('auth')->getName(); + } + + /** + * Returns the plugin currently being used to display the page + * + * @return Piwik_Plugin + */ + static public function getCurrentPlugin() + { + return Piwik_PluginsManager::getInstance()->getLoadedPlugin(Piwik::getModule()); + } + + /** + * 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 + * + * @return string + */ + static public function getAction() + { + return Piwik_Common::getRequestVar('action', '', 'string'); + } + + /** + * Helper method used in API function to introduce array elements in API parameters. + * Array elements can be passed by comma separated values, or using the notation + * array[]=value1&array[]=value2 in the URL. + * This function will handle both cases and return the array. + * + * @param array|string $columns + * @return array + */ + static public function getArrayFromApiParameter($columns) + { + return $columns === false + ? array() + : (is_array($columns) + ? $columns + : explode(',', $columns) + ); + } + + /** + * Redirect to module (and action) + * + * @param string $newModule Target module + * @param string $newAction Target action + * @param array $parameters Parameters to modify in the URL + * @return bool false if the URL to redirect to is already this URL + */ + static public function redirectToModule($newModule, $newAction = '', $parameters = array()) + { + $newUrl = 'index.php' . Piwik_Url::getCurrentQueryStringWithParametersModified( + array('module' => $newModule, 'action' => $newAction) + + $parameters + ); + Piwik_Url::redirectToUrl($newUrl); + } + + /* * Global database object */ - /** - * Create database object and connect to database - * @param array|null $dbInfos - */ - static public function createDatabaseObject( $dbInfos = null ) - { - $config = Piwik_Config::getInstance(); - - if(is_null($dbInfos)) - { - $dbInfos = $config->database; - } - - Piwik_PostEvent('Reporting.getDatabaseConfig', $dbInfos); - - $dbInfos['profiler'] = $config->Debug['enable_sql_profiler']; - - $db = null; - Piwik_PostEvent('Reporting.createDatabase', $db); - if(is_null($db)) - { - $adapter = $dbInfos['adapter']; - $db = @Piwik_Db_Adapter::factory($adapter, $dbInfos); - } - Zend_Registry::set('db', $db); - } - - /** - * Disconnect from database - */ - static public function disconnectDatabase() - { - Zend_Registry::get('db')->closeConnection(); - } - - /** - * Checks the database server version against the required minimum - * version. - * - * @see config/global.ini.php - * @since 0.4.4 - * @throws Exception if server version is less than the required version - */ - static public function checkDatabaseVersion() - { - Zend_Registry::get('db')->checkServerVersion(); - } - - /** - * Check database connection character set is utf8. - * - * @return bool True if it is (or doesn't matter); false otherwise - */ - static public function isDatabaseConnectionUTF8() - { - return Zend_Registry::get('db')->isConnectionUTF8(); - } - -/* + /** + * Create database object and connect to database + * @param array|null $dbInfos + */ + static public function createDatabaseObject($dbInfos = null) + { + $config = Piwik_Config::getInstance(); + + if (is_null($dbInfos)) { + $dbInfos = $config->database; + } + + Piwik_PostEvent('Reporting.getDatabaseConfig', $dbInfos); + + $dbInfos['profiler'] = $config->Debug['enable_sql_profiler']; + + $db = null; + Piwik_PostEvent('Reporting.createDatabase', $db); + if (is_null($db)) { + $adapter = $dbInfos['adapter']; + $db = @Piwik_Db_Adapter::factory($adapter, $dbInfos); + } + Zend_Registry::set('db', $db); + } + + /** + * Disconnect from database + */ + static public function disconnectDatabase() + { + Zend_Registry::get('db')->closeConnection(); + } + + /** + * Checks the database server version against the required minimum + * version. + * + * @see config/global.ini.php + * @since 0.4.4 + * @throws Exception if server version is less than the required version + */ + static public function checkDatabaseVersion() + { + Zend_Registry::get('db')->checkServerVersion(); + } + + /** + * Check database connection character set is utf8. + * + * @return bool True if it is (or doesn't matter); false otherwise + */ + static public function isDatabaseConnectionUTF8() + { + return Zend_Registry::get('db')->isConnectionUTF8(); + } + + /* * Global log object */ - /** - * Create log object - * @throws Exception - */ - static public function createLogObject() - { - $configAPI = Piwik_Config::getInstance()->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) - { - case 'screen': - $logger->addWriteToScreen(); - break; - - case 'database': - $logger->addWriteToDatabase(); - break; - - case 'file': - $logger->addWriteToFile(); - break; - - default: - throw new Exception("'$recordTo' is not a valid Log type. Valid logger types are: screen, database, file."); - break; - } - } - } - } - - foreach($aLoggers as $loggerType =>$logger) - { - if($logger->getWritersCount() == 0) - { - $logger->addWriteToNull(); - } - Zend_Registry::set($loggerType, $logger); - } - } - -/* + /** + * Create log object + * @throws Exception + */ + static public function createLogObject() + { + $configAPI = Piwik_Config::getInstance()->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) { + case 'screen': + $logger->addWriteToScreen(); + break; + + case 'database': + $logger->addWriteToDatabase(); + break; + + case 'file': + $logger->addWriteToFile(); + break; + + default: + throw new Exception("'$recordTo' is not a valid Log type. Valid logger types are: screen, database, file."); + break; + } + } + } + } + + foreach ($aLoggers as $loggerType => $logger) { + if ($logger->getWritersCount() == 0) { + $logger->addWriteToNull(); + } + Zend_Registry::set($loggerType, $logger); + } + } + + /* * Global config object */ - /** - * Create configuration object - */ - static public function createConfigObject() - { - // for backward compatibility - Zend_Registry::set('config', new Piwik_Config_Compat()); + /** + * Create configuration object + */ + static public function createConfigObject() + { + // for backward compatibility + Zend_Registry::set('config', new Piwik_Config_Compat()); - // instantiate the singleton - $config = Piwik_Config::getInstance(); - $config->init(); - } + // instantiate the singleton + $config = Piwik_Config::getInstance(); + $config->init(); + } -/* + /* * Global access object */ - /** - * Create access object - */ - static public function createAccessObject() - { - Zend_Registry::set('access', new Piwik_Access()); - } + /** + * Create access object + */ + static public function createAccessObject() + { + Zend_Registry::set('access', new Piwik_Access()); + } -/* + /* * User input validation */ - /** - * Returns true if the email is a valid email - * - * @param string $email - * @return bool - */ - static public function isValidEmailString( $email ) - { - return (preg_match('/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9_.-]+\.[a-zA-Z]{2,7}$/D', $email) > 0); - } - - /** - * 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 $userLogin - * @throws Exception - * @return bool - */ - static public function checkValidLoginString( $userLogin ) - { - if(!self::isChecksEnabled() - && !empty($userLogin)) - { - return; - } - $loginMinimumLength = 3; - $loginMaximumLength = 100; - $l = strlen($userLogin); - if(!($l >= $loginMinimumLength - && $l <= $loginMaximumLength - && (preg_match('/^[A-Za-z0-9_.@+-]*$/D', $userLogin) > 0)) - ) - { - throw new Exception(Piwik_TranslateException('UsersManager_ExceptionInvalidLoginFormat', array($loginMinimumLength, $loginMaximumLength))); - } - } - - /** - * Should Piwik check that the login & password have minimum length and valid characters? - * - * @return bool True if checks enabled; false otherwise - */ - static public function isChecksEnabled() - { - return Piwik_Config::getInstance()->General['disable_checks_usernames_attributes'] == 0; - } - - /** - * Is GD php extension (sparklines, graphs) available? - * - * @return bool - */ - static public function isGdExtensionEnabled() - { - static $gd = null; - if(is_null($gd)) - { - $extensions = @get_loaded_extensions(); - $gd = in_array('gd', $extensions) && function_exists('imageftbbox'); - } - return $gd; - } - -/* + /** + * Returns true if the email is a valid email + * + * @param string $email + * @return bool + */ + static public function isValidEmailString($email) + { + return (preg_match('/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9_.-]+\.[a-zA-Z]{2,7}$/D', $email) > 0); + } + + /** + * 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 $userLogin + * @throws Exception + * @return bool + */ + static public function checkValidLoginString($userLogin) + { + if (!self::isChecksEnabled() + && !empty($userLogin) + ) { + return; + } + $loginMinimumLength = 3; + $loginMaximumLength = 100; + $l = strlen($userLogin); + if (!($l >= $loginMinimumLength + && $l <= $loginMaximumLength + && (preg_match('/^[A-Za-z0-9_.@+-]*$/D', $userLogin) > 0)) + ) { + throw new Exception(Piwik_TranslateException('UsersManager_ExceptionInvalidLoginFormat', array($loginMinimumLength, $loginMaximumLength))); + } + } + + /** + * Should Piwik check that the login & password have minimum length and valid characters? + * + * @return bool True if checks enabled; false otherwise + */ + static public function isChecksEnabled() + { + return Piwik_Config::getInstance()->General['disable_checks_usernames_attributes'] == 0; + } + + /** + * Is GD php extension (sparklines, graphs) available? + * + * @return bool + */ + static public function isGdExtensionEnabled() + { + static $gd = null; + if (is_null($gd)) { + $extensions = @get_loaded_extensions(); + $gd = in_array('gd', $extensions) && function_exists('imageftbbox'); + } + return $gd; + } + + /* * Date / Timezone */ - /** - * Determine if this php version/build supports timezone manipulation - * (e.g., php >= 5.2, or compiled with EXPERIMENTAL_DATE_SUPPORT=1 for - * php < 5.2). - * - * @return bool True if timezones supported; false otherwise - */ - static public function isTimezoneSupportEnabled() - { - 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' ); - } - -/* + /** + * Determine if this php version/build supports timezone manipulation + * (e.g., php >= 5.2, or compiled with EXPERIMENTAL_DATE_SUPPORT=1 for + * php < 5.2). + * + * @return bool True if timezones supported; false otherwise + */ + static public function isTimezoneSupportEnabled() + { + 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'); + } + + /* * Database and table definition methods */ - /** - * Is the schema available? - * - * @return bool True if schema is available; false otherwise - */ - static public function isAvailable() - { - return Piwik_Db_Schema::getInstance()->isAvailable(); - } - - /** - * 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); - } - - /** - * Get the SQL to create Piwik tables - * - * @return array array of strings containing SQL - */ - static public function getTablesCreateSql() - { - return Piwik_Db_Schema::getInstance()->getTablesCreateSql(); - } - - /** - * Create database - * - * @param string|null $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() - { - Piwik_Db_Schema::getInstance()->truncateAllTables(); - } - - /** - * Drop specific tables - * - * @param array $doNotDelete Names of tables to not delete - */ - static public function dropTables( $doNotDelete = array() ) - { - Piwik_Db_Schema::getInstance()->dropTables($doNotDelete); - } - - /** - * 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 - * @return array Tables installed - */ - static public function getTablesInstalled($forceReload = true) - { - return Piwik_Db_Schema::getInstance()->getTablesInstalled($forceReload); - } - - /** - * Returns all table names archive_* - * - * @return array - */ - static public function getTablesArchivesInstalled() - { - $archiveTables = array(); - $tables = Piwik::getTablesInstalled(); - foreach($tables as $table) - { - if(strpos($table, 'archive_') !== false) - { - $archiveTables[] = $table; - } - } - return $archiveTables; - } - - /** - * Batch insert into table from CSV (or other delimited) file. - * - * @param string $tableName Name of table - * @param array $fields Field names - * @param string $filePath Path name of a file. - * @param array $fileSpec File specifications (delimiter, line terminator, etc) - * @param book $throwException Should re-throw any exception, used in system check - * @return bool True if successful; false otherwise - */ - static public function createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec) - { - // On Windows, MySQL expects forward slashes as directory separators - if (Piwik_Common::isWindows()) { - $filePath = str_replace('\\', '/', $filePath); - } - - $query = " + /** + * Is the schema available? + * + * @return bool True if schema is available; false otherwise + */ + static public function isAvailable() + { + return Piwik_Db_Schema::getInstance()->isAvailable(); + } + + /** + * 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); + } + + /** + * Get the SQL to create Piwik tables + * + * @return array array of strings containing SQL + */ + static public function getTablesCreateSql() + { + return Piwik_Db_Schema::getInstance()->getTablesCreateSql(); + } + + /** + * Create database + * + * @param string|null $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() + { + Piwik_Db_Schema::getInstance()->truncateAllTables(); + } + + /** + * Drop specific tables + * + * @param array $doNotDelete Names of tables to not delete + */ + static public function dropTables($doNotDelete = array()) + { + Piwik_Db_Schema::getInstance()->dropTables($doNotDelete); + } + + /** + * 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 + * @return array Tables installed + */ + static public function getTablesInstalled($forceReload = true) + { + return Piwik_Db_Schema::getInstance()->getTablesInstalled($forceReload); + } + + /** + * Returns all table names archive_* + * + * @return array + */ + static public function getTablesArchivesInstalled() + { + $archiveTables = array(); + $tables = Piwik::getTablesInstalled(); + foreach ($tables as $table) { + if (strpos($table, 'archive_') !== false) { + $archiveTables[] = $table; + } + } + return $archiveTables; + } + + /** + * Batch insert into table from CSV (or other delimited) file. + * + * @param string $tableName Name of table + * @param array $fields Field names + * @param string $filePath Path name of a file. + * @param array $fileSpec File specifications (delimiter, line terminator, etc) + * @param book $throwException Should re-throw any exception, used in system check + * @return bool True if successful; false otherwise + */ + static public function createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec) + { + // On Windows, MySQL expects forward slashes as directory separators + if (Piwik_Common::isWindows()) { + $filePath = str_replace('\\', '/', $filePath); + } + + $query = " '$filePath' REPLACE INTO TABLE - `".$tableName."`"; + `" . $tableName . "`"; - if(isset($fileSpec['charset'])) - { - $query .= ' CHARACTER SET '.$fileSpec['charset']; - } + if (isset($fileSpec['charset'])) { + $query .= ' CHARACTER SET ' . $fileSpec['charset']; + } - $fieldList = '('.join(',', $fields).')'; + $fieldList = '(' . join(',', $fields) . ')'; - $query .= " + $query .= " FIELDS TERMINATED BY - '".$fileSpec['delim']."' + '" . $fileSpec['delim'] . "' ENCLOSED BY - '".$fileSpec['quote']."' + '" . $fileSpec['quote'] . "' "; - if(isset($fileSpec['escape'])) - { - $query .= " ESCAPED BY '".$fileSpec['escape']."'"; - } - $query .= " + if (isset($fileSpec['escape'])) { + $query .= " ESCAPED BY '" . $fileSpec['escape'] . "'"; + } + $query .= " LINES TERMINATED BY - '".$fileSpec['eol']."' + '" . $fileSpec['eol'] . "' $fieldList "; - /* + /* * First attempt: assume web server and MySQL server are on the same machine; * this requires that the db user have the FILE privilege; however, since this is * a global privilege, it may not be granted due to security concerns */ - $keywords = array(''); + $keywords = array(''); - /* + /* * Second attempt: using the LOCAL keyword means the client reads the file and sends it to the server; * the LOCAL keyword may trigger a known PHP PDO_MYSQL bug when MySQL not built with --enable-local-infile * @see http://bugs.php.net/bug.php?id=54158 */ - $openBaseDir = ini_get('open_basedir'); - $safeMode = ini_get('safe_mode'); - if(empty($openBaseDir) && empty($safeMode)) - { - // php 5.x - LOAD DATA LOCAL INFILE is disabled if open_basedir restrictions or safe_mode enabled - $keywords[] = 'LOCAL '; - } - - $exceptions = array(); - foreach($keywords as $keyword) - { - try { - $queryStart = 'LOAD DATA '.$keyword.'INFILE '; - $sql = $queryStart.$query; - $result = @Piwik_Exec($sql); - if(empty($result) || $result < 0) - { - continue; - } - - return true; - } catch(Exception $e) { + $openBaseDir = ini_get('open_basedir'); + $safeMode = ini_get('safe_mode'); + if (empty($openBaseDir) && empty($safeMode)) { + // php 5.x - LOAD DATA LOCAL INFILE is disabled if open_basedir restrictions or safe_mode enabled + $keywords[] = 'LOCAL '; + } + + $exceptions = array(); + foreach ($keywords as $keyword) { + try { + $queryStart = 'LOAD DATA ' . $keyword . 'INFILE '; + $sql = $queryStart . $query; + $result = @Piwik_Exec($sql); + if (empty($result) || $result < 0) { + continue; + } + + return true; + } catch (Exception $e) { // echo $sql . ' ---- ' . $e->getMessage(); - $code = $e->getCode(); - $message = $e->getMessage() . ($code ? "[$code]" : ''); - if(!Zend_Registry::get('db')->isErrNo($e, '1148')) - { - Piwik::log(sprintf("LOAD DATA INFILE failed... Error was: %s", $message)); - } - $exceptions[] = "\n Try #" . (count($exceptions)+1) . ': ' . $queryStart .": ". $message; - } - } - if(count($exceptions)) - { - throw new Exception( implode( ",", $exceptions ) ); - } - return false; - } - - /** - * Performs a batch insert into a specific table using either LOAD DATA INFILE or plain INSERTs, - * as a fallback. On MySQL, LOAD DATA INFILE is 20x faster than a series of plain INSERTs. - * - * @param string $tableName PREFIXED table name! you must call Piwik_Common::prefixTable() before passing the table name - * @param array $fields array of unquoted field names - * @param array $values array of data to be inserted - * @param bool $throwException Whether to throw an exception that was caught while trying - * LOAD DATA INFILE, or not. - * @throws Exception - * @return bool True if the bulk LOAD was used, false if we fallback to plain INSERTs - */ - static public function tableInsertBatch($tableName, $fields, $values, $throwException = false) - { - $filePath = PIWIK_USER_PATH . '/' . Piwik_AssetManager::MERGED_FILE_DIR . $tableName . '-'.Piwik_Common::generateUniqId().'.csv'; - - if(Zend_Registry::get('db')->hasBulkLoader()) - { - try { + $code = $e->getCode(); + $message = $e->getMessage() . ($code ? "[$code]" : ''); + if (!Zend_Registry::get('db')->isErrNo($e, '1148')) { + Piwik::log(sprintf("LOAD DATA INFILE failed... Error was: %s", $message)); + } + $exceptions[] = "\n Try #" . (count($exceptions) + 1) . ': ' . $queryStart . ": " . $message; + } + } + if (count($exceptions)) { + throw new Exception(implode(",", $exceptions)); + } + return false; + } + + /** + * Performs a batch insert into a specific table using either LOAD DATA INFILE or plain INSERTs, + * as a fallback. On MySQL, LOAD DATA INFILE is 20x faster than a series of plain INSERTs. + * + * @param string $tableName PREFIXED table name! you must call Piwik_Common::prefixTable() before passing the table name + * @param array $fields array of unquoted field names + * @param array $values array of data to be inserted + * @param bool $throwException Whether to throw an exception that was caught while trying + * LOAD DATA INFILE, or not. + * @throws Exception + * @return bool True if the bulk LOAD was used, false if we fallback to plain INSERTs + */ + static public function tableInsertBatch($tableName, $fields, $values, $throwException = false) + { + $filePath = PIWIK_USER_PATH . '/' . Piwik_AssetManager::MERGED_FILE_DIR . $tableName . '-' . Piwik_Common::generateUniqId() . '.csv'; + + if (Zend_Registry::get('db')->hasBulkLoader()) { + try { // throw new Exception(''); - $fileSpec = array( - 'delim' => "\t", - 'quote' => '"', // chr(34) - 'escape' => '\\\\', // chr(92) - 'escapespecial_cb' => create_function('$str', 'return str_replace(array(chr(92), chr(34)), array(chr(92).chr(92), chr(92).chr(34)), $str);'), - 'eol' => "\r\n", - 'null' => 'NULL', - ); - - // hack for charset mismatch - if(!self::isDatabaseConnectionUTF8() && !isset(Piwik_Config::getInstance()->database['charset'])) - { - $fileSpec['charset'] = 'latin1'; - } - - self::createCSVFile($filePath, $fileSpec, $values); - - if(!is_readable($filePath)) - { - throw new Exception("File $filePath could not be read."); - } - - $rc = self::createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec); - if($rc) { - unlink($filePath); - return true; - } - } catch(Exception $e) { - Piwik::log(sprintf("LOAD DATA INFILE failed or not supported, falling back to normal INSERTs... Error was: %s", $e->getMessage())); - - if ($throwException) - { - throw $e; - } - } - } - - // if all else fails, fallback to a series of INSERTs - @unlink($filePath); - self::tableInsertBatchIterate($tableName, $fields, $values); - return false; - } - - /** - * Performs a batch insert into a specific table by iterating through the data - * - * NOTE: you should use tableInsertBatch() which will fallback to this function if LOAD DATA INFILE not available - * - * @param string $tableName PREFIXED table name! you must call Piwik_Common::prefixTable() before passing the table name - * @param array $fields array of unquoted field names - * @param array $values array of data to be inserted - * @param bool $ignoreWhenDuplicate Ignore new rows that contain unique key values that duplicate old rows - */ - static public function tableInsertBatchIterate($tableName, $fields, $values, $ignoreWhenDuplicate = true) - { - $fieldList = '('.join(',', $fields).')'; - $ignore = $ignoreWhenDuplicate ? 'IGNORE' : ''; - - foreach($values as $row) { - $query = "INSERT $ignore - INTO ".$tableName." + $fileSpec = array( + 'delim' => "\t", + 'quote' => '"', // chr(34) + 'escape' => '\\\\', // chr(92) + 'escapespecial_cb' => create_function('$str', 'return str_replace(array(chr(92), chr(34)), array(chr(92).chr(92), chr(92).chr(34)), $str);'), + 'eol' => "\r\n", + 'null' => 'NULL', + ); + + // hack for charset mismatch + if (!self::isDatabaseConnectionUTF8() && !isset(Piwik_Config::getInstance()->database['charset'])) { + $fileSpec['charset'] = 'latin1'; + } + + self::createCSVFile($filePath, $fileSpec, $values); + + if (!is_readable($filePath)) { + throw new Exception("File $filePath could not be read."); + } + + $rc = self::createTableFromCSVFile($tableName, $fields, $filePath, $fileSpec); + if ($rc) { + unlink($filePath); + return true; + } + } catch (Exception $e) { + Piwik::log(sprintf("LOAD DATA INFILE failed or not supported, falling back to normal INSERTs... Error was: %s", $e->getMessage())); + + if ($throwException) { + throw $e; + } + } + } + + // if all else fails, fallback to a series of INSERTs + @unlink($filePath); + self::tableInsertBatchIterate($tableName, $fields, $values); + return false; + } + + /** + * Performs a batch insert into a specific table by iterating through the data + * + * NOTE: you should use tableInsertBatch() which will fallback to this function if LOAD DATA INFILE not available + * + * @param string $tableName PREFIXED table name! you must call Piwik_Common::prefixTable() before passing the table name + * @param array $fields array of unquoted field names + * @param array $values array of data to be inserted + * @param bool $ignoreWhenDuplicate Ignore new rows that contain unique key values that duplicate old rows + */ + static public function tableInsertBatchIterate($tableName, $fields, $values, $ignoreWhenDuplicate = true) + { + $fieldList = '(' . join(',', $fields) . ')'; + $ignore = $ignoreWhenDuplicate ? 'IGNORE' : ''; + + foreach ($values as $row) { + $query = "INSERT $ignore + INTO " . $tableName . " $fieldList - VALUES (".Piwik_Common::getSqlStringFieldsArray($row).")"; - Piwik_Query($query, $row); - } - } - - /** - * Generate advisory lock name - * - * @param int $idsite - * @param Piwik_Period $period - * @param Piwik_Segment $segment - * @return string - */ - static public function getArchiveProcessingLockName($idsite, $period, Piwik_Segment $segment) - { - $config = Piwik_Config::getInstance(); - - $lockName = 'piwik.' - . $config->database['dbname'] . '.' - . $config->database['tables_prefix'] . '/' - . $idsite . '/' - . (!$segment->isEmpty() ? $segment->getHash().'/' : '' ) - . $period->getId() . '/' - . $period->getDateStart()->toString('Y-m-d') . ',' - . $period->getDateEnd()->toString('Y-m-d'); - return $lockName .'/'. md5($lockName . Piwik_Common::getSalt()); - } - - /** - * Get an advisory lock - * - * @param int $idsite - * @param Piwik_Period $period - * @param Piwik_Segment $segment - * @return bool True if lock acquired; false otherwise - */ - static public function getArchiveProcessingLock($idsite, $period, $segment) - { - $lockName = self::getArchiveProcessingLockName($idsite, $period, $segment); - return Piwik_GetDbLock($lockName, $maxRetries = 30); - } - - /** - * Release an advisory lock - * - * @param int $idsite - * @param Piwik_Period $period - * @param Piwik_Segment $segment - * @return bool True if lock released; false otherwise - */ - static public function releaseArchiveProcessingLock($idsite, $period, $segment) - { - $lockName = self::getArchiveProcessingLockName($idsite, $period, $segment); - return Piwik_ReleaseDbLock($lockName); - } - - /** - * Cached result of isLockprivilegeGranted function. - * - * Public so tests can simulate the situation where the lock tables privilege isn't granted. - * - * @var bool - */ - static public $lockPrivilegeGranted = null; - - /** - * Checks whether the database user is allowed to lock tables. - * - * @return bool - */ - static public function isLockPrivilegeGranted() - { - if (is_null(self::$lockPrivilegeGranted)) - { - try - { - Piwik_LockTables(Piwik_Common::prefixTable('log_visit')); - Piwik_UnlockAllTables(); - - self::$lockPrivilegeGranted = true; - } - catch (Exception $ex) - { - self::$lockPrivilegeGranted = false; - } - } - - return self::$lockPrivilegeGranted; - } - - /** - * Utility function that checks if an object type is in a set of types. - * - * @param mixed $o - * @param array $types List of class names that $o is expected to be one of. - * @throws Exception if $o is not an instance of the types contained in $types. - */ - static public function checkObjectTypeIs( $o, $types ) - { - foreach ($types as $type) - { - if ($o instanceof $type) - { - return; - } - } - - $oType = is_object($o) ? get_class($o) : gettype($o); - throw new Exception("Invalid variable type '$oType', expected one of following: ".implode(', ', $types)); - } - - /** - * Returns true if an array is an associative array, false if otherwise. - * - * This method determines if an array is associative by checking that the - * first element's key is 0, and that each successive element's key is - * one greater than the last. - * - * @param array $array - * @return bool - */ - static public function isAssociativeArray( $array ) - { - reset($array); - if (!is_numeric(key($array)) - || key($array) != 0) // first key must be 0 - { - return true; - } - - // check that each key is == next key - 1 w/o actually indexing the array - while (true) - { - $current = key($array); - - next($array); - $next = key($array); - - if ($next === null) - { - break; - } - else if ($current + 1 != $next) - { - return true; - } - } - - return false; - } - - /** - * Checks if the filesystem Piwik stores sessions in is NFS or not. This - * check is done in order to avoid using file based sessions on NFS system, - * since on such a filesystem file locking can make file based sessions - * incredibly slow. - * - * Note: In order to figure this out, we try to run the 'df' program. If - * the 'exec' or 'shell_exec' functions are not available, we can't do - * the check. - * - * @return bool True if on an NFS filesystem, false if otherwise or if we - * can't use shell_exec or exec. - */ - public static function checkIfFileSystemIsNFS() - { - $sessionsPath = Piwik_Session::getSessionsDirectory(); - - // this command will display details for the filesystem that holds the $sessionsPath - // path, but only if its type is NFS. if not NFS, df will return one or less lines - // and the return code 1. if NFS, it will return 0 and at least 2 lines of text. - $command = "df -T -t nfs \"$sessionsPath\" 2>&1"; - - if (function_exists('exec')) // use exec - { - $output = $returnCode = null; - @exec($command, $output, $returnCode); - - // check if filesystem is NFS - if ($returnCode == 0 - && count($output) > 1) - { - return true; - } - } - else if (function_exists('shell_exec')) // use shell_exec - { - $output = @shell_exec($command); - if ($output) - { - $output = explode("\n", $output); - if (count($output) > 1) // check if filesystem is NFS - { - return true; - } - } - } - - return false; // not NFS, or we can't run a program to find out - } + VALUES (" . Piwik_Common::getSqlStringFieldsArray($row) . ")"; + Piwik_Query($query, $row); + } + } + + /** + * Generate advisory lock name + * + * @param int $idsite + * @param Piwik_Period $period + * @param Piwik_Segment $segment + * @return string + */ + static public function getArchiveProcessingLockName($idsite, $period, Piwik_Segment $segment) + { + $config = Piwik_Config::getInstance(); + + $lockName = 'piwik.' + . $config->database['dbname'] . '.' + . $config->database['tables_prefix'] . '/' + . $idsite . '/' + . (!$segment->isEmpty() ? $segment->getHash() . '/' : '') + . $period->getId() . '/' + . $period->getDateStart()->toString('Y-m-d') . ',' + . $period->getDateEnd()->toString('Y-m-d'); + return $lockName . '/' . md5($lockName . Piwik_Common::getSalt()); + } + + /** + * Get an advisory lock + * + * @param int $idsite + * @param Piwik_Period $period + * @param Piwik_Segment $segment + * @return bool True if lock acquired; false otherwise + */ + static public function getArchiveProcessingLock($idsite, $period, $segment) + { + $lockName = self::getArchiveProcessingLockName($idsite, $period, $segment); + return Piwik_GetDbLock($lockName, $maxRetries = 30); + } + + /** + * Release an advisory lock + * + * @param int $idsite + * @param Piwik_Period $period + * @param Piwik_Segment $segment + * @return bool True if lock released; false otherwise + */ + static public function releaseArchiveProcessingLock($idsite, $period, $segment) + { + $lockName = self::getArchiveProcessingLockName($idsite, $period, $segment); + return Piwik_ReleaseDbLock($lockName); + } + + /** + * Cached result of isLockprivilegeGranted function. + * + * Public so tests can simulate the situation where the lock tables privilege isn't granted. + * + * @var bool + */ + static public $lockPrivilegeGranted = null; + + /** + * Checks whether the database user is allowed to lock tables. + * + * @return bool + */ + static public function isLockPrivilegeGranted() + { + if (is_null(self::$lockPrivilegeGranted)) { + try { + Piwik_LockTables(Piwik_Common::prefixTable('log_visit')); + Piwik_UnlockAllTables(); + + self::$lockPrivilegeGranted = true; + } catch (Exception $ex) { + self::$lockPrivilegeGranted = false; + } + } + + return self::$lockPrivilegeGranted; + } + + /** + * Utility function that checks if an object type is in a set of types. + * + * @param mixed $o + * @param array $types List of class names that $o is expected to be one of. + * @throws Exception if $o is not an instance of the types contained in $types. + */ + static public function checkObjectTypeIs($o, $types) + { + foreach ($types as $type) { + if ($o instanceof $type) { + return; + } + } + + $oType = is_object($o) ? get_class($o) : gettype($o); + throw new Exception("Invalid variable type '$oType', expected one of following: " . implode(', ', $types)); + } + + /** + * Returns true if an array is an associative array, false if otherwise. + * + * This method determines if an array is associative by checking that the + * first element's key is 0, and that each successive element's key is + * one greater than the last. + * + * @param array $array + * @return bool + */ + static public function isAssociativeArray($array) + { + reset($array); + if (!is_numeric(key($array)) + || key($array) != 0 + ) // first key must be 0 + { + return true; + } + + // check that each key is == next key - 1 w/o actually indexing the array + while (true) { + $current = key($array); + + next($array); + $next = key($array); + + if ($next === null) { + break; + } else if ($current + 1 != $next) { + return true; + } + } + + return false; + } + + /** + * Checks if the filesystem Piwik stores sessions in is NFS or not. This + * check is done in order to avoid using file based sessions on NFS system, + * since on such a filesystem file locking can make file based sessions + * incredibly slow. + * + * Note: In order to figure this out, we try to run the 'df' program. If + * the 'exec' or 'shell_exec' functions are not available, we can't do + * the check. + * + * @return bool True if on an NFS filesystem, false if otherwise or if we + * can't use shell_exec or exec. + */ + public static function checkIfFileSystemIsNFS() + { + $sessionsPath = Piwik_Session::getSessionsDirectory(); + + // this command will display details for the filesystem that holds the $sessionsPath + // path, but only if its type is NFS. if not NFS, df will return one or less lines + // and the return code 1. if NFS, it will return 0 and at least 2 lines of text. + $command = "df -T -t nfs \"$sessionsPath\" 2>&1"; + + if (function_exists('exec')) // use exec + { + $output = $returnCode = null; + @exec($command, $output, $returnCode); + + // check if filesystem is NFS + if ($returnCode == 0 + && count($output) > 1 + ) { + return true; + } + } else if (function_exists('shell_exec')) // use shell_exec + { + $output = @shell_exec($command); + if ($output) { + $output = explode("\n", $output); + if (count($output) > 1) // check if filesystem is NFS + { + return true; + } + } + } + + return false; // not NFS, or we can't run a program to find out + } } diff --git a/core/Plugin.php b/core/Plugin.php index 793423f26e..cfc6a0cdb4 100644 --- a/core/Plugin.php +++ b/core/Plugin.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,113 +12,113 @@ /** * Abstract class to define a Piwik_Plugin. * Any plugin has to at least implement the abstract methods of this class. - * + * * @package Piwik */ abstract class Piwik_Plugin { - /** - * Returns the plugin details - * - '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 - * - 'license' => string // plugin license - * - 'license_homepage' => string // license 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? - * - * @return array - */ - abstract public function getInformation(); + /** + * Returns the plugin details + * - '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 + * - 'license' => string // plugin license + * - 'license_homepage' => string // license 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? + * + * @return array + */ + abstract public function getInformation(); - /** - * Returns the list of hooks registered with the methods names - * - * @return array - */ - public function getListHooksRegistered() - { - return array(); - } + /** + * Returns the list of hooks registered with the methods names + * + * @return array + */ + public function getListHooksRegistered() + { + return array(); + } - /** - * Executed after loading plugin and registering translations - * Useful for code that uses translated strings from the plugin. - */ - public function postLoad() - { - return; - } - - /** - * Install the plugin - * - create tables - * - update existing tables - * - etc. - */ - public function install() - { - return; - } - - /** - * Remove the created resources during the install - */ - public function uninstall() - { - return; - } - - /** - * Executed every time the plugin is enabled - */ - public function activate() - { - return; - } - - /** - * Executed every time the plugin is disabled - */ - public function deactivate() - { - return; - } - - /** - * Returns the plugin version number - * - * @return string - */ - public function getVersion() - { - $info = $this->getInformation(); - return $info['version']; - } - - /** - * Returns the plugin's base class name without the "Piwik_" prefix, - * e.g., "UserCountry" when the plugin class is "Piwik_UserCountry" - * - * @return string - */ - final public function getPluginName() - { - return Piwik::unprefixClass(get_class($this)); - } + /** + * Executed after loading plugin and registering translations + * Useful for code that uses translated strings from the plugin. + */ + public function postLoad() + { + return; + } - /** - * Returns the plugin's base class name without the "Piwik_" prefix, - * e.g., "UserCountry" when the plugin class is "Piwik_UserCountry" - * - * @deprecated since 1.2 - for backward compatibility - * - * @return string - */ - final public function getClassName() - { - return $this->getPluginName(); - } + /** + * Install the plugin + * - create tables + * - update existing tables + * - etc. + */ + public function install() + { + return; + } + + /** + * Remove the created resources during the install + */ + public function uninstall() + { + return; + } + + /** + * Executed every time the plugin is enabled + */ + public function activate() + { + return; + } + + /** + * Executed every time the plugin is disabled + */ + public function deactivate() + { + return; + } + + /** + * Returns the plugin version number + * + * @return string + */ + public function getVersion() + { + $info = $this->getInformation(); + return $info['version']; + } + + /** + * Returns the plugin's base class name without the "Piwik_" prefix, + * e.g., "UserCountry" when the plugin class is "Piwik_UserCountry" + * + * @return string + */ + final public function getPluginName() + { + return Piwik::unprefixClass(get_class($this)); + } + + /** + * Returns the plugin's base class name without the "Piwik_" prefix, + * e.g., "UserCountry" when the plugin class is "Piwik_UserCountry" + * + * @deprecated since 1.2 - for backward compatibility + * + * @return string + */ + final public function getClassName() + { + return $this->getPluginName(); + } } diff --git a/core/Plugin/Config.php b/core/Plugin/Config.php index 81df393b60..2b41f23d5a 100644 --- a/core/Plugin/Config.php +++ b/core/Plugin/Config.php @@ -1,55 +1,55 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Read / write local plugin-specific configuration - * + * * @package Piwik */ class Piwik_Plugin_Config { - private $pluginName; - private $configFileName; + private $pluginName; + private $configFileName; - /** - * Constructor - * - * @param string $pluginName name of the plugin - * @param string $configFileName name of the plugin file; defaults to local.config.php - */ - public function __construct($pluginName, $configFileName = 'local.config.php') - { - $this->pluginName = $pluginName; - $this->configFileName = $configFileName; - } + /** + * Constructor + * + * @param string $pluginName name of the plugin + * @param string $configFileName name of the plugin file; defaults to local.config.php + */ + public function __construct($pluginName, $configFileName = 'local.config.php') + { + $this->pluginName = $pluginName; + $this->configFileName = $configFileName; + } - /** - * Load local plugin configuration - * - * @return array - */ - public function load() - { - $pluginConfig = @include(PIWIK_USER_PATH . '/plugins/' . $this->pluginName . '/config/' . $this->configFileName); + /** + * Load local plugin configuration + * + * @return array + */ + public function load() + { + $pluginConfig = @include(PIWIK_USER_PATH . '/plugins/' . $this->pluginName . '/config/' . $this->configFileName); - return $pluginConfig; - } + return $pluginConfig; + } - /** - * Store local plugin configuration - * - * @param array $pluginConfig - */ - public function store($pluginConfig) - { - file_put_contents(PIWIK_USER_PATH . '/plugins/' . $this->pluginName . '/config/' . $this->configFileName, "<?php\nreturn ".var_export($pluginConfig, true).";\n"); - } + /** + * Store local plugin configuration + * + * @param array $pluginConfig + */ + public function store($pluginConfig) + { + file_put_contents(PIWIK_USER_PATH . '/plugins/' . $this->pluginName . '/config/' . $this->configFileName, "<?php\nreturn " . var_export($pluginConfig, true) . ";\n"); + } } diff --git a/core/PluginsFunctions/Sql.php b/core/PluginsFunctions/Sql.php index 886325f086..5b2f2ee45a 100644 --- a/core/PluginsFunctions/Sql.php +++ b/core/PluginsFunctions/Sql.php @@ -16,375 +16,375 @@ */ class Piwik_Sql { - /** - * Returns the database adapter to use - * - * @return Piwik_Tracker_Db|Piwik_Db_Adapter_Interface - */ - 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; - } - - /** - * 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 $sql SQL Query - * @return integer|Zend_Db_Statement - */ - static public function exec($sql) - { - $db = Zend_Registry::get('db'); - $profiler = $db->getProfiler(); - $q = $profiler->queryStart($sql, Zend_Db_Profiler::INSERT); - $return = self::getDb()->exec($sql); - $profiler->queryEnd($q); - return $return; - } - - /** - * Executes a SQL query on the DB and returns the Zend_Db_Statement object - * If you want to fetch data from the DB you should use the function Piwik_FetchAll() - * - * See also http://framework.zend.com/manual/en/zend.db.statement.html - * - * @param string $sql SQL Query - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) - * @return Zend_Db_Statement - */ - static public function query($sql, $parameters = array()) - { - return self::getDb()->query($sql, $parameters); - } - - /** - * Executes the SQL Query and fetches all the rows from the database query - * - * @param string $sql SQL Query - * @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) - */ - static public function fetchAll($sql, $parameters = array()) - { - return self::getDb()->fetchAll($sql, $parameters); - } - - /** - * Fetches first row of result from the database query - * - * @param string $sql SQL Query - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) - * @return array - */ - static public function fetchRow($sql, $parameters = array()) - { - return self::getDb()->fetchRow($sql, $parameters); - } - - /** - * Fetches first column of first row of result from the database query - * - * @param string $sql SQL Query - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) - * @return string - */ - static public function fetchOne($sql, $parameters = array()) - { - return self::getDb()->fetchOne($sql, $parameters); - } - - /** - * Fetches result from the database query as an array of associative arrays. - * - * @param string $sql SQL query - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) - * @return array - */ - static public function fetchAssoc($sql, $parameters = array()) - { - return self::getDb()->fetchAssoc($sql, $parameters); - } - - /** - * Deletes all desired rows in a table, while using a limit. This function will execute a - * DELETE query until there are no more rows to delete. - * - * @param string $table The name of the table to delete from. Must be prefixed. - * @param string $where The where clause of the query. Must include the WHERE keyword. - * @param int $maxRowsPerQuery The maximum number of rows to delete per DELETE query. - * @param array $parameters Parameters to bind in the query. - * @return int The total number of rows deleted. - */ - static public function deleteAllRows($table, $where, $maxRowsPerQuery = 100000, $parameters = array()) - { - $sql = "DELETE FROM $table $where LIMIT " . (int)$maxRowsPerQuery; - - // delete rows w/ a limit - $totalRowsDeleted = 0; - do { - $rowsDeleted = self::query($sql, $parameters)->rowCount(); - - $totalRowsDeleted += $rowsDeleted; - } while ($rowsDeleted >= $maxRowsPerQuery); - - return $totalRowsDeleted; - } - - /** - * Runs an OPTIMIZE TABLE query on the supplied table or tables. The table names must be prefixed. - * - * @param string|array $tables The name of the table to optimize or an array of tables to optimize. - * @return Zend_Db_Statement - */ - static public function optimizeTables($tables) - { - $optimize = Piwik_Config::getInstance()->General['enable_sql_optimize_queries']; - if (empty($optimize)) { - return; - } - - if (empty($tables)) { - return false; - } - if (!is_array($tables)) { - $tables = array($tables); - } - - // filter out all InnoDB tables - $nonInnoDbTables = array(); - foreach (Piwik_FetchAll("SHOW TABLE STATUS") as $row) { - if (strtolower($row['Engine']) != 'innodb' - && in_array($row['Name'], $tables) - ) { - $nonInnoDbTables[] = $row['Name']; - } - } - - if (empty($nonInnoDbTables)) { - return false; - } - - // optimize the tables - return self::query("OPTIMIZE TABLE " . implode(',', $nonInnoDbTables)); - } - - /** - * Drops the supplied table or tables. The table names must be prefixed. - * - * @param string|array $tables The name of the table to drop or an array of table names to drop. - * @return Zend_Db_Statement - */ - static public function dropTables($tables) - { - if (!is_array($tables)) { - $tables = array($tables); - } - - return self::query("DROP TABLE " . implode(',', $tables)); - } - - /** - * Locks the supplied table or tables. The table names must be prefixed. - * - * @param string|array $tablesToRead The table or tables to obtain 'read' locks on. - * @param string|array $tablesToWrite The table or tables to obtain 'write' locks on. - * @return Zend_Db_Statement - */ - static public function lockTables($tablesToRead, $tablesToWrite = array()) - { - if (!is_array($tablesToRead)) { - $tablesToRead = array($tablesToRead); - } - if (!is_array($tablesToWrite)) { - $tablesToWrite = array($tablesToWrite); - } - - $lockExprs = array(); - foreach ($tablesToWrite as $table) { - $lockExprs[] = $table . " WRITE"; - } - foreach ($tablesToRead as $table) { - $lockExprs[] = $table . " READ"; - } - - return self::exec("LOCK TABLES " . implode(', ', $lockExprs)); - } - - /** - * Releases all table locks. - * - * @return Zend_Db_Statement - */ - static public function unlockAllTables() - { - return self::exec("UNLOCK TABLES"); - } - - /** - * Performs a SELECT on a table one chunk at a time and returns the first - * fetched value. - * - * @param string $sql The SQL to perform. The last two conditions of the WHERE - * expression must be as follows: 'id >= ? AND id < ?' where - * 'id' is the int id of the table. If $step < 0, the condition - * should be 'id <= ? AND id > ?'. - * @param int $first The minimum ID to loop from. - * @param int $last The maximum ID to loop to. - * @param int $step The maximum number of rows to scan in each smaller SELECT. - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) - * @return array - */ - static public function segmentedFetchFirst($sql, $first, $last, $step, $params) - { - $result = false; - if ($step > 0) { - for ($i = $first; $result === false && $i <= $last; $i += $step) { - $result = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); - } - } else { - for ($i = $first; $result === false && $i >= $last; $i += $step) { - $result = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); - } - } - return $result; - } - - /** - * Performs a SELECT on a table one chunk at a time and returns an array - * of every fetched value. - * - * @param string $sql The SQL to perform. The last two conditions of the WHERE - * expression must be as follows: 'id >= ? AND id < ?' where - * 'id' is the int id of the table. - * @param int $first The minimum ID to loop from. - * @param int $last The maximum ID to loop to. - * @param int $step The maximum number of rows to scan in each smaller SELECT. - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) - * @return array - */ - static public function segmentedFetchOne($sql, $first, $last, $step, $params) - { - $result = array(); - if ($step > 0) { - for ($i = $first; $i <= $last; $i += $step) { - $result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); - } - } else { - for ($i = $first; $i >= $last; $i += $step) { - $result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); - } - } - return $result; - } - - /** - * Performs a SELECT on a table one chunk at a time and returns an array - * of every fetched row. - * - * @param string $sql The SQL to perform. The last two conditions of the WHERE - * expression must be as follows: 'id >= ? AND id < ?' where - * 'id' is the int id of the table. - * @param int $first The minimum ID to loop from. - * @param int $last The maximum ID to loop to. - * @param int $step The maximum number of rows to scan in each smaller SELECT. - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) - * @return array - */ - static public function segmentedFetchAll($sql, $first, $last, $step, $params) - { - $result = array(); - if ($step > 0) { - for ($i = $first; $i <= $last; $i += $step) { - $currentParams = array_merge($params, array($i, $i + $step)); - $result = array_merge($result, self::fetchAll($sql, $currentParams)); - } - } else { - for ($i = $first; $i >= $last; $i += $step) { - $currentParams = array_merge($params, array($i, $i + $step)); - $result = array_merge($result, self::fetchAll($sql, $currentParams)); - } - } - return $result; - } - - /** - * Performs a non-SELECT query on a table one chunk at a time. - * - * @param string $sql The SQL to perform. The last two conditions of the WHERE - * expression must be as follows: 'id >= ? AND id < ?' where - * 'id' is the int id of the table. - * @param int $first The minimum ID to loop from. - * @param int $last The maximum ID to loop to. - * @param int $step The maximum number of rows to scan in each smaller query. - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) - * @return array - */ - static public function segmentedQuery($sql, $first, $last, $step, $params) - { - if ($step > 0) { - for ($i = $first; $i <= $last; $i += $step) { - $currentParams = array_merge($params, array($i, $i + $step)); - self::query($sql, $currentParams); - } - } else { - for ($i = $first; $i >= $last; $i += $step) { - $currentParams = array_merge($params, array($i, $i + $step)); - self::query($sql, $currentParams); - } - } - } - - /** - * Attempts to get a named lock. This function uses a timeout of 1s, but will - * retry a set number of time. - * - * @param string $lockName The lock name. - * @param int $maxRetries The max number of times to retry. - * @return bool true if the lock was obtained, false if otherwise. - */ - static public function getDbLock($lockName, $maxRetries = 30) - { - /* - * the server (e.g., shared hosting) may have a low wait timeout - * so instead of a single GET_LOCK() with a 30 second timeout, - * we use a 1 second timeout and loop, to avoid losing our MySQL - * connection - */ - $sql = 'SELECT GET_LOCK(?, 1)'; - - $db = Zend_Registry::get('db'); - - while ($maxRetries > 0) { - if ($db->fetchOne($sql, array($lockName)) == '1') { - return true; - } - $maxRetries--; - } - return false; - } - - /** - * Releases a named lock. - * - * @param string $lockName The lock name. - * @return bool true if the lock was released, false if otherwise. - */ - static public function releaseDbLock($lockName) - { - $sql = 'SELECT RELEASE_LOCK(?)'; - - $db = Zend_Registry::get('db'); - return $db->fetchOne($sql, array($lockName)) == '1'; - } + /** + * Returns the database adapter to use + * + * @return Piwik_Tracker_Db|Piwik_Db_Adapter_Interface + */ + 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; + } + + /** + * 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 $sql SQL Query + * @return integer|Zend_Db_Statement + */ + static public function exec($sql) + { + $db = Zend_Registry::get('db'); + $profiler = $db->getProfiler(); + $q = $profiler->queryStart($sql, Zend_Db_Profiler::INSERT); + $return = self::getDb()->exec($sql); + $profiler->queryEnd($q); + return $return; + } + + /** + * Executes a SQL query on the DB and returns the Zend_Db_Statement object + * If you want to fetch data from the DB you should use the function Piwik_FetchAll() + * + * See also http://framework.zend.com/manual/en/zend.db.statement.html + * + * @param string $sql SQL Query + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return Zend_Db_Statement + */ + static public function query($sql, $parameters = array()) + { + return self::getDb()->query($sql, $parameters); + } + + /** + * Executes the SQL Query and fetches all the rows from the database query + * + * @param string $sql SQL Query + * @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) + */ + static public function fetchAll($sql, $parameters = array()) + { + return self::getDb()->fetchAll($sql, $parameters); + } + + /** + * Fetches first row of result from the database query + * + * @param string $sql SQL Query + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return array + */ + static public function fetchRow($sql, $parameters = array()) + { + return self::getDb()->fetchRow($sql, $parameters); + } + + /** + * Fetches first column of first row of result from the database query + * + * @param string $sql SQL Query + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return string + */ + static public function fetchOne($sql, $parameters = array()) + { + return self::getDb()->fetchOne($sql, $parameters); + } + + /** + * Fetches result from the database query as an array of associative arrays. + * + * @param string $sql SQL query + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return array + */ + static public function fetchAssoc($sql, $parameters = array()) + { + return self::getDb()->fetchAssoc($sql, $parameters); + } + + /** + * Deletes all desired rows in a table, while using a limit. This function will execute a + * DELETE query until there are no more rows to delete. + * + * @param string $table The name of the table to delete from. Must be prefixed. + * @param string $where The where clause of the query. Must include the WHERE keyword. + * @param int $maxRowsPerQuery The maximum number of rows to delete per DELETE query. + * @param array $parameters Parameters to bind in the query. + * @return int The total number of rows deleted. + */ + static public function deleteAllRows($table, $where, $maxRowsPerQuery = 100000, $parameters = array()) + { + $sql = "DELETE FROM $table $where LIMIT " . (int)$maxRowsPerQuery; + + // delete rows w/ a limit + $totalRowsDeleted = 0; + do { + $rowsDeleted = self::query($sql, $parameters)->rowCount(); + + $totalRowsDeleted += $rowsDeleted; + } while ($rowsDeleted >= $maxRowsPerQuery); + + return $totalRowsDeleted; + } + + /** + * Runs an OPTIMIZE TABLE query on the supplied table or tables. The table names must be prefixed. + * + * @param string|array $tables The name of the table to optimize or an array of tables to optimize. + * @return Zend_Db_Statement + */ + static public function optimizeTables($tables) + { + $optimize = Piwik_Config::getInstance()->General['enable_sql_optimize_queries']; + if (empty($optimize)) { + return; + } + + if (empty($tables)) { + return false; + } + if (!is_array($tables)) { + $tables = array($tables); + } + + // filter out all InnoDB tables + $nonInnoDbTables = array(); + foreach (Piwik_FetchAll("SHOW TABLE STATUS") as $row) { + if (strtolower($row['Engine']) != 'innodb' + && in_array($row['Name'], $tables) + ) { + $nonInnoDbTables[] = $row['Name']; + } + } + + if (empty($nonInnoDbTables)) { + return false; + } + + // optimize the tables + return self::query("OPTIMIZE TABLE " . implode(',', $nonInnoDbTables)); + } + + /** + * Drops the supplied table or tables. The table names must be prefixed. + * + * @param string|array $tables The name of the table to drop or an array of table names to drop. + * @return Zend_Db_Statement + */ + static public function dropTables($tables) + { + if (!is_array($tables)) { + $tables = array($tables); + } + + return self::query("DROP TABLE " . implode(',', $tables)); + } + + /** + * Locks the supplied table or tables. The table names must be prefixed. + * + * @param string|array $tablesToRead The table or tables to obtain 'read' locks on. + * @param string|array $tablesToWrite The table or tables to obtain 'write' locks on. + * @return Zend_Db_Statement + */ + static public function lockTables($tablesToRead, $tablesToWrite = array()) + { + if (!is_array($tablesToRead)) { + $tablesToRead = array($tablesToRead); + } + if (!is_array($tablesToWrite)) { + $tablesToWrite = array($tablesToWrite); + } + + $lockExprs = array(); + foreach ($tablesToWrite as $table) { + $lockExprs[] = $table . " WRITE"; + } + foreach ($tablesToRead as $table) { + $lockExprs[] = $table . " READ"; + } + + return self::exec("LOCK TABLES " . implode(', ', $lockExprs)); + } + + /** + * Releases all table locks. + * + * @return Zend_Db_Statement + */ + static public function unlockAllTables() + { + return self::exec("UNLOCK TABLES"); + } + + /** + * Performs a SELECT on a table one chunk at a time and returns the first + * fetched value. + * + * @param string $sql The SQL to perform. The last two conditions of the WHERE + * expression must be as follows: 'id >= ? AND id < ?' where + * 'id' is the int id of the table. If $step < 0, the condition + * should be 'id <= ? AND id > ?'. + * @param int $first The minimum ID to loop from. + * @param int $last The maximum ID to loop to. + * @param int $step The maximum number of rows to scan in each smaller SELECT. + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return array + */ + static public function segmentedFetchFirst($sql, $first, $last, $step, $params) + { + $result = false; + if ($step > 0) { + for ($i = $first; $result === false && $i <= $last; $i += $step) { + $result = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); + } + } else { + for ($i = $first; $result === false && $i >= $last; $i += $step) { + $result = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); + } + } + return $result; + } + + /** + * Performs a SELECT on a table one chunk at a time and returns an array + * of every fetched value. + * + * @param string $sql The SQL to perform. The last two conditions of the WHERE + * expression must be as follows: 'id >= ? AND id < ?' where + * 'id' is the int id of the table. + * @param int $first The minimum ID to loop from. + * @param int $last The maximum ID to loop to. + * @param int $step The maximum number of rows to scan in each smaller SELECT. + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return array + */ + static public function segmentedFetchOne($sql, $first, $last, $step, $params) + { + $result = array(); + if ($step > 0) { + for ($i = $first; $i <= $last; $i += $step) { + $result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); + } + } else { + for ($i = $first; $i >= $last; $i += $step) { + $result[] = self::fetchOne($sql, array_merge($params, array($i, $i + $step))); + } + } + return $result; + } + + /** + * Performs a SELECT on a table one chunk at a time and returns an array + * of every fetched row. + * + * @param string $sql The SQL to perform. The last two conditions of the WHERE + * expression must be as follows: 'id >= ? AND id < ?' where + * 'id' is the int id of the table. + * @param int $first The minimum ID to loop from. + * @param int $last The maximum ID to loop to. + * @param int $step The maximum number of rows to scan in each smaller SELECT. + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return array + */ + static public function segmentedFetchAll($sql, $first, $last, $step, $params) + { + $result = array(); + if ($step > 0) { + for ($i = $first; $i <= $last; $i += $step) { + $currentParams = array_merge($params, array($i, $i + $step)); + $result = array_merge($result, self::fetchAll($sql, $currentParams)); + } + } else { + for ($i = $first; $i >= $last; $i += $step) { + $currentParams = array_merge($params, array($i, $i + $step)); + $result = array_merge($result, self::fetchAll($sql, $currentParams)); + } + } + return $result; + } + + /** + * Performs a non-SELECT query on a table one chunk at a time. + * + * @param string $sql The SQL to perform. The last two conditions of the WHERE + * expression must be as follows: 'id >= ? AND id < ?' where + * 'id' is the int id of the table. + * @param int $first The minimum ID to loop from. + * @param int $last The maximum ID to loop to. + * @param int $step The maximum number of rows to scan in each smaller query. + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @return array + */ + static public function segmentedQuery($sql, $first, $last, $step, $params) + { + if ($step > 0) { + for ($i = $first; $i <= $last; $i += $step) { + $currentParams = array_merge($params, array($i, $i + $step)); + self::query($sql, $currentParams); + } + } else { + for ($i = $first; $i >= $last; $i += $step) { + $currentParams = array_merge($params, array($i, $i + $step)); + self::query($sql, $currentParams); + } + } + } + + /** + * Attempts to get a named lock. This function uses a timeout of 1s, but will + * retry a set number of time. + * + * @param string $lockName The lock name. + * @param int $maxRetries The max number of times to retry. + * @return bool true if the lock was obtained, false if otherwise. + */ + static public function getDbLock($lockName, $maxRetries = 30) + { + /* + * the server (e.g., shared hosting) may have a low wait timeout + * so instead of a single GET_LOCK() with a 30 second timeout, + * we use a 1 second timeout and loop, to avoid losing our MySQL + * connection + */ + $sql = 'SELECT GET_LOCK(?, 1)'; + + $db = Zend_Registry::get('db'); + + while ($maxRetries > 0) { + if ($db->fetchOne($sql, array($lockName)) == '1') { + return true; + } + $maxRetries--; + } + return false; + } + + /** + * Releases a named lock. + * + * @param string $lockName The lock name. + * @return bool true if the lock was released, false if otherwise. + */ + static public function releaseDbLock($lockName) + { + $sql = 'SELECT RELEASE_LOCK(?)'; + + $db = Zend_Registry::get('db'); + return $db->fetchOne($sql, array($lockName)) == '1'; + } } /** @@ -394,12 +394,12 @@ class Piwik_Sql * * @see Piwik_Sql::exec * - * @param string $sqlQuery SQL Query + * @param string $sqlQuery SQL Query * @return integer|Zend_Db_Statement */ function Piwik_Exec($sqlQuery) { - return Piwik_Sql::exec($sqlQuery); + return Piwik_Sql::exec($sqlQuery); } /** @@ -410,13 +410,13 @@ function Piwik_Exec($sqlQuery) * * @see Piwik_Sql::query * - * @param string $sqlQuery SQL Query - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @param string $sqlQuery SQL Query + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) * @return Zend_Db_Statement */ function Piwik_Query($sqlQuery, $parameters = array()) { - return Piwik_Sql::query($sqlQuery, $parameters); + return Piwik_Sql::query($sqlQuery, $parameters); } /** @@ -424,13 +424,13 @@ function Piwik_Query($sqlQuery, $parameters = array()) * * @see Piwik_Sql::fetchAll * - * @param string $sqlQuery SQL Query - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @param string $sqlQuery SQL Query + * @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 Piwik_Sql::fetchAll($sqlQuery, $parameters); + return Piwik_Sql::fetchAll($sqlQuery, $parameters); } /** @@ -438,13 +438,13 @@ function Piwik_FetchAll($sqlQuery, $parameters = array()) * * @see Piwik_Sql::fetchRow * - * @param string $sqlQuery SQL Query - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @param string $sqlQuery SQL Query + * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) * @return array */ function Piwik_FetchRow($sqlQuery, $parameters = array()) { - return Piwik_Sql::fetchRow($sqlQuery, $parameters); + return Piwik_Sql::fetchRow($sqlQuery, $parameters); } /** @@ -452,13 +452,13 @@ function Piwik_FetchRow($sqlQuery, $parameters = array()) * * @see Piwik_Sql::fetchOne * - * @param string $sqlQuery SQL Query - * @param array $parameters Parameters to bind in the query, array( param1 => value1, param2 => value2) + * @param string $sqlQuery SQL Query + * @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); + return Piwik_Sql::fetchOne($sqlQuery, $parameters); } /** @@ -470,7 +470,7 @@ function Piwik_FetchOne($sqlQuery, $parameters = array()) */ function Piwik_FetchAssoc($sqlQuery, $parameters = array()) { - return Piwik_Sql::fetchAssoc($sqlQuery, $parameters); + return Piwik_Sql::fetchAssoc($sqlQuery, $parameters); } /** @@ -479,15 +479,15 @@ function Piwik_FetchAssoc($sqlQuery, $parameters = array()) * * @see Piwik_Sql::deleteAllRows * - * @param string $table The name of the table to delete from. Must be prefixed. - * @param string $where The where clause of the query. Must include the WHERE keyword. - * @param int $maxRowsPerQuery The maximum number of rows to delete per DELETE query. - * @param array $parameters Parameters to bind in the query. + * @param string $table The name of the table to delete from. Must be prefixed. + * @param string $where The where clause of the query. Must include the WHERE keyword. + * @param int $maxRowsPerQuery The maximum number of rows to delete per DELETE query. + * @param array $parameters Parameters to bind in the query. * @return int The total number of rows deleted. */ function Piwik_DeleteAllRows($table, $where, $maxRowsPerQuery, $parameters = array()) { - return Piwik_Sql::deleteAllRows($table, $where, $maxRowsPerQuery, $parameters); + return Piwik_Sql::deleteAllRows($table, $where, $maxRowsPerQuery, $parameters); } /** @@ -495,12 +495,12 @@ function Piwik_DeleteAllRows($table, $where, $maxRowsPerQuery, $parameters = arr * * @see Piwik_Sql::optimizeTables * - * @param string|array $tables The name of the table to optimize or an array of tables to optimize. + * @param string|array $tables The name of the table to optimize or an array of tables to optimize. * @return Zend_Db_Statement */ function Piwik_OptimizeTables($tables) { - return Piwik_Sql::optimizeTables($tables); + return Piwik_Sql::optimizeTables($tables); } /** @@ -508,12 +508,12 @@ function Piwik_OptimizeTables($tables) * * @see Piwik_Sql::dropTables * - * @param string|array $tables The name of the table to drop or an array of table names to drop. + * @param string|array $tables The name of the table to drop or an array of table names to drop. * @return Zend_Db_Statement */ function Piwik_DropTables($tables) { - return Piwik_Sql::dropTables($tables); + return Piwik_Sql::dropTables($tables); } /** @@ -521,13 +521,13 @@ function Piwik_DropTables($tables) * * @see Piwik_Sql::lockTables * - * @param string|array $tablesToRead The table or tables to obtain 'read' locks on. - * @param string|array $tablesToWrite The table or tables to obtain 'write' locks on. + * @param string|array $tablesToRead The table or tables to obtain 'read' locks on. + * @param string|array $tablesToWrite The table or tables to obtain 'write' locks on. * @return Zend_Db_Statement */ function Piwik_LockTables($tablesToRead, $tablesToWrite = array()) { - return Piwik_Sql::lockTables($tablesToRead, $tablesToWrite); + return Piwik_Sql::lockTables($tablesToRead, $tablesToWrite); } /** @@ -539,7 +539,7 @@ function Piwik_LockTables($tablesToRead, $tablesToWrite = array()) */ function Piwik_UnlockAllTables() { - return Piwik_Sql::unlockAllTables(); + return Piwik_Sql::unlockAllTables(); } /** @@ -564,7 +564,7 @@ function Piwik_UnlockAllTables() */ function Piwik_SegmentedFetchFirst($sql, $first, $last, $step, $params = array()) { - return Piwik_Sql::segmentedFetchFirst($sql, $first, $last, $step, $params); + return Piwik_Sql::segmentedFetchFirst($sql, $first, $last, $step, $params); } /** @@ -589,7 +589,7 @@ function Piwik_SegmentedFetchFirst($sql, $first, $last, $step, $params = array() */ function Piwik_SegmentedFetchOne($sql, $first, $last, $step, $params = array()) { - return Piwik_Sql::segmentedFetchOne($sql, $first, $last, $step, $params); + return Piwik_Sql::segmentedFetchOne($sql, $first, $last, $step, $params); } /** @@ -614,7 +614,7 @@ function Piwik_SegmentedFetchOne($sql, $first, $last, $step, $params = array()) */ function Piwik_SegmentedFetchAll($sql, $first, $last, $step, $params = array()) { - return Piwik_Sql::segmentedFetchAll($sql, $first, $last, $step, $params); + return Piwik_Sql::segmentedFetchAll($sql, $first, $last, $step, $params); } /** @@ -639,7 +639,7 @@ function Piwik_SegmentedFetchAll($sql, $first, $last, $step, $params = array()) */ function Piwik_SegmentedQuery($sql, $first, $last, $step, $params = array()) { - return Piwik_Sql::segmentedQuery($sql, $first, $last, $step, $params); + return Piwik_Sql::segmentedQuery($sql, $first, $last, $step, $params); } /** @@ -654,7 +654,7 @@ function Piwik_SegmentedQuery($sql, $first, $last, $step, $params = array()) */ function Piwik_GetDbLock($lockName, $maxRetries = 30) { - return Piwik_Sql::getDbLock($lockName, $maxRetries); + return Piwik_Sql::getDbLock($lockName, $maxRetries); } /** @@ -667,6 +667,6 @@ function Piwik_GetDbLock($lockName, $maxRetries = 30) */ function Piwik_ReleaseDbLock($lockName) { - return Piwik_Sql::releaseDbLock($lockName); + return Piwik_Sql::releaseDbLock($lockName); } diff --git a/core/PluginsFunctions/WidgetsList.php b/core/PluginsFunctions/WidgetsList.php index a0b080ef95..08701776b0 100644 --- a/core/PluginsFunctions/WidgetsList.php +++ b/core/PluginsFunctions/WidgetsList.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package PluginsFunctions */ @@ -14,161 +14,157 @@ */ class Piwik_WidgetsList { - /** - * List of widgets - * - * @var array - */ - static protected $widgets = null; - - /** - * Indicates whether the hook was posted or not - * - * @var bool - */ - static protected $hookCalled = false; - - /** - * Returns all available widgets - * The event WidgetsList.add is used to create the list - * - * @return array - */ - static public function get() - { - self::addWidgets(); - Piwik_PostEvent('WidgetsList.get'); - - uksort(self::$widgets, array('Piwik_WidgetsList', '_sortWidgetCategories')); - - $widgets = array(); - foreach(self::$widgets as $key => $v) - { - if(isset($widgets[Piwik_Translate($key)])) { - $v = array_merge($widgets[Piwik_Translate($key)], $v); - } - $widgets[Piwik_Translate($key)] = $v; - } - return $widgets; - } - - private static function addWidgets() - { - if (!self::$hookCalled) { - self::$hookCalled = true; - Piwik_PostEvent('WidgetsList.add'); - } - } - - /** - * Sorting method for widget categories - * - * @param string $a - * @param string $b - * @return bool - */ - protected static function _sortWidgetCategories($a, $b) - { - $order = array( - 'VisitsSummary_VisitsSummary', - 'Live!', - 'General_Visitors', - 'UserSettings_VisitorSettings', - 'Actions_Actions', - 'Actions_SubmenuSitesearch', - 'Referers_Referers', - 'Goals_Goals', - 'Goals_Ecommerce', - '_others_', - 'Example Widgets', - 'ExamplePlugin_exampleWidgets', - ); - - if(($oa = array_search($a, $order)) === false) { - $oa = array_search('_others_', $order); - } - if(($ob = array_search($b, $order)) === false) { - $ob = array_search('_others_', $order); - } - return $oa > $ob; - } - - /** - * Adds an widget to the list - * - * @param string $widgetCategory - * @param string $widgetName - * @param string $controllerName - * @param string $controllerAction - * @param array $customParameters - */ - static public function add($widgetCategory, $widgetName, $controllerName, $controllerAction, $customParameters) - { - $widgetName = Piwik_Translate($widgetName); - $widgetUniqueId = 'widget' . $controllerName . $controllerAction; - foreach($customParameters as $name => $value) - { - if (is_array($value)) - { - // use 'Array' for backward compatibility; - // could we switch to using $value[0]? - $value = 'Array'; - } - $widgetUniqueId .= $name . $value; - } - self::$widgets[$widgetCategory][] = array( - 'name' => $widgetName, - 'uniqueId' => $widgetUniqueId, - 'parameters' => array ( 'module' => $controllerName, - 'action' => $controllerAction - ) + $customParameters - ); - } - - - static public function remove($widgetCategory, $widgetName = false) - { - if(empty($widgetName)) { - unset(self::$widgets[$widgetCategory]); - return; - } - foreach(self::$widgets[$widgetCategory] as $id => $widget) { - if($widget['name'] == $widgetName) { - unset(self::$widgets[$widgetCategory][$id]); - return; - } - } - } - - /** - * Checks if the widget with the given parameters exists in der widget list - * - * @param string $controllerName - * @param string $controllerAction - * @return bool - */ - static public 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; - } + /** + * List of widgets + * + * @var array + */ + static protected $widgets = null; + + /** + * Indicates whether the hook was posted or not + * + * @var bool + */ + static protected $hookCalled = false; + + /** + * Returns all available widgets + * The event WidgetsList.add is used to create the list + * + * @return array + */ + static public function get() + { + self::addWidgets(); + Piwik_PostEvent('WidgetsList.get'); + + uksort(self::$widgets, array('Piwik_WidgetsList', '_sortWidgetCategories')); + + $widgets = array(); + foreach (self::$widgets as $key => $v) { + if (isset($widgets[Piwik_Translate($key)])) { + $v = array_merge($widgets[Piwik_Translate($key)], $v); + } + $widgets[Piwik_Translate($key)] = $v; + } + return $widgets; + } + + private static function addWidgets() + { + if (!self::$hookCalled) { + self::$hookCalled = true; + Piwik_PostEvent('WidgetsList.add'); + } + } + + /** + * Sorting method for widget categories + * + * @param string $a + * @param string $b + * @return bool + */ + protected static function _sortWidgetCategories($a, $b) + { + $order = array( + 'VisitsSummary_VisitsSummary', + 'Live!', + 'General_Visitors', + 'UserSettings_VisitorSettings', + 'Actions_Actions', + 'Actions_SubmenuSitesearch', + 'Referers_Referers', + 'Goals_Goals', + 'Goals_Ecommerce', + '_others_', + 'Example Widgets', + 'ExamplePlugin_exampleWidgets', + ); + + if (($oa = array_search($a, $order)) === false) { + $oa = array_search('_others_', $order); + } + if (($ob = array_search($b, $order)) === false) { + $ob = array_search('_others_', $order); + } + return $oa > $ob; + } + + /** + * Adds an widget to the list + * + * @param string $widgetCategory + * @param string $widgetName + * @param string $controllerName + * @param string $controllerAction + * @param array $customParameters + */ + static public function add($widgetCategory, $widgetName, $controllerName, $controllerAction, $customParameters) + { + $widgetName = Piwik_Translate($widgetName); + $widgetUniqueId = 'widget' . $controllerName . $controllerAction; + foreach ($customParameters as $name => $value) { + if (is_array($value)) { + // use 'Array' for backward compatibility; + // could we switch to using $value[0]? + $value = 'Array'; + } + $widgetUniqueId .= $name . $value; + } + self::$widgets[$widgetCategory][] = array( + 'name' => $widgetName, + 'uniqueId' => $widgetUniqueId, + 'parameters' => array('module' => $controllerName, + 'action' => $controllerAction + ) + $customParameters + ); + } + + + static public function remove($widgetCategory, $widgetName = false) + { + if (empty($widgetName)) { + unset(self::$widgets[$widgetCategory]); + return; + } + foreach (self::$widgets[$widgetCategory] as $id => $widget) { + if ($widget['name'] == $widgetName) { + unset(self::$widgets[$widgetCategory][$id]); + return; + } + } + } + + /** + * Checks if the widget with the given parameters exists in der widget list + * + * @param string $controllerName + * @param string $controllerAction + * @return bool + */ + static public 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; + } /** * Method to reset the widget list * For testing only */ - public static function _reset() { + public static function _reset() + { self::$widgets = null; self::$hookCalled = false; } @@ -183,7 +179,7 @@ class Piwik_WidgetsList */ function Piwik_GetWidgetsList() { - return Piwik_WidgetsList::get(); + return Piwik_WidgetsList::get(); } /** @@ -191,15 +187,15 @@ function Piwik_GetWidgetsList() * * @see Piwik_WidgetsList::add * - * @param string $widgetCategory - * @param string $widgetName - * @param string $controllerName - * @param string $controllerAction - * @param array $customParameters + * @param string $widgetCategory + * @param string $widgetName + * @param string $controllerName + * @param string $controllerAction + * @param array $customParameters */ -function Piwik_AddWidget( $widgetCategory, $widgetName, $controllerName, $controllerAction, $customParameters = array()) +function Piwik_AddWidget($widgetCategory, $widgetName, $controllerName, $controllerAction, $customParameters = array()) { - Piwik_WidgetsList::add($widgetCategory, $widgetName, $controllerName, $controllerAction, $customParameters); + Piwik_WidgetsList::add($widgetCategory, $widgetName, $controllerName, $controllerAction, $customParameters); } /** @@ -207,11 +203,11 @@ function Piwik_AddWidget( $widgetCategory, $widgetName, $controllerName, $contro * * @see Piwik_WidgetsList::isDefined * - * @param string $controllerName - * @param string $controllerAction + * @param string $controllerName + * @param string $controllerAction * @return bool */ function Piwik_IsWidgetDefined($controllerName, $controllerAction) { - return Piwik_WidgetsList::isDefined($controllerName, $controllerAction); + return Piwik_WidgetsList::isDefined($controllerName, $controllerAction); } diff --git a/core/PluginsManager.php b/core/PluginsManager.php index de5a580d8a..5e4c7e3a5f 100644 --- a/core/PluginsManager.php +++ b/core/PluginsManager.php @@ -32,664 +32,614 @@ require_once PIWIK_INCLUDE_PATH . '/core/PluginsFunctions/Sql.php'; */ class Piwik_PluginsManager { - /** - * @var Event_Dispatcher - */ - public $dispatcher; - - protected $pluginsToLoad = array(); - - protected $doLoadPlugins = true; - protected $loadedPlugins = array(); - - protected $doLoadAlwaysActivatedPlugins = true; - protected $pluginToAlwaysActivate = array( - 'CoreHome', - 'CoreUpdater', - 'CoreAdminHome', - 'CorePluginsAdmin', - 'Installation', - 'SitesManager', - 'UsersManager', - 'API', - 'Proxy', - 'LanguagesManager', - ); - - static private $instance = null; - - /** - * Returns the singleton Piwik_PluginsManager - * - * @return Piwik_PluginsManager - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - private function __construct() - { - $this->dispatcher = Event_Dispatcher::getInstance(); - } - - /** - * Update Plugins config - * - * @param array $plugins Plugins - */ - private function updatePluginsConfig($plugins) - { - $section = Piwik_Config::getInstance()->Plugins; - $section['Plugins'] = $plugins; - Piwik_Config::getInstance()->Plugins = $section; - } - - /** - * Update Plugins_Tracker config - * - * @param array $plugins Plugins - */ - private function updatePluginsTrackerConfig($plugins) - { - $section = Piwik_Config::getInstance()->Plugins_Tracker; - $section['Plugins_Tracker'] = $plugins; - Piwik_Config::getInstance()->Plugins_Tracker = $section; - } - - /** - * Update PluginsInstalled config - * - * @param array $plugins Plugins - */ - private function updatePluginsInstalledConfig($plugins) - { - $section = Piwik_Config::getInstance()->PluginsInstalled; - $section['PluginsInstalled'] = $plugins; - Piwik_Config::getInstance()->PluginsInstalled = $section; - } - - /** - * Returns true if plugin is always activated - * - * @param string $name Name of plugin - * @return bool - */ - public function isPluginAlwaysActivated( $name ) - { - return in_array( $name, $this->pluginToAlwaysActivate); - } - - /** - * Returns true if plugin has been activated - * - * @param string $name Name of plugin - * @return bool - */ - public function isPluginActivated( $name ) - { - return in_array( $name, $this->pluginsToLoad) - || $this->isPluginAlwaysActivated( $name ); - } - - /** - * Returns true if plugin is loaded (in memory) - * - * @param string $name Name of plugin - * @return bool - */ - public function isPluginLoaded( $name ) - { - return isset($this->loadedPlugins[$name]); - } - - /** - * Reads the directories inside the plugins/ directory and returns their names in an array - * - * @return array - */ - public function readPluginsDirectory() - { - $pluginsName = _glob( PIWIK_INCLUDE_PATH . '/plugins/*', GLOB_ONLYDIR); - $result = array(); - if ($pluginsName != false) - { - foreach ($pluginsName as $path) - { - $name = basename($path); - if (file_exists($path.'/'.$name.'.php')) // only add folder if a Plugin/Plugin.php file exists - { - $result[] = $name; - } - } - } - return $result; - } - - /** - * Deactivate plugin - * - * @param string $pluginName Name of plugin - */ - public function deactivatePlugin($pluginName) - { - $plugins = $this->pluginsToLoad; - $key = array_search($pluginName, $plugins); - - $plugin = $this->loadPlugin($pluginName); - if ($plugin !== null) - { - $plugin->deactivate(); - } - - if($key !== false) - { - unset($plugins[$key]); - } - $this->updatePluginsConfig($plugins); - - $pluginsTracker = Piwik_Config::getInstance()->Plugins_Tracker['Plugins_Tracker']; - if(!is_null($pluginsTracker)) - { - $key = array_search($pluginName, $pluginsTracker); - if($key !== false) - { - unset($pluginsTracker[$key]); - $this->updatePluginsTrackerConfig($pluginsTracker); - } - } - - // Delete merged js/css files to force regenerations to exclude the deactivated plugin - Piwik_Config::getInstance()->forceSave(); - Piwik::deleteAllCacheOnUpdate(); - } - - /** - * Install loaded plugins - */ - public function installLoadedPlugins() - { - foreach($this->getLoadedPlugins() as $plugin) - { - try { - $this->installPluginIfNecessary( $plugin ); - }catch(Exception $e){ - echo $e->getMessage(); - } - } - } - - /** - * Activate the specified plugin and install (if needed) - * - * @param string $pluginName Name of plugin - * @throws Exception - */ - public function activatePlugin($pluginName) - { - $plugins = Piwik_Config::getInstance()->Plugins['Plugins']; - if(in_array($pluginName, $plugins)) - { - throw new Exception("Plugin '$pluginName' already activated."); - } - - $existingPlugins = $this->readPluginsDirectory(); - if( array_search($pluginName, $existingPlugins) === false) - { - // ToDo: This fails in tracker-mode. We should log this however. - //Piwik::log(sprintf("Unable to find the plugin '%s' in activatePlugin.", $pluginName)); - return; - } - - $plugin = $this->loadPlugin($pluginName); - if ($plugin === null) - { - return; - } - - $this->installPluginIfNecessary($plugin); - - $plugin->activate(); - - // we add the plugin to the list of activated plugins - if(!in_array($pluginName, $plugins)) - { - $plugins[] = $pluginName; - } - else - { - // clean up if we find a dupe - $plugins = array_unique($plugins); - } - - // the config file will automatically be saved with the new plugin - $this->updatePluginsConfig($plugins); - Piwik_Config::getInstance()->forceSave(); - - // Delete merged js/css files to force regenerations to include the activated plugin - Piwik::deleteAllCacheOnUpdate(); - } - - /** - * Load the specified plugins - * - * @param array $pluginsToLoad Array of plugins to load - */ - public function loadPlugins( array $pluginsToLoad ) - { - // case no plugins to load - if(is_null($pluginsToLoad)) - { - $pluginsToLoad = array(); - } - $this->pluginsToLoad = $pluginsToLoad; - $this->reloadPlugins(); - } - - /** - * Disable plugin loading - */ - public function doNotLoadPlugins() - { - $this->doLoadPlugins = false; - } - - /** - * Disable loading of "always activated" plugins - */ - public function doNotLoadAlwaysActivatedPlugins() - { - $this->doLoadAlwaysActivatedPlugins = false; - } - - /** - * Load translations for loaded plugins - * - * @param bool|string $language Optional language code - */ - public function loadPluginTranslations($language = false) - { - if(empty($language)) - { - $language = Piwik_Translate::getInstance()->getLanguageToLoad(); - } - $plugins = $this->getLoadedPlugins(); - - foreach($plugins as $plugin) - { - $this->loadTranslation( $plugin, $language ); - } - } - - /** - * Execute postLoad() hook for loaded plugins - * - * @see Piwik_Plugin::postLoad() - */ - public function postLoadPlugins() - { - $plugins = $this->getLoadedPlugins(); - foreach($plugins as $plugin) - { - $plugin->postLoad(); - } - } - - /** - * Returns an array containing the plugins class names (eg. 'Piwik_UserCountry' and NOT 'UserCountry') - * - * @return array - */ - public function getLoadedPluginsName() - { - return array_map('get_class', $this->getLoadedPlugins()); - } - - /** - * Returns an array of key,value with the following format: array( - * 'UserCountry' => Piwik_Plugin $pluginObject, - * 'UserSettings' => Piwik_Plugin $pluginObject, - * ); - * - * @return array - */ - public function getLoadedPlugins() - { - return $this->loadedPlugins; - } - - /** - * Returns the given Piwik_Plugin object - * - * @param string $name - * @throws Exception - * @return array - */ - public function getLoadedPlugin($name) - { - if(!isset($this->loadedPlugins[$name])) - { - throw new Exception("The plugin '$name' has not been loaded."); - } - return $this->loadedPlugins[$name]; - } - - /** - * Load the plugins classes installed. - * Register the observers for every plugin. - */ - private function reloadPlugins() - { - $this->pluginsToLoad = array_unique($this->pluginsToLoad); - - if($this->doLoadAlwaysActivatedPlugins) - { - $this->pluginsToLoad = array_merge($this->pluginsToLoad, $this->pluginToAlwaysActivate); - } - - foreach($this->pluginsToLoad as $pluginName) - { - if(!$this->isPluginLoaded($pluginName)) - { - $newPlugin = $this->loadPlugin($pluginName); - if ($newPlugin === null) - { - continue; - } - - if($this->doLoadPlugins - && $this->isPluginActivated($pluginName)) - { - $this->addPluginObservers( $newPlugin ); - } - } - } - } - - /** - * 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 string $pluginName - * @throws Exception - * @return Piwik_Plugin|null - */ - public function loadPlugin( $pluginName ) - { - if(isset($this->loadedPlugins[$pluginName])) - { - return $this->loadedPlugins[$pluginName]; - } - $pluginFileName = sprintf("%s/%s.php", $pluginName, $pluginName); - $pluginClassName = sprintf('Piwik_%s', $pluginName); - - if( !Piwik_Common::isValidFilename($pluginName)) - { - throw new Exception(sprintf("The plugin filename '%s' is not a valid filename", $pluginFileName)); - } - - $path = PIWIK_INCLUDE_PATH . '/plugins/' . $pluginFileName; - - if(!file_exists($path)) - { - // ToDo: We should log this - but this will crash in Tracker mode since core/Piwik is not loaded - //Piwik::log(sprintf("Unable to load plugin '%s' because '%s' couldn't be found.", $pluginName, $path)); - throw new Exception(sprintf("Unable to load plugin '%s' because '%s' couldn't be found.", $pluginName, $path)); - } - - // Don't remove this. - // Our autoloader can't find plugins/PluginName/PluginName.php - require_once $path; // prefixed by PIWIK_INCLUDE_PATH - - if(!class_exists($pluginClassName, false)) - { - throw new Exception("The class $pluginClassName couldn't be found in the file '$path'"); - } - $newPlugin = new $pluginClassName(); - - if(!($newPlugin instanceof Piwik_Plugin)) - { - throw new Exception("The plugin $pluginClassName in the file $path must inherit from Piwik_Plugin."); - } - - $this->addLoadedPlugin( $pluginName, $newPlugin); - - return $newPlugin; - } - - /** - * Unload plugin - * - * @param Piwik_Plugin $plugin - * @throws Exception - */ - public function unloadPlugin( $plugin ) - { - if(!($plugin instanceof Piwik_Plugin )) - { - $oPlugin = $this->loadPlugin( $plugin ); - if ($oPlugin === null) - { - unset($this->loadedPlugins[$plugin]); - return; - } - - $plugin = $oPlugin; - } - $hooks = $plugin->getListHooksRegistered(); - - foreach($hooks as $hookName => $methodToCall) - { - $success = $this->dispatcher->removeObserver( array( $plugin, $methodToCall), $hookName ); - if($success !== true) - { - throw new Exception("Error unloading plugin = ".$plugin->getPluginName() . ", method = $methodToCall, hook = $hookName "); - } - } - unset($this->loadedPlugins[$plugin->getPluginName()]); - } - - /** - * Unload all loaded plugins - */ - public function unloadPlugins() - { - $pluginsLoaded = $this->getLoadedPlugins(); - foreach($pluginsLoaded as $plugin) - { - $this->unloadPlugin($plugin); - } - } - - /** - * Install loaded plugins - */ - private function installPlugins() - { - foreach($this->getLoadedPlugins() as $plugin) - { - $this->installPlugin($plugin); - } - } - - /** - * Install a specific plugin - * - * @param Piwik_Plugin $plugin - * @throws Piwik_PluginsManager_PluginException if installation fails - */ - private function installPlugin( Piwik_Plugin $plugin ) - { - try{ - $plugin->install(); - } catch(Exception $e) { - throw new Piwik_PluginsManager_PluginException($plugin->getPluginName(), $e->getMessage()); - } - } - - - /** - * For the given plugin, add all the observers of this plugin. - * - * @param Piwik_Plugin $plugin - */ - private function addPluginObservers( Piwik_Plugin $plugin ) - { - $hooks = $plugin->getListHooksRegistered(); - - foreach($hooks as $hookName => $methodToCall) - { - $this->dispatcher->addObserver( array( $plugin, $methodToCall), $hookName ); - } - } - - /** - * Add a plugin in the loaded plugins array - * - * @param string $pluginName plugin name without prefix (eg. 'UserCountry') - * @param Piwik_Plugin $newPlugin - */ - private function addLoadedPlugin( $pluginName, Piwik_Plugin $newPlugin ) - { - $this->loadedPlugins[$pluginName] = $newPlugin; - } - - /** - * Load translation - * - * @param Piwik_Plugin $plugin - * @param string $langCode - * @throws Exception - * @return void - */ - private function loadTranslation( $plugin, $langCode ) - { - // 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'])) - { - $infos['translationAvailable'] = false; - } - $translationAvailable = $infos['translationAvailable']; - - if(!$translationAvailable) - { - return; - } - - $pluginName = $plugin->getPluginName(); - - $path = PIWIK_INCLUDE_PATH . '/plugins/' . $pluginName .'/lang/%s.php'; - - $defaultLangPath = sprintf($path, $langCode); - $defaultEnglishLangPath = sprintf($path, 'en'); - - $translations = array(); - - if(file_exists($defaultLangPath)) - { - require $defaultLangPath; - } - elseif(file_exists($defaultEnglishLangPath)) - { - require $defaultEnglishLangPath; - } - else - { - throw new Exception("Language file not found for the plugin '$pluginName'."); - } - Piwik_Translate::getInstance()->mergeTranslationArray($translations); - } - - /** - * Return names of installed plugins - * - * @return array - */ - public function getInstalledPluginsName() - { - $pluginNames = Piwik_Config::getInstance()->PluginsInstalled['PluginsInstalled']; - return $pluginNames; - } - - /** - * Returns names of plugins that should be loaded, but cannot be since their - * files cannot be found. - * - * @return array - */ - public function getMissingPlugins() - { - $missingPlugins = array(); - if (isset(Piwik_Config::getInstance()->Plugins['Plugins'])) - { - $plugins = Piwik_Config::getInstance()->Plugins['Plugins']; - foreach ($plugins as $pluginName) - { - // if a plugin is listed in the config, but is not loaded, it does not exist in the folder - if (!Piwik_PluginsManager::getInstance()->isPluginLoaded($pluginName)) - { - $missingPlugins[] = $pluginName; - } - } - } - return $missingPlugins; - } - - /** - * Install a plugin, if necessary - * - * @param Piwik_Plugin $plugin - */ - private function installPluginIfNecessary( Piwik_Plugin $plugin ) - { - $pluginName = $plugin->getPluginName(); - - $saveConfig = false; - - // is the plugin already installed or is it the first time we activate it? - $pluginsInstalled = $this->getInstalledPluginsName(); - if(!in_array($pluginName, $pluginsInstalled)) - { - $this->installPlugin($plugin); - $pluginsInstalled[] = $pluginName; - $this->updatePluginsInstalledConfig($pluginsInstalled); - $saveConfig = true; - } - - $information = $plugin->getInformation(); - - // if the plugin is to be loaded during the statistics logging - if(isset($information['TrackerPlugin']) - && $information['TrackerPlugin'] === true) - { - $pluginsTracker = Piwik_Config::getInstance()->Plugins_Tracker['Plugins_Tracker']; - if(is_null($pluginsTracker)) - { - $pluginsTracker = array(); - } - if(!in_array($pluginName, $pluginsTracker)) - { - $pluginsTracker[] = $pluginName; - $this->updatePluginsTrackerConfig($pluginsTracker); - $saveConfig = true; - } - } - - if($saveConfig) - { - Piwik_Config::getInstance()->forceSave(); - } - } + /** + * @var Event_Dispatcher + */ + public $dispatcher; + + protected $pluginsToLoad = array(); + + protected $doLoadPlugins = true; + protected $loadedPlugins = array(); + + protected $doLoadAlwaysActivatedPlugins = true; + protected $pluginToAlwaysActivate = array( + 'CoreHome', + 'CoreUpdater', + 'CoreAdminHome', + 'CorePluginsAdmin', + 'Installation', + 'SitesManager', + 'UsersManager', + 'API', + 'Proxy', + 'LanguagesManager', + ); + + static private $instance = null; + + /** + * Returns the singleton Piwik_PluginsManager + * + * @return Piwik_PluginsManager + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + private function __construct() + { + $this->dispatcher = Event_Dispatcher::getInstance(); + } + + /** + * Update Plugins config + * + * @param array $plugins Plugins + */ + private function updatePluginsConfig($plugins) + { + $section = Piwik_Config::getInstance()->Plugins; + $section['Plugins'] = $plugins; + Piwik_Config::getInstance()->Plugins = $section; + } + + /** + * Update Plugins_Tracker config + * + * @param array $plugins Plugins + */ + private function updatePluginsTrackerConfig($plugins) + { + $section = Piwik_Config::getInstance()->Plugins_Tracker; + $section['Plugins_Tracker'] = $plugins; + Piwik_Config::getInstance()->Plugins_Tracker = $section; + } + + /** + * Update PluginsInstalled config + * + * @param array $plugins Plugins + */ + private function updatePluginsInstalledConfig($plugins) + { + $section = Piwik_Config::getInstance()->PluginsInstalled; + $section['PluginsInstalled'] = $plugins; + Piwik_Config::getInstance()->PluginsInstalled = $section; + } + + /** + * Returns true if plugin is always activated + * + * @param string $name Name of plugin + * @return bool + */ + public function isPluginAlwaysActivated($name) + { + return in_array($name, $this->pluginToAlwaysActivate); + } + + /** + * Returns true if plugin has been activated + * + * @param string $name Name of plugin + * @return bool + */ + public function isPluginActivated($name) + { + return in_array($name, $this->pluginsToLoad) + || $this->isPluginAlwaysActivated($name); + } + + /** + * Returns true if plugin is loaded (in memory) + * + * @param string $name Name of plugin + * @return bool + */ + public function isPluginLoaded($name) + { + return isset($this->loadedPlugins[$name]); + } + + /** + * Reads the directories inside the plugins/ directory and returns their names in an array + * + * @return array + */ + public function readPluginsDirectory() + { + $pluginsName = _glob(PIWIK_INCLUDE_PATH . '/plugins/*', GLOB_ONLYDIR); + $result = array(); + if ($pluginsName != false) { + foreach ($pluginsName as $path) { + $name = basename($path); + if (file_exists($path . '/' . $name . '.php')) // only add folder if a Plugin/Plugin.php file exists + { + $result[] = $name; + } + } + } + return $result; + } + + /** + * Deactivate plugin + * + * @param string $pluginName Name of plugin + */ + public function deactivatePlugin($pluginName) + { + $plugins = $this->pluginsToLoad; + $key = array_search($pluginName, $plugins); + + $plugin = $this->loadPlugin($pluginName); + if ($plugin !== null) { + $plugin->deactivate(); + } + + if ($key !== false) { + unset($plugins[$key]); + } + $this->updatePluginsConfig($plugins); + + $pluginsTracker = Piwik_Config::getInstance()->Plugins_Tracker['Plugins_Tracker']; + if (!is_null($pluginsTracker)) { + $key = array_search($pluginName, $pluginsTracker); + if ($key !== false) { + unset($pluginsTracker[$key]); + $this->updatePluginsTrackerConfig($pluginsTracker); + } + } + + // Delete merged js/css files to force regenerations to exclude the deactivated plugin + Piwik_Config::getInstance()->forceSave(); + Piwik::deleteAllCacheOnUpdate(); + } + + /** + * Install loaded plugins + */ + public function installLoadedPlugins() + { + foreach ($this->getLoadedPlugins() as $plugin) { + try { + $this->installPluginIfNecessary($plugin); + } catch (Exception $e) { + echo $e->getMessage(); + } + } + } + + /** + * Activate the specified plugin and install (if needed) + * + * @param string $pluginName Name of plugin + * @throws Exception + */ + public function activatePlugin($pluginName) + { + $plugins = Piwik_Config::getInstance()->Plugins['Plugins']; + if (in_array($pluginName, $plugins)) { + throw new Exception("Plugin '$pluginName' already activated."); + } + + $existingPlugins = $this->readPluginsDirectory(); + if (array_search($pluginName, $existingPlugins) === false) { + // ToDo: This fails in tracker-mode. We should log this however. + //Piwik::log(sprintf("Unable to find the plugin '%s' in activatePlugin.", $pluginName)); + return; + } + + $plugin = $this->loadPlugin($pluginName); + if ($plugin === null) { + return; + } + + $this->installPluginIfNecessary($plugin); + + $plugin->activate(); + + // we add the plugin to the list of activated plugins + if (!in_array($pluginName, $plugins)) { + $plugins[] = $pluginName; + } else { + // clean up if we find a dupe + $plugins = array_unique($plugins); + } + + // the config file will automatically be saved with the new plugin + $this->updatePluginsConfig($plugins); + Piwik_Config::getInstance()->forceSave(); + + // Delete merged js/css files to force regenerations to include the activated plugin + Piwik::deleteAllCacheOnUpdate(); + } + + /** + * Load the specified plugins + * + * @param array $pluginsToLoad Array of plugins to load + */ + public function loadPlugins(array $pluginsToLoad) + { + // case no plugins to load + if (is_null($pluginsToLoad)) { + $pluginsToLoad = array(); + } + $this->pluginsToLoad = $pluginsToLoad; + $this->reloadPlugins(); + } + + /** + * Disable plugin loading + */ + public function doNotLoadPlugins() + { + $this->doLoadPlugins = false; + } + + /** + * Disable loading of "always activated" plugins + */ + public function doNotLoadAlwaysActivatedPlugins() + { + $this->doLoadAlwaysActivatedPlugins = false; + } + + /** + * Load translations for loaded plugins + * + * @param bool|string $language Optional language code + */ + public function loadPluginTranslations($language = false) + { + if (empty($language)) { + $language = Piwik_Translate::getInstance()->getLanguageToLoad(); + } + $plugins = $this->getLoadedPlugins(); + + foreach ($plugins as $plugin) { + $this->loadTranslation($plugin, $language); + } + } + + /** + * Execute postLoad() hook for loaded plugins + * + * @see Piwik_Plugin::postLoad() + */ + public function postLoadPlugins() + { + $plugins = $this->getLoadedPlugins(); + foreach ($plugins as $plugin) { + $plugin->postLoad(); + } + } + + /** + * Returns an array containing the plugins class names (eg. 'Piwik_UserCountry' and NOT 'UserCountry') + * + * @return array + */ + public function getLoadedPluginsName() + { + return array_map('get_class', $this->getLoadedPlugins()); + } + + /** + * Returns an array of key,value with the following format: array( + * 'UserCountry' => Piwik_Plugin $pluginObject, + * 'UserSettings' => Piwik_Plugin $pluginObject, + * ); + * + * @return array + */ + public function getLoadedPlugins() + { + return $this->loadedPlugins; + } + + /** + * Returns the given Piwik_Plugin object + * + * @param string $name + * @throws Exception + * @return array + */ + public function getLoadedPlugin($name) + { + if (!isset($this->loadedPlugins[$name])) { + throw new Exception("The plugin '$name' has not been loaded."); + } + return $this->loadedPlugins[$name]; + } + + /** + * Load the plugins classes installed. + * Register the observers for every plugin. + */ + private function reloadPlugins() + { + $this->pluginsToLoad = array_unique($this->pluginsToLoad); + + if ($this->doLoadAlwaysActivatedPlugins) { + $this->pluginsToLoad = array_merge($this->pluginsToLoad, $this->pluginToAlwaysActivate); + } + + foreach ($this->pluginsToLoad as $pluginName) { + if (!$this->isPluginLoaded($pluginName)) { + $newPlugin = $this->loadPlugin($pluginName); + if ($newPlugin === null) { + continue; + } + + if ($this->doLoadPlugins + && $this->isPluginActivated($pluginName) + ) { + $this->addPluginObservers($newPlugin); + } + } + } + } + + /** + * 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 string $pluginName + * @throws Exception + * @return Piwik_Plugin|null + */ + public function loadPlugin($pluginName) + { + if (isset($this->loadedPlugins[$pluginName])) { + return $this->loadedPlugins[$pluginName]; + } + $pluginFileName = sprintf("%s/%s.php", $pluginName, $pluginName); + $pluginClassName = sprintf('Piwik_%s', $pluginName); + + if (!Piwik_Common::isValidFilename($pluginName)) { + throw new Exception(sprintf("The plugin filename '%s' is not a valid filename", $pluginFileName)); + } + + $path = PIWIK_INCLUDE_PATH . '/plugins/' . $pluginFileName; + + if (!file_exists($path)) { + // ToDo: We should log this - but this will crash in Tracker mode since core/Piwik is not loaded + //Piwik::log(sprintf("Unable to load plugin '%s' because '%s' couldn't be found.", $pluginName, $path)); + throw new Exception(sprintf("Unable to load plugin '%s' because '%s' couldn't be found.", $pluginName, $path)); + } + + // Don't remove this. + // Our autoloader can't find plugins/PluginName/PluginName.php + require_once $path; // prefixed by PIWIK_INCLUDE_PATH + + if (!class_exists($pluginClassName, false)) { + throw new Exception("The class $pluginClassName couldn't be found in the file '$path'"); + } + $newPlugin = new $pluginClassName(); + + if (!($newPlugin instanceof Piwik_Plugin)) { + throw new Exception("The plugin $pluginClassName in the file $path must inherit from Piwik_Plugin."); + } + + $this->addLoadedPlugin($pluginName, $newPlugin); + + return $newPlugin; + } + + /** + * Unload plugin + * + * @param Piwik_Plugin $plugin + * @throws Exception + */ + public function unloadPlugin($plugin) + { + if (!($plugin instanceof Piwik_Plugin)) { + $oPlugin = $this->loadPlugin($plugin); + if ($oPlugin === null) { + unset($this->loadedPlugins[$plugin]); + return; + } + + $plugin = $oPlugin; + } + $hooks = $plugin->getListHooksRegistered(); + + foreach ($hooks as $hookName => $methodToCall) { + $success = $this->dispatcher->removeObserver(array($plugin, $methodToCall), $hookName); + if ($success !== true) { + throw new Exception("Error unloading plugin = " . $plugin->getPluginName() . ", method = $methodToCall, hook = $hookName "); + } + } + unset($this->loadedPlugins[$plugin->getPluginName()]); + } + + /** + * Unload all loaded plugins + */ + public function unloadPlugins() + { + $pluginsLoaded = $this->getLoadedPlugins(); + foreach ($pluginsLoaded as $plugin) { + $this->unloadPlugin($plugin); + } + } + + /** + * Install loaded plugins + */ + private function installPlugins() + { + foreach ($this->getLoadedPlugins() as $plugin) { + $this->installPlugin($plugin); + } + } + + /** + * Install a specific plugin + * + * @param Piwik_Plugin $plugin + * @throws Piwik_PluginsManager_PluginException if installation fails + */ + private function installPlugin(Piwik_Plugin $plugin) + { + try { + $plugin->install(); + } catch (Exception $e) { + throw new Piwik_PluginsManager_PluginException($plugin->getPluginName(), $e->getMessage()); + } + } + + + /** + * For the given plugin, add all the observers of this plugin. + * + * @param Piwik_Plugin $plugin + */ + private function addPluginObservers(Piwik_Plugin $plugin) + { + $hooks = $plugin->getListHooksRegistered(); + + foreach ($hooks as $hookName => $methodToCall) { + $this->dispatcher->addObserver(array($plugin, $methodToCall), $hookName); + } + } + + /** + * Add a plugin in the loaded plugins array + * + * @param string $pluginName plugin name without prefix (eg. 'UserCountry') + * @param Piwik_Plugin $newPlugin + */ + private function addLoadedPlugin($pluginName, Piwik_Plugin $newPlugin) + { + $this->loadedPlugins[$pluginName] = $newPlugin; + } + + /** + * Load translation + * + * @param Piwik_Plugin $plugin + * @param string $langCode + * @throws Exception + * @return void + */ + private function loadTranslation($plugin, $langCode) + { + // 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'])) { + $infos['translationAvailable'] = false; + } + $translationAvailable = $infos['translationAvailable']; + + if (!$translationAvailable) { + return; + } + + $pluginName = $plugin->getPluginName(); + + $path = PIWIK_INCLUDE_PATH . '/plugins/' . $pluginName . '/lang/%s.php'; + + $defaultLangPath = sprintf($path, $langCode); + $defaultEnglishLangPath = sprintf($path, 'en'); + + $translations = array(); + + if (file_exists($defaultLangPath)) { + require $defaultLangPath; + } elseif (file_exists($defaultEnglishLangPath)) { + require $defaultEnglishLangPath; + } else { + throw new Exception("Language file not found for the plugin '$pluginName'."); + } + Piwik_Translate::getInstance()->mergeTranslationArray($translations); + } + + /** + * Return names of installed plugins + * + * @return array + */ + public function getInstalledPluginsName() + { + $pluginNames = Piwik_Config::getInstance()->PluginsInstalled['PluginsInstalled']; + return $pluginNames; + } + + /** + * Returns names of plugins that should be loaded, but cannot be since their + * files cannot be found. + * + * @return array + */ + public function getMissingPlugins() + { + $missingPlugins = array(); + if (isset(Piwik_Config::getInstance()->Plugins['Plugins'])) { + $plugins = Piwik_Config::getInstance()->Plugins['Plugins']; + foreach ($plugins as $pluginName) { + // if a plugin is listed in the config, but is not loaded, it does not exist in the folder + if (!Piwik_PluginsManager::getInstance()->isPluginLoaded($pluginName)) { + $missingPlugins[] = $pluginName; + } + } + } + return $missingPlugins; + } + + /** + * Install a plugin, if necessary + * + * @param Piwik_Plugin $plugin + */ + private function installPluginIfNecessary(Piwik_Plugin $plugin) + { + $pluginName = $plugin->getPluginName(); + + $saveConfig = false; + + // is the plugin already installed or is it the first time we activate it? + $pluginsInstalled = $this->getInstalledPluginsName(); + if (!in_array($pluginName, $pluginsInstalled)) { + $this->installPlugin($plugin); + $pluginsInstalled[] = $pluginName; + $this->updatePluginsInstalledConfig($pluginsInstalled); + $saveConfig = true; + } + + $information = $plugin->getInformation(); + + // if the plugin is to be loaded during the statistics logging + if (isset($information['TrackerPlugin']) + && $information['TrackerPlugin'] === true + ) { + $pluginsTracker = Piwik_Config::getInstance()->Plugins_Tracker['Plugins_Tracker']; + if (is_null($pluginsTracker)) { + $pluginsTracker = array(); + } + if (!in_array($pluginName, $pluginsTracker)) { + $pluginsTracker[] = $pluginName; + $this->updatePluginsTrackerConfig($pluginsTracker); + $saveConfig = true; + } + } + + if ($saveConfig) { + Piwik_Config::getInstance()->forceSave(); + } + } } /** @@ -698,41 +648,41 @@ class Piwik_PluginsManager */ class Piwik_PluginsManager_PluginException extends Exception { - function __construct($pluginName, $message) - { - parent::__construct("There was a problem installing the plugin ". $pluginName . ": " . $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[] = $pluginName" ); - } + PluginsInstalled[] = $pluginName"); + } } /** * Post an event to the dispatcher which will notice the observers * - * @param string $eventName The event name - * @param mixed $object Object, array or string that the listeners can read and/or modify. + * @param string $eventName The event name + * @param mixed $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 array $info Additional array of data that can be used by the listeners, but not edited - * @param bool $pending Should the notification be posted to plugins that register after the notification was sent? + * @param array $info Additional array of data that can be used by the listeners, but not edited + * @param bool $pending Should the notification be posted to plugins that register after the notification was sent? * @return void */ -function Piwik_PostEvent( $eventName, &$object = null, $info = array(), $pending = false ) +function Piwik_PostEvent($eventName, &$object = null, $info = array(), $pending = false) { - $notification = new Piwik_Event_Notification($object, $eventName, $info); - Piwik_PluginsManager::getInstance()->dispatcher->postNotification( $notification, $pending, $bubble = false ); + $notification = new Piwik_Event_Notification($object, $eventName, $info); + Piwik_PluginsManager::getInstance()->dispatcher->postNotification($notification, $pending, $bubble = false); } /** * Register an action to execute for a given event * - * @param string $hookName Name of event - * @param function $function Callback hook + * @param string $hookName Name of event + * @param function $function Callback hook */ -function Piwik_AddAction( $hookName, $function ) +function Piwik_AddAction($hookName, $function) { - Piwik_PluginsManager::getInstance()->dispatcher->addObserver( $function, $hookName ); + Piwik_PluginsManager::getInstance()->dispatcher->addObserver($function, $hookName); } /** @@ -745,27 +695,29 @@ function Piwik_AddAction( $hookName, $function ) */ class Piwik_Event_Notification extends Event_Notification { - static $showProfiler = false; - - /** - * Use notification counter to profile runtime execution - * time and memory usage. - */ - function increaseNotificationCount(/* array($className|object, $method) */) { - parent::increaseNotificationCount(); - if(self::$showProfiler && func_num_args() == 1) - { - $callback = func_get_arg(0); - if(is_array($callback)) { - $className = is_object($callback[0]) ? get_class($callback[0]) : $callback[0]; - $method = $callback[1]; - - echo "after $className -> $method <br />"; - echo "-"; Piwik::printTimer(); - echo "<br />"; - echo "-"; Piwik::printMemoryLeak(); - echo "<br />"; - } - } - } + static $showProfiler = false; + + /** + * Use notification counter to profile runtime execution + * time and memory usage. + */ + function increaseNotificationCount( /* array($className|object, $method) */) + { + parent::increaseNotificationCount(); + if (self::$showProfiler && func_num_args() == 1) { + $callback = func_get_arg(0); + if (is_array($callback)) { + $className = is_object($callback[0]) ? get_class($callback[0]) : $callback[0]; + $method = $callback[1]; + + echo "after $className -> $method <br />"; + echo "-"; + Piwik::printTimer(); + echo "<br />"; + echo "-"; + Piwik::printMemoryLeak(); + echo "<br />"; + } + } + } } diff --git a/core/ProxyHeaders.php b/core/ProxyHeaders.php index 14e1a7a0f0..5eb05b5076 100644 --- a/core/ProxyHeaders.php +++ b/core/ProxyHeaders.php @@ -16,85 +16,78 @@ */ class Piwik_ProxyHeaders { - /** - * Get protocol information, with the exception of HTTPS - * - * @return string protocol information - */ - public static function getProtocolInformation() - { - if(Piwik_Common::getRequestVar('clientProtocol', 'http', 'string') == 'https') - { - return 'https'; - } + /** + * Get protocol information, with the exception of HTTPS + * + * @return string protocol information + */ + public static function getProtocolInformation() + { + if (Piwik_Common::getRequestVar('clientProtocol', 'http', 'string') == 'https') { + return 'https'; + } - if(isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) - { - return 'SERVER_PORT=443'; - } + if (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) { + return 'SERVER_PORT=443'; + } - if(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') - { - return 'X-Forwarded-Proto'; - } + if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') { + return 'X-Forwarded-Proto'; + } - if(isset($_SERVER['HTTP_X_FORWARDED_SCHEME']) && strtolower($_SERVER['HTTP_X_FORWARDED_SCHEME']) == 'https') - { - return 'X-Forwarded-Scheme'; - } + if (isset($_SERVER['HTTP_X_FORWARDED_SCHEME']) && strtolower($_SERVER['HTTP_X_FORWARDED_SCHEME']) == 'https') { + return 'X-Forwarded-Scheme'; + } - if(isset($_SERVER['HTTP_X_URL_SCHEME']) && strtolower($_SERVER['HTTP_X_URL_SCHEME']) == 'https') - { - return 'X-Url-Scheme'; - } + if (isset($_SERVER['HTTP_X_URL_SCHEME']) && strtolower($_SERVER['HTTP_X_URL_SCHEME']) == 'https') { + return 'X-Url-Scheme'; + } - return null; - } + return null; + } - /** - * Get headers present in the HTTP request - * - * @param array $recognizedHeaders - * @return array HTTP headers - */ - private static function getHeaders($recognizedHeaders) - { - $headers = array(); + /** + * Get headers present in the HTTP request + * + * @param array $recognizedHeaders + * @return array HTTP headers + */ + private static function getHeaders($recognizedHeaders) + { + $headers = array(); - foreach($recognizedHeaders as $header) - { - if(isset($_SERVER[$header])) - { - $headers[] = $header; - } - } + foreach ($recognizedHeaders as $header) { + if (isset($_SERVER[$header])) { + $headers[] = $header; + } + } - return $headers; - } + return $headers; + } - /** - * Detect proxy client headers - * - * @return array Proxy client HTTP headers - */ - public static function getProxyClientHeaders() - { - return self::getHeaders(array( - 'HTTP_CF_CONNECTING_IP', - 'HTTP_CLIENT_IP', - 'HTTP_X_FORWARDED_FOR', - )); - } + /** + * Detect proxy client headers + * + * @return array Proxy client HTTP headers + */ + public static function getProxyClientHeaders() + { + return self::getHeaders(array( + 'HTTP_CF_CONNECTING_IP', + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + )); + } - /** - * Detect proxy host headers - * - * @return array Proxy host HTTP headers - */ - public static function getProxyHostHeaders() - { - return self::getHeaders(array( - 'HTTP_X_FORWARDED_HOST', - )); - } + /** + * Detect proxy host headers + * + * @return array Proxy host HTTP headers + */ + public static function getProxyHostHeaders() + { + return self::getHeaders(array( + 'HTTP_X_FORWARDED_HOST', + )); + } } diff --git a/core/QuickForm2.php b/core/QuickForm2.php index 63793c235e..a41f2e5eb9 100644 --- a/core/QuickForm2.php +++ b/core/QuickForm2.php @@ -1,135 +1,128 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Parent class for forms to be included in Smarty - * + * * For an example, @see Piwik_Login_FormLogin - * + * * @package Piwik - * @see HTML_QuickForm2, libs/HTML/QuickForm2.php + * @see HTML_QuickForm2, libs/HTML/QuickForm2.php * @link http://pear.php.net/package/HTML_QuickForm2/ */ abstract class Piwik_QuickForm2 extends HTML_QuickForm2 { - protected $a_formElements = array(); - - function __construct( $id, $method = 'post', $attributes = null, $trackSubmit = false) - { - if(!isset($attributes['action'])) - { - $attributes['action'] = Piwik_Url::getCurrentQueryString(); - } - if(!isset($attributes['name'])) - { - $attributes['name'] = $id; - } - parent::__construct($id, $method, $attributes, $trackSubmit); + protected $a_formElements = array(); - $this->init(); - } + function __construct($id, $method = 'post', $attributes = null, $trackSubmit = false) + { + if (!isset($attributes['action'])) { + $attributes['action'] = Piwik_Url::getCurrentQueryString(); + } + if (!isset($attributes['name'])) { + $attributes['name'] = $id; + } + parent::__construct($id, $method, $attributes, $trackSubmit); - /** - * Class specific initialization - */ - abstract function init(); + $this->init(); + } - /** - * The elements in this form - * - * @return array Element names - */ - public function getElementList() - { - return $this->a_formElements; - } + /** + * Class specific initialization + */ + abstract function init(); - /** - * Wrapper around HTML_QuickForm2_Container's addElement() - * - * @param string|HTML_QuickForm2_Node Either type name (treated - * case-insensitively) or an element instance - * @param mixed Element name - * @param mixed Element attributes - * @param array Element-specific data - * @return HTML_QuickForm2_Node Added element - * @throws HTML_QuickForm2_InvalidArgumentException - * @throws HTML_QuickForm2_NotFoundException - */ + /** + * The elements in this form + * + * @return array Element names + */ + public function getElementList() + { + return $this->a_formElements; + } + + /** + * Wrapper around HTML_QuickForm2_Container's addElement() + * + * @param string|HTML_QuickForm2_Node Either type name (treated + * case-insensitively) or an element instance + * @param mixed Element name + * @param mixed Element attributes + * @param array Element-specific data + * @return HTML_QuickForm2_Node Added element + * @throws HTML_QuickForm2_InvalidArgumentException + * @throws HTML_QuickForm2_NotFoundException + */ public function addElement($elementOrType, $name = null, $attributes = null, array $data = array()) - { - if($name != 'submit') - { - $this->a_formElements[] = $name; - } + { + if ($name != 'submit') { + $this->a_formElements[] = $name; + } + + return parent::addElement($elementOrType, $name, $attributes, $data); + } + + function setChecked($nameElement) + { + foreach ($this->_elements as $key => $value) { + if ($value->_attributes['name'] == $nameElement) { + $this->_elements[$key]->_attributes['checked'] = 'checked'; + } + } + } + + function setSelected($nameElement, $value) + { + foreach ($this->_elements as $key => $value) { + if ($value->_attributes['name'] == $nameElement) { + $this->_elements[$key]->_attributes['selected'] = 'selected'; + } + } + } + + /** + * Ported from HTML_QuickForm to minimize changes to Controllers + * + * @param string $elementName + * @return mixed + */ + function getSubmitValue($elementName) + { + $value = $this->getValue(); + return isset($value[$elementName]) ? $value[$elementName] : null; + } + + /** + * Returns the rendered form as an array. + * + * @param bool $groupErrors Whether to group errors together or not. + * @return array + */ + public function getFormData($groupErrors = true) + { + static $registered = false; + if (!$registered) { + HTML_QuickForm2_Renderer::register('smarty', 'HTML_QuickForm2_Renderer_Smarty'); + $registered = true; + } - return parent::addElement($elementOrType, $name, $attributes, $data); - } - - function setChecked( $nameElement ) - { - foreach( $this->_elements as $key => $value) - { - if($value->_attributes['name'] == $nameElement) - { - $this->_elements[$key]->_attributes['checked'] = 'checked'; - } - } - } - function setSelected( $nameElement, $value ) - { - foreach( $this->_elements as $key => $value) - { - if($value->_attributes['name'] == $nameElement) - { - $this->_elements[$key]->_attributes['selected'] = 'selected'; - } - } - } + // Create the renderer object + $renderer = HTML_QuickForm2_Renderer::factory('smarty'); + $renderer->setOption('group_errors', $groupErrors); - /** - * Ported from HTML_QuickForm to minimize changes to Controllers - * - * @param string $elementName - * @return mixed - */ - function getSubmitValue($elementName) - { - $value = $this->getValue(); - return isset($value[$elementName]) ? $value[$elementName] : null; - } - - /** - * Returns the rendered form as an array. - * - * @param bool $groupErrors Whether to group errors together or not. - * @return array - */ - public function getFormData( $groupErrors = true ) - { - static $registered = false; - if(!$registered) - { - HTML_QuickForm2_Renderer::register('smarty', 'HTML_QuickForm2_Renderer_Smarty'); - $registered = true; - } - - // Create the renderer object - $renderer = HTML_QuickForm2_Renderer::factory('smarty'); - $renderer->setOption('group_errors', $groupErrors); + // build the HTML for the form + $this->render($renderer); - // build the HTML for the form - $this->render($renderer); - - return $renderer->toArray(); - } + return $renderer->toArray(); + } } diff --git a/core/RankingQuery.php b/core/RankingQuery.php index 2c1fe13c12..147f58e1bf 100644 --- a/core/RankingQuery.php +++ b/core/RankingQuery.php @@ -10,324 +10,297 @@ */ /** - * The ranking query class wraps an arbitrary SQL query with more SQL that limits - * the number of results while grouping the rest to "Others" and allows for some + * The ranking query class wraps an arbitrary SQL query with more SQL that limits + * the number of results while grouping the rest to "Others" and allows for some * more fancy things that can be configured via method calls of this class. The * advanced use cases are explained in the doc comments of the methods. - * + * * The general use case looks like this: - * + * * // limit to 500 rows + "Others" * $rankingQuery = new Piwik_RankingQuery(500); - * + * * // idaction_url will be "Others" in the row that contains the aggregated rest * $rankingQuery->addLabelColumn('idaction_url'); - * + * * // the actual query. it's important to sort it before the limit is applied * $sql = 'SELECT idaction_url, COUNT(*) AS nb_hits * FROM log_link_visit_action * GROUP BY idaction_url * ORDER BY nb_hits DESC'; - * + * * // execute the query * $rankingQuery->execute($sql); - * - * + * + * * For more examples, see RankingQueryTest.php - * - * + * + * * @package Piwik */ class Piwik_RankingQuery { - - /** - * Contains the labels of the inner query. - * Format: "label" => true (to make sure labels don't appear twice) - * @var array - */ - private $labelColumns = array(); - - /** - * The columns of the inner query that are not labels - * Format: "label" => "aggregation function" or false for no aggregation - * @var array - */ - private $additionalColumns = array(); - - /** - * The limit for each group - * @var int - */ - private $limit = 5; - - /** - * The name of the columns that marks rows to be excluded from the limit - * @var string - */ - private $columnToMarkExcludedRows = false; - - /** - * The column that is used to partition the result - * @var bool|string - */ - private $partitionColumn = false; - - /** - * The possible values for the column $this->partitionColumn - * @var array - */ - private $partitionColumnValues = array(); - - /** - * The value to use in the label of the 'Others' row. - * @var string - */ - private $othersLabelValue = 'Others'; - - /** - * The constructor. - * Can be used as a shortcut for setLimit() - */ - public function __construct($limit = false) - { - if ($limit !== false) - { - $this->setLimit($limit); - } - } - - /** - * Set the limit after which everything is grouped to "Others" - * - * @param $limit int - */ - public function setLimit($limit) - { - $this->limit = $limit; - } - - /** - * Set the value to use for the label in the 'Others' row. - * - * @param $value string - */ - public function setOthersLabel($value) - { - $this->othersLabelValue = $value; - } - - /** - * Add a label column. - * Labels are the columns that are replaced with "Others" after the limit. - * - * @param $labelColumn string|array - */ - public function addLabelColumn($labelColumn) - { - if (is_array($labelColumn)) - { - foreach ($labelColumn as $label) - { - $this->addLabelColumn($label); - } - return; - } - $this->labelColumns[$labelColumn] = true; - } - - /** - * Add a column that has be added to the outer queries. - * - * @param $column - * @param string|bool $aggregationFunction string - * If set, this function is used to aggregate the values of "Others" - */ - public function addColumn($column, $aggregationFunction=false) - { - if (is_array($column)) - { - foreach ($column as $c) - { - $this->addColumn($c, $aggregationFunction); - } - return; - } - $this->additionalColumns[$column] = $aggregationFunction; - } - - /** - * The inner query can have a column that marks the rows that shall be excluded from limiting. - * If the column contains 0, rows are handled as usual. For values greater than 0, separate - * groups are made. If this method is used, generate() returns both the regular result and - * the excluded columns separately. - * - * @param $column string name of the column - * @throws Exception when method is used more than once - */ - public function setColumnToMarkExcludedRows($column) - { - if ($this->columnToMarkExcludedRows !== false) - { - throw new Exception("setColumnToMarkExcludedRows can only be used once"); - } - - $this->columnToMarkExcludedRows = $column; - $this->addColumn($this->columnToMarkExcludedRows); - } - - /** - * This method can be used to get multiple groups in one go. For example, one might query - * the top following pages, outlinks and downloads in one go by using log_action.type as - * the partition column and [TYPE_ACTION_URL, TYPE_OUTLINK, TYPE_DOWNLOAD] as the possible - * values. - * When this method has been used, generate() returns as array that contains one array - * per group of data. - * - * @param $partitionColumn string - * @param $possibleValues array of integers - * @throws Exception when method is used more than once - */ - public function partitionResultIntoMultipleGroups($partitionColumn, $possibleValues) - { - if ($this->partitionColumn !== false) - { - throw new Exception("partitionResultIntoMultipleGroups can only be used once"); - } - - $this->partitionColumn = $partitionColumn; - $this->partitionColumnValues = $possibleValues; - $this->addColumn($partitionColumn); - } - - /** - * Execute the query. - * The object has to be configured first using the other methods. - * - * @param $innerQuery string The "payload" query. The result has be sorted as desired. - * @param $bind array Bindings for the inner query. - * @return array The format depends on which methods have been used - * to configure the ranking query - */ - public function execute($innerQuery, $bind=array()) - { - $query = $this->generateQuery($innerQuery); - $data = Piwik_FetchAll($query, $bind); - - if ($this->columnToMarkExcludedRows !== false) - { - // split the result into the regular result and the rows with special treatment - $excludedFromLimit = array(); - $result = array(); - foreach ($data as &$row) - { - if ($row[$this->columnToMarkExcludedRows] != 0) - { - $excludedFromLimit[] = $row; - } - else - { - $result[] = $row; - } - } - $data = array( - 'result' => &$result, - 'excludedFromLimit' => &$excludedFromLimit - ); - } - - if ($this->partitionColumn !== false) - { - if ($this->columnToMarkExcludedRows !== false) - { - $data['result'] = $this->splitPartitions($data['result']); - } - else - { - $data = $this->splitPartitions($data); - } - } - - return $data; - } - - private function splitPartitions(&$data) - { - $result = array(); - foreach ($data as &$row) - { - $partition = $row[$this->partitionColumn]; - if (!isset($result[$partition])) - { - $result[$partition] = array(); - } - $result[$partition][] = &$row; - } - return $result; - } - - /** - * Generate the SQL code that does the magic. - * If you want to get the result, use execute() instead. If you're interested in - * the generated SQL code (e.g. for debugging), use this method. - * - * @param $innerQuery string SQL of the actual query - * @return string entire ranking query SQL - */ - public function generateQuery($innerQuery) - { - // +1 to include "Others" - $limit = $this->limit + 1; - $counterExpression = $this->getCounterExpression($limit); - - // generate select clauses for label columns - $labelColumnsString = '`'.implode('`, `', array_keys($this->labelColumns)).'`'; - $labelColumnsOthersSwitch = array(); - foreach ($this->labelColumns as $column => $true) - { - $labelColumnsOthersSwitch[] = " + + /** + * Contains the labels of the inner query. + * Format: "label" => true (to make sure labels don't appear twice) + * @var array + */ + private $labelColumns = array(); + + /** + * The columns of the inner query that are not labels + * Format: "label" => "aggregation function" or false for no aggregation + * @var array + */ + private $additionalColumns = array(); + + /** + * The limit for each group + * @var int + */ + private $limit = 5; + + /** + * The name of the columns that marks rows to be excluded from the limit + * @var string + */ + private $columnToMarkExcludedRows = false; + + /** + * The column that is used to partition the result + * @var bool|string + */ + private $partitionColumn = false; + + /** + * The possible values for the column $this->partitionColumn + * @var array + */ + private $partitionColumnValues = array(); + + /** + * The value to use in the label of the 'Others' row. + * @var string + */ + private $othersLabelValue = 'Others'; + + /** + * The constructor. + * Can be used as a shortcut for setLimit() + */ + public function __construct($limit = false) + { + if ($limit !== false) { + $this->setLimit($limit); + } + } + + /** + * Set the limit after which everything is grouped to "Others" + * + * @param $limit int + */ + public function setLimit($limit) + { + $this->limit = $limit; + } + + /** + * Set the value to use for the label in the 'Others' row. + * + * @param $value string + */ + public function setOthersLabel($value) + { + $this->othersLabelValue = $value; + } + + /** + * Add a label column. + * Labels are the columns that are replaced with "Others" after the limit. + * + * @param $labelColumn string|array + */ + public function addLabelColumn($labelColumn) + { + if (is_array($labelColumn)) { + foreach ($labelColumn as $label) { + $this->addLabelColumn($label); + } + return; + } + $this->labelColumns[$labelColumn] = true; + } + + /** + * Add a column that has be added to the outer queries. + * + * @param $column + * @param string|bool $aggregationFunction string + * If set, this function is used to aggregate the values of "Others" + */ + public function addColumn($column, $aggregationFunction = false) + { + if (is_array($column)) { + foreach ($column as $c) { + $this->addColumn($c, $aggregationFunction); + } + return; + } + $this->additionalColumns[$column] = $aggregationFunction; + } + + /** + * The inner query can have a column that marks the rows that shall be excluded from limiting. + * If the column contains 0, rows are handled as usual. For values greater than 0, separate + * groups are made. If this method is used, generate() returns both the regular result and + * the excluded columns separately. + * + * @param $column string name of the column + * @throws Exception when method is used more than once + */ + public function setColumnToMarkExcludedRows($column) + { + if ($this->columnToMarkExcludedRows !== false) { + throw new Exception("setColumnToMarkExcludedRows can only be used once"); + } + + $this->columnToMarkExcludedRows = $column; + $this->addColumn($this->columnToMarkExcludedRows); + } + + /** + * This method can be used to get multiple groups in one go. For example, one might query + * the top following pages, outlinks and downloads in one go by using log_action.type as + * the partition column and [TYPE_ACTION_URL, TYPE_OUTLINK, TYPE_DOWNLOAD] as the possible + * values. + * When this method has been used, generate() returns as array that contains one array + * per group of data. + * + * @param $partitionColumn string + * @param $possibleValues array of integers + * @throws Exception when method is used more than once + */ + public function partitionResultIntoMultipleGroups($partitionColumn, $possibleValues) + { + if ($this->partitionColumn !== false) { + throw new Exception("partitionResultIntoMultipleGroups can only be used once"); + } + + $this->partitionColumn = $partitionColumn; + $this->partitionColumnValues = $possibleValues; + $this->addColumn($partitionColumn); + } + + /** + * Execute the query. + * The object has to be configured first using the other methods. + * + * @param $innerQuery string The "payload" query. The result has be sorted as desired. + * @param $bind array Bindings for the inner query. + * @return array The format depends on which methods have been used + * to configure the ranking query + */ + public function execute($innerQuery, $bind = array()) + { + $query = $this->generateQuery($innerQuery); + $data = Piwik_FetchAll($query, $bind); + + if ($this->columnToMarkExcludedRows !== false) { + // split the result into the regular result and the rows with special treatment + $excludedFromLimit = array(); + $result = array(); + foreach ($data as &$row) { + if ($row[$this->columnToMarkExcludedRows] != 0) { + $excludedFromLimit[] = $row; + } else { + $result[] = $row; + } + } + $data = array( + 'result' => &$result, + 'excludedFromLimit' => &$excludedFromLimit + ); + } + + if ($this->partitionColumn !== false) { + if ($this->columnToMarkExcludedRows !== false) { + $data['result'] = $this->splitPartitions($data['result']); + } else { + $data = $this->splitPartitions($data); + } + } + + return $data; + } + + private function splitPartitions(&$data) + { + $result = array(); + foreach ($data as &$row) { + $partition = $row[$this->partitionColumn]; + if (!isset($result[$partition])) { + $result[$partition] = array(); + } + $result[$partition][] = & $row; + } + return $result; + } + + /** + * Generate the SQL code that does the magic. + * If you want to get the result, use execute() instead. If you're interested in + * the generated SQL code (e.g. for debugging), use this method. + * + * @param $innerQuery string SQL of the actual query + * @return string entire ranking query SQL + */ + public function generateQuery($innerQuery) + { + // +1 to include "Others" + $limit = $this->limit + 1; + $counterExpression = $this->getCounterExpression($limit); + + // generate select clauses for label columns + $labelColumnsString = '`' . implode('`, `', array_keys($this->labelColumns)) . '`'; + $labelColumnsOthersSwitch = array(); + foreach ($this->labelColumns as $column => $true) { + $labelColumnsOthersSwitch[] = " CASE - WHEN counter = $limit THEN '".$this->othersLabelValue."' + WHEN counter = $limit THEN '" . $this->othersLabelValue . "' ELSE `$column` END AS `$column` "; - } - $labelColumnsOthersSwitch = implode(', ', $labelColumnsOthersSwitch); - - // generate select clauses for additional columns - $additionalColumnsString = ''; - $additionalColumnsAggregatedString = ''; - foreach ($this->additionalColumns as $additionalColumn => $aggregation) - { - $additionalColumnsString .= ', `'.$additionalColumn.'`'; - if ($aggregation !== false) - { - $additionalColumnsAggregatedString .= ', '.$aggregation.'(`'.$additionalColumn.'`) AS `'.$additionalColumn.'`'; - } - else - { - $additionalColumnsAggregatedString .= ', `'.$additionalColumn.'`'; - } - - } - - // initialize the counters - if ($this->partitionColumn !== false) - { - $initCounter = ''; - foreach ($this->partitionColumnValues as $value) - { - $initCounter .= '( SELECT @counter'.intval($value).':=0 ) initCounter'.intval($value).', '; - } - } - else - { - $initCounter = '( SELECT @counter:=0 ) initCounter,'; - } - - // add a counter to the query - // we rely on the sorting of the inner query - $withCounter = " + } + $labelColumnsOthersSwitch = implode(', ', $labelColumnsOthersSwitch); + + // generate select clauses for additional columns + $additionalColumnsString = ''; + $additionalColumnsAggregatedString = ''; + foreach ($this->additionalColumns as $additionalColumn => $aggregation) { + $additionalColumnsString .= ', `' . $additionalColumn . '`'; + if ($aggregation !== false) { + $additionalColumnsAggregatedString .= ', ' . $aggregation . '(`' . $additionalColumn . '`) AS `' . $additionalColumn . '`'; + } else { + $additionalColumnsAggregatedString .= ', `' . $additionalColumn . '`'; + } + + } + + // initialize the counters + if ($this->partitionColumn !== false) { + $initCounter = ''; + foreach ($this->partitionColumnValues as $value) { + $initCounter .= '( SELECT @counter' . intval($value) . ':=0 ) initCounter' . intval($value) . ', '; + } + } else { + $initCounter = '( SELECT @counter:=0 ) initCounter,'; + } + + // add a counter to the query + // we rely on the sorting of the inner query + $withCounter = " SELECT $labelColumnsString, $counterExpression AS counter @@ -336,61 +309,55 @@ class Piwik_RankingQuery $initCounter ( $innerQuery ) actualQuery "; - - // group by the counter - this groups "Others" because the counter stops at $limit - $groupBy = 'counter'; - if ($this->partitionColumn !== false) - { - $groupBy .= ', `'.$this->partitionColumn.'`'; - } - $groupOthers = " + + // group by the counter - this groups "Others" because the counter stops at $limit + $groupBy = 'counter'; + if ($this->partitionColumn !== false) { + $groupBy .= ', `' . $this->partitionColumn . '`'; + } + $groupOthers = " SELECT $labelColumnsOthersSwitch $additionalColumnsAggregatedString FROM ( $withCounter ) AS withCounter GROUP BY $groupBy "; - return $groupOthers; - } - - private function getCounterExpression($limit) - { - $whens = array(); - - if ($this->columnToMarkExcludedRows !== false) - { - // when a row has been specified that marks which records should be excluded - // from limiting, we don't give those rows the normal counter but -1 times the - // value they had before. this way, they have a separate number space (i.e. negative - // integers). - $whens[] = "WHEN {$this->columnToMarkExcludedRows} != 0 THEN -1 * {$this->columnToMarkExcludedRows}"; - } - - if ($this->partitionColumn !== false) - { - // partition: one counter per possible value - foreach ($this->partitionColumnValues as $value) - { - $isValue = '`'.$this->partitionColumn.'` = '.intval($value); - $counter = '@counter'.intval($value); - $whens[] = "WHEN $isValue AND $counter = $limit THEN $limit"; - $whens[] = "WHEN $isValue THEN $counter:=$counter+1"; - } - $whens[] = "ELSE 0"; - } - else - { - // no partitioning: add a single counter - $whens[] = "WHEN @counter = $limit THEN $limit"; - $whens[] = "ELSE @counter:=@counter+1"; - } - - return " + return $groupOthers; + } + + private function getCounterExpression($limit) + { + $whens = array(); + + if ($this->columnToMarkExcludedRows !== false) { + // when a row has been specified that marks which records should be excluded + // from limiting, we don't give those rows the normal counter but -1 times the + // value they had before. this way, they have a separate number space (i.e. negative + // integers). + $whens[] = "WHEN {$this->columnToMarkExcludedRows} != 0 THEN -1 * {$this->columnToMarkExcludedRows}"; + } + + if ($this->partitionColumn !== false) { + // partition: one counter per possible value + foreach ($this->partitionColumnValues as $value) { + $isValue = '`' . $this->partitionColumn . '` = ' . intval($value); + $counter = '@counter' . intval($value); + $whens[] = "WHEN $isValue AND $counter = $limit THEN $limit"; + $whens[] = "WHEN $isValue THEN $counter:=$counter+1"; + } + $whens[] = "ELSE 0"; + } else { + // no partitioning: add a single counter + $whens[] = "WHEN @counter = $limit THEN $limit"; + $whens[] = "ELSE @counter:=@counter+1"; + } + + return " CASE - ".implode(" - ", $whens)." + " . implode(" + ", $whens) . " END "; - } + } } diff --git a/core/ReportRenderer.php b/core/ReportRenderer.php index 7a94305047..9b26b7f236 100644 --- a/core/ReportRenderer.php +++ b/core/ReportRenderer.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -18,238 +18,235 @@ */ abstract class Piwik_ReportRenderer { - const DEFAULT_REPORT_FONT = 'dejavusans'; - const REPORT_TEXT_COLOR = "68,68,68"; - const REPORT_TITLE_TEXT_COLOR = "126,115,99"; - const TABLE_HEADER_BG_COLOR = "228,226,215"; - const TABLE_HEADER_TEXT_COLOR = "37,87,146"; - const TABLE_CELL_BORDER_COLOR = "231,231,231"; - const TABLE_BG_COLOR = "249,250,250"; - - const HTML_FORMAT = 'html'; - const PDF_FORMAT = 'pdf'; - - static private $availableReportRenderers = array( - self::PDF_FORMAT, - self::HTML_FORMAT, - ); - - /** - * Return the ReportRenderer associated to the renderer type $rendererType - * - * @throws exception If the renderer is unknown - * @param string $rendererType - * @return Piwik_ReportRenderer - */ - static public function factory($rendererType) - { - $name = ucfirst(strtolower($rendererType)); - $className = 'Piwik_ReportRenderer_' . $name; - - try { - Piwik_Loader::loadClass($className); - return new $className; - } catch(Exception $e) { - - @header('Content-Type: text/html; charset=utf-8'); - - throw new Exception( - Piwik_TranslateException( - 'General_ExceptionInvalidReportRendererFormat', - array($name, implode(', ', self::$availableReportRenderers)) - ) - ); - } - } - - /** - * Initialize locale settings. - * If not called, locale settings defaults to 'en' - * - * @param string $locale - */ - abstract public function setLocale($locale); - - /** - * Save rendering to disk - * - * @param string $filename without path & without format extension - * @return string path of file - */ - abstract public function sendToDisk($filename); - - /** - * Send rendering to browser with a 'download file' prompt - * - * @param string $filename without path & without format extension - */ - abstract public function sendToBrowserDownload($filename); - - /** - * Output rendering to browser - * - * @param string $filename without path & without format extension - */ - abstract public function sendToBrowserInline($filename); - - /** - * Get rendered report - */ - abstract public function getRenderedReport(); - - /** - * Generate the first page. - * - * @param string $websiteName - * @param string $prettyDate formatted date - * @param string $description - * @param array $reportMetadata metadata for all reports - */ - abstract public function renderFrontPage($websiteName, $prettyDate, $description, $reportMetadata); - - /** - * Render the provided report. - * Multiple calls to this method before calling outputRendering appends each report content. - * - * @param array $processedReport @see Piwik_API_API::getProcessedReport() - */ - abstract public function renderReport($processedReport); - - /** - * Append $extension to $filename - * - * @static - * @param $filename - * @param $extension - * @return filename with extension - */ - protected static function appendExtension($filename, $extension) - { - return $filename.".".$extension; - } - - /** - * Return $filename with temp directory and delete file - * - * @static - * @param $filename - * @return string path of file in temp directory - */ - protected static function getOutputPath($filename) - { - $outputFilename = PIWIK_USER_PATH . '/tmp/assets/' . $filename; - @chmod($outputFilename, 0600); - @unlink($outputFilename); - return $outputFilename; - } - - protected static function writeFile($filename, $extension, $content) - { - $filename = self::appendExtension($filename, $extension); - $outputFilename = self::getOutputPath($filename); - - $emailReport = @fopen($outputFilename, "w"); - - if (!$emailReport) { - throw new Exception ("The file : " . $outputFilename . " can not be opened in write mode."); - } - - fwrite($emailReport, $content); - fclose($emailReport); - - return $outputFilename; - } - - protected static function sendToBrowser($filename, $extension, $contentType, $content) - { - $filename = Piwik_ReportRenderer::appendExtension($filename, $extension); - - Piwik::overrideCacheControlHeaders(); - header('Content-Description: File Transfer'); - header('Content-Type: ' . $contentType); - header('Content-Disposition: attachment; filename="'.str_replace('"', '\'', basename($filename)).'";'); - header('Content-Length: '.strlen($content)); - - echo $content; - } - - protected static function inlineToBrowser($contentType, $content) - { - header('Content-Type: ' . $contentType); - echo $content; - } - - /** - * Convert a dimension-less report to a multi-row two-column data table - * - * @static - * @param $reportMetadata array - * @param $report Piwik_DataTable - * @param $reportColumns array - * @return array Piwik_DataTable $report & array $columns - */ - protected static function processTableFormat($reportMetadata, $report, $reportColumns) - { - $finalReport = $report; - if(empty($reportMetadata['dimension'])) - { - $simpleReportMetrics = $report->getFirstRow(); - if($simpleReportMetrics) - { - $finalReport = new Piwik_DataTable_Simple(); - foreach($simpleReportMetrics->getColumns() as $metricId => $metric) - { - $newRow = new Piwik_DataTable_Row(); - $newRow->addColumn("label", $reportColumns[$metricId]); - $newRow->addColumn("value", $metric); - $finalReport->addRow($newRow); - } - } - - $reportColumns = array( - 'label' => Piwik_Translate('General_Name'), - 'value' => Piwik_Translate('General_Value'), - ); - } - - return array( - $finalReport, - $reportColumns, - ); - } - - public static function getStaticGraph($reportMetadata, $width, $height, $evolution) { - - $imageGraphUrl = $reportMetadata['imageGraphUrl']; - - if($evolution && !empty($reportMetadata['imageGraphEvolutionUrl'])) - { - $imageGraphUrl = $reportMetadata['imageGraphEvolutionUrl']; - } - - $request = new Piwik_API_Request( - $imageGraphUrl . - '&outputType='.Piwik_ImageGraph_API::GRAPH_OUTPUT_PHP. - '&format=original&serialize=0'. - '&filter_truncate='. - '&width='.$width. - '&height='.$height - ); - - try { - $imageGraph = $request->process(); - - // Get image data as string - ob_start(); - imagepng($imageGraph); - $imageGraphData = ob_get_contents(); - ob_end_clean(); - imagedestroy($imageGraph); - - return $imageGraphData; - - } catch(Exception $e) { - throw new Exception("ImageGraph API returned an error: ".$e->getMessage()."\n"); - } - } + const DEFAULT_REPORT_FONT = 'dejavusans'; + const REPORT_TEXT_COLOR = "68,68,68"; + const REPORT_TITLE_TEXT_COLOR = "126,115,99"; + const TABLE_HEADER_BG_COLOR = "228,226,215"; + const TABLE_HEADER_TEXT_COLOR = "37,87,146"; + const TABLE_CELL_BORDER_COLOR = "231,231,231"; + const TABLE_BG_COLOR = "249,250,250"; + + const HTML_FORMAT = 'html'; + const PDF_FORMAT = 'pdf'; + + static private $availableReportRenderers = array( + self::PDF_FORMAT, + self::HTML_FORMAT, + ); + + /** + * Return the ReportRenderer associated to the renderer type $rendererType + * + * @throws exception If the renderer is unknown + * @param string $rendererType + * @return Piwik_ReportRenderer + */ + static public function factory($rendererType) + { + $name = ucfirst(strtolower($rendererType)); + $className = 'Piwik_ReportRenderer_' . $name; + + try { + Piwik_Loader::loadClass($className); + return new $className; + } catch (Exception $e) { + + @header('Content-Type: text/html; charset=utf-8'); + + throw new Exception( + Piwik_TranslateException( + 'General_ExceptionInvalidReportRendererFormat', + array($name, implode(', ', self::$availableReportRenderers)) + ) + ); + } + } + + /** + * Initialize locale settings. + * If not called, locale settings defaults to 'en' + * + * @param string $locale + */ + abstract public function setLocale($locale); + + /** + * Save rendering to disk + * + * @param string $filename without path & without format extension + * @return string path of file + */ + abstract public function sendToDisk($filename); + + /** + * Send rendering to browser with a 'download file' prompt + * + * @param string $filename without path & without format extension + */ + abstract public function sendToBrowserDownload($filename); + + /** + * Output rendering to browser + * + * @param string $filename without path & without format extension + */ + abstract public function sendToBrowserInline($filename); + + /** + * Get rendered report + */ + abstract public function getRenderedReport(); + + /** + * Generate the first page. + * + * @param string $websiteName + * @param string $prettyDate formatted date + * @param string $description + * @param array $reportMetadata metadata for all reports + */ + abstract public function renderFrontPage($websiteName, $prettyDate, $description, $reportMetadata); + + /** + * Render the provided report. + * Multiple calls to this method before calling outputRendering appends each report content. + * + * @param array $processedReport @see Piwik_API_API::getProcessedReport() + */ + abstract public function renderReport($processedReport); + + /** + * Append $extension to $filename + * + * @static + * @param $filename + * @param $extension + * @return filename with extension + */ + protected static function appendExtension($filename, $extension) + { + return $filename . "." . $extension; + } + + /** + * Return $filename with temp directory and delete file + * + * @static + * @param $filename + * @return string path of file in temp directory + */ + protected static function getOutputPath($filename) + { + $outputFilename = PIWIK_USER_PATH . '/tmp/assets/' . $filename; + @chmod($outputFilename, 0600); + @unlink($outputFilename); + return $outputFilename; + } + + protected static function writeFile($filename, $extension, $content) + { + $filename = self::appendExtension($filename, $extension); + $outputFilename = self::getOutputPath($filename); + + $emailReport = @fopen($outputFilename, "w"); + + if (!$emailReport) { + throw new Exception ("The file : " . $outputFilename . " can not be opened in write mode."); + } + + fwrite($emailReport, $content); + fclose($emailReport); + + return $outputFilename; + } + + protected static function sendToBrowser($filename, $extension, $contentType, $content) + { + $filename = Piwik_ReportRenderer::appendExtension($filename, $extension); + + Piwik::overrideCacheControlHeaders(); + header('Content-Description: File Transfer'); + header('Content-Type: ' . $contentType); + header('Content-Disposition: attachment; filename="' . str_replace('"', '\'', basename($filename)) . '";'); + header('Content-Length: ' . strlen($content)); + + echo $content; + } + + protected static function inlineToBrowser($contentType, $content) + { + header('Content-Type: ' . $contentType); + echo $content; + } + + /** + * Convert a dimension-less report to a multi-row two-column data table + * + * @static + * @param $reportMetadata array + * @param $report Piwik_DataTable + * @param $reportColumns array + * @return array Piwik_DataTable $report & array $columns + */ + protected static function processTableFormat($reportMetadata, $report, $reportColumns) + { + $finalReport = $report; + if (empty($reportMetadata['dimension'])) { + $simpleReportMetrics = $report->getFirstRow(); + if ($simpleReportMetrics) { + $finalReport = new Piwik_DataTable_Simple(); + foreach ($simpleReportMetrics->getColumns() as $metricId => $metric) { + $newRow = new Piwik_DataTable_Row(); + $newRow->addColumn("label", $reportColumns[$metricId]); + $newRow->addColumn("value", $metric); + $finalReport->addRow($newRow); + } + } + + $reportColumns = array( + 'label' => Piwik_Translate('General_Name'), + 'value' => Piwik_Translate('General_Value'), + ); + } + + return array( + $finalReport, + $reportColumns, + ); + } + + public static function getStaticGraph($reportMetadata, $width, $height, $evolution) + { + + $imageGraphUrl = $reportMetadata['imageGraphUrl']; + + if ($evolution && !empty($reportMetadata['imageGraphEvolutionUrl'])) { + $imageGraphUrl = $reportMetadata['imageGraphEvolutionUrl']; + } + + $request = new Piwik_API_Request( + $imageGraphUrl . + '&outputType=' . Piwik_ImageGraph_API::GRAPH_OUTPUT_PHP . + '&format=original&serialize=0' . + '&filter_truncate=' . + '&width=' . $width . + '&height=' . $height + ); + + try { + $imageGraph = $request->process(); + + // Get image data as string + ob_start(); + imagepng($imageGraph); + $imageGraphData = ob_get_contents(); + ob_end_clean(); + imagedestroy($imageGraph); + + return $imageGraphData; + + } catch (Exception $e) { + throw new Exception("ImageGraph API returned an error: " . $e->getMessage() . "\n"); + } + } } diff --git a/core/ReportRenderer/Html.php b/core/ReportRenderer/Html.php index 1bf56eb341..45c14bffa4 100644 --- a/core/ReportRenderer/Html.php +++ b/core/ReportRenderer/Html.php @@ -16,141 +16,139 @@ */ class Piwik_ReportRenderer_Html extends Piwik_ReportRenderer { - const IMAGE_GRAPH_WIDTH = 700; - const IMAGE_GRAPH_HEIGHT = 200; - - const REPORT_TITLE_TEXT_SIZE = 11; - const REPORT_TABLE_HEADER_TEXT_SIZE = 11; - const REPORT_TABLE_ROW_TEXT_SIZE = 11; - const REPORT_BACK_TO_TOP_TEXT_SIZE = 9; - - const HTML_CONTENT_TYPE = 'text/html'; - const HTML_FILE_EXTENSION = 'html'; - - protected $renderImageInline = false; - - private $rendering = ""; - - public function setLocale($locale) - { - //Nothing to do - } - - /** - * Currently only used for HTML reports. - * When sent by mail, images are attached to the mail: renderImageInline = false - * When downloaded, images are included base64 encoded in the report body: renderImageInline = true - * - * @param boolean $renderImageInline - */ - public function setRenderImageInline($renderImageInline) - { - $this->renderImageInline = $renderImageInline; - } - - public function sendToDisk($filename) - { - $this->epilogue(); - - return Piwik_ReportRenderer::writeFile($filename, self::HTML_FILE_EXTENSION, $this->rendering); - } - - public function sendToBrowserDownload($filename) - { - $this->epilogue(); - - Piwik_ReportRenderer::sendToBrowser($filename, self::HTML_FILE_EXTENSION, self::HTML_CONTENT_TYPE, $this->rendering); - } - - public function sendToBrowserInline($filename) - { - $this->epilogue(); - - Piwik_ReportRenderer::inlineToBrowser(self::HTML_CONTENT_TYPE, $this->rendering); - } - - public function getRenderedReport() - { - $this->epilogue(); - - return $this->rendering; - } - - private function epilogue() - { - $smarty = new Piwik_Smarty(); - $this->rendering .= $smarty->fetch(self::prefixTemplatePath("html_report_footer.tpl")); - } - - public function renderFrontPage($websiteName, $prettyDate, $description, $reportMetadata) - { - $smarty = new Piwik_Smarty(); - $this->assignCommonParameters($smarty); - - $smarty->assign("websiteName", $websiteName); - $smarty->assign("prettyDate", $prettyDate); - $smarty->assign("description", $description); - $smarty->assign("reportMetadata", $reportMetadata); - - $this->rendering .= $smarty->fetch(self::prefixTemplatePath("html_report_header.tpl")); - } - - private function assignCommonParameters($smarty) - { - $smarty->assign("reportTitleTextColor", Piwik_ReportRenderer::REPORT_TITLE_TEXT_COLOR); - $smarty->assign("reportTitleTextSize", self::REPORT_TITLE_TEXT_SIZE); - $smarty->assign("reportTextColor", Piwik_ReportRenderer::REPORT_TEXT_COLOR); - $smarty->assign("tableHeaderBgColor", Piwik_ReportRenderer::TABLE_HEADER_BG_COLOR); - $smarty->assign("tableHeaderTextColor", Piwik_ReportRenderer::TABLE_HEADER_TEXT_COLOR); - $smarty->assign("tableCellBorderColor", Piwik_ReportRenderer::TABLE_CELL_BORDER_COLOR); - $smarty->assign("tableBgColor", Piwik_ReportRenderer::TABLE_BG_COLOR); - $smarty->assign("reportTableHeaderTextSize", self::REPORT_TABLE_HEADER_TEXT_SIZE); - $smarty->assign("reportTableRowTextSize", self::REPORT_TABLE_ROW_TEXT_SIZE); - $smarty->assign("reportBackToTopTextSize", self::REPORT_BACK_TO_TOP_TEXT_SIZE); - $smarty->assign("currentPath", Piwik::getPiwikUrl()); - $smarty->assign("logoHeader", Piwik_API_API::getInstance()->getHeaderLogoUrl()); - } - - public function renderReport($processedReport) - { - $smarty = new Piwik_Smarty(); - $this->assignCommonParameters($smarty); - - $reportMetadata = $processedReport['metadata']; - $reportData = $processedReport['reportData']; - $columns = $processedReport['columns']; - list($reportData, $columns) = self::processTableFormat($reportMetadata, $reportData, $columns); - - $smarty->assign("reportName", $reportMetadata['name']); - $smarty->assign("reportId", $reportMetadata['uniqueId']); - $smarty->assign("reportColumns", $columns); - $smarty->assign("reportRows", $reportData->getRows()); - $smarty->assign("reportRowsMetadata", $processedReport['reportMetadata']->getRows()); - $smarty->assign("displayTable", $processedReport['displayTable']); - - $displayGraph = $processedReport['displayGraph']; - $evolutionGraph = $processedReport['evolutionGraph']; - $smarty->assign("displayGraph", $displayGraph); - - if($displayGraph) - { - $smarty->assign("graphWidth", self::IMAGE_GRAPH_WIDTH); - $smarty->assign("graphHeight", self::IMAGE_GRAPH_HEIGHT); - $smarty->assign("renderImageInline", $this->renderImageInline); - - if($this->renderImageInline) - { - $staticGraph = parent::getStaticGraph($reportMetadata, self::IMAGE_GRAPH_WIDTH, self::IMAGE_GRAPH_HEIGHT, $evolutionGraph); - $smarty->assign("generatedImageGraph", base64_encode($staticGraph)); - unset($generatedImageGraph); - } - } - - $this->rendering .= $smarty->fetch(self::prefixTemplatePath("html_report_body.tpl")); - } - - private static function prefixTemplatePath($templateFile) - { - return PIWIK_USER_PATH . "/plugins/CoreHome/templates/" . $templateFile; - } + const IMAGE_GRAPH_WIDTH = 700; + const IMAGE_GRAPH_HEIGHT = 200; + + const REPORT_TITLE_TEXT_SIZE = 11; + const REPORT_TABLE_HEADER_TEXT_SIZE = 11; + const REPORT_TABLE_ROW_TEXT_SIZE = 11; + const REPORT_BACK_TO_TOP_TEXT_SIZE = 9; + + const HTML_CONTENT_TYPE = 'text/html'; + const HTML_FILE_EXTENSION = 'html'; + + protected $renderImageInline = false; + + private $rendering = ""; + + public function setLocale($locale) + { + //Nothing to do + } + + /** + * Currently only used for HTML reports. + * When sent by mail, images are attached to the mail: renderImageInline = false + * When downloaded, images are included base64 encoded in the report body: renderImageInline = true + * + * @param boolean $renderImageInline + */ + public function setRenderImageInline($renderImageInline) + { + $this->renderImageInline = $renderImageInline; + } + + public function sendToDisk($filename) + { + $this->epilogue(); + + return Piwik_ReportRenderer::writeFile($filename, self::HTML_FILE_EXTENSION, $this->rendering); + } + + public function sendToBrowserDownload($filename) + { + $this->epilogue(); + + Piwik_ReportRenderer::sendToBrowser($filename, self::HTML_FILE_EXTENSION, self::HTML_CONTENT_TYPE, $this->rendering); + } + + public function sendToBrowserInline($filename) + { + $this->epilogue(); + + Piwik_ReportRenderer::inlineToBrowser(self::HTML_CONTENT_TYPE, $this->rendering); + } + + public function getRenderedReport() + { + $this->epilogue(); + + return $this->rendering; + } + + private function epilogue() + { + $smarty = new Piwik_Smarty(); + $this->rendering .= $smarty->fetch(self::prefixTemplatePath("html_report_footer.tpl")); + } + + public function renderFrontPage($websiteName, $prettyDate, $description, $reportMetadata) + { + $smarty = new Piwik_Smarty(); + $this->assignCommonParameters($smarty); + + $smarty->assign("websiteName", $websiteName); + $smarty->assign("prettyDate", $prettyDate); + $smarty->assign("description", $description); + $smarty->assign("reportMetadata", $reportMetadata); + + $this->rendering .= $smarty->fetch(self::prefixTemplatePath("html_report_header.tpl")); + } + + private function assignCommonParameters($smarty) + { + $smarty->assign("reportTitleTextColor", Piwik_ReportRenderer::REPORT_TITLE_TEXT_COLOR); + $smarty->assign("reportTitleTextSize", self::REPORT_TITLE_TEXT_SIZE); + $smarty->assign("reportTextColor", Piwik_ReportRenderer::REPORT_TEXT_COLOR); + $smarty->assign("tableHeaderBgColor", Piwik_ReportRenderer::TABLE_HEADER_BG_COLOR); + $smarty->assign("tableHeaderTextColor", Piwik_ReportRenderer::TABLE_HEADER_TEXT_COLOR); + $smarty->assign("tableCellBorderColor", Piwik_ReportRenderer::TABLE_CELL_BORDER_COLOR); + $smarty->assign("tableBgColor", Piwik_ReportRenderer::TABLE_BG_COLOR); + $smarty->assign("reportTableHeaderTextSize", self::REPORT_TABLE_HEADER_TEXT_SIZE); + $smarty->assign("reportTableRowTextSize", self::REPORT_TABLE_ROW_TEXT_SIZE); + $smarty->assign("reportBackToTopTextSize", self::REPORT_BACK_TO_TOP_TEXT_SIZE); + $smarty->assign("currentPath", Piwik::getPiwikUrl()); + $smarty->assign("logoHeader", Piwik_API_API::getInstance()->getHeaderLogoUrl()); + } + + public function renderReport($processedReport) + { + $smarty = new Piwik_Smarty(); + $this->assignCommonParameters($smarty); + + $reportMetadata = $processedReport['metadata']; + $reportData = $processedReport['reportData']; + $columns = $processedReport['columns']; + list($reportData, $columns) = self::processTableFormat($reportMetadata, $reportData, $columns); + + $smarty->assign("reportName", $reportMetadata['name']); + $smarty->assign("reportId", $reportMetadata['uniqueId']); + $smarty->assign("reportColumns", $columns); + $smarty->assign("reportRows", $reportData->getRows()); + $smarty->assign("reportRowsMetadata", $processedReport['reportMetadata']->getRows()); + $smarty->assign("displayTable", $processedReport['displayTable']); + + $displayGraph = $processedReport['displayGraph']; + $evolutionGraph = $processedReport['evolutionGraph']; + $smarty->assign("displayGraph", $displayGraph); + + if ($displayGraph) { + $smarty->assign("graphWidth", self::IMAGE_GRAPH_WIDTH); + $smarty->assign("graphHeight", self::IMAGE_GRAPH_HEIGHT); + $smarty->assign("renderImageInline", $this->renderImageInline); + + if ($this->renderImageInline) { + $staticGraph = parent::getStaticGraph($reportMetadata, self::IMAGE_GRAPH_WIDTH, self::IMAGE_GRAPH_HEIGHT, $evolutionGraph); + $smarty->assign("generatedImageGraph", base64_encode($staticGraph)); + unset($generatedImageGraph); + } + } + + $this->rendering .= $smarty->fetch(self::prefixTemplatePath("html_report_body.tpl")); + } + + private static function prefixTemplatePath($templateFile) + { + return PIWIK_USER_PATH . "/plugins/CoreHome/templates/" . $templateFile; + } }
\ No newline at end of file diff --git a/core/ReportRenderer/Pdf.php b/core/ReportRenderer/Pdf.php index 95e7a39a5d..23d24ab18f 100644 --- a/core/ReportRenderer/Pdf.php +++ b/core/ReportRenderer/Pdf.php @@ -22,499 +22,481 @@ require_once PIWIK_INCLUDE_PATH . '/core/TCPDF.php'; */ class Piwik_ReportRenderer_Pdf extends Piwik_ReportRenderer { - const IMAGE_GRAPH_WIDTH_LANDSCAPE = 1050; - const IMAGE_GRAPH_WIDTH_PORTRAIT = 760; - const IMAGE_GRAPH_HEIGHT = 220; - - const LANDSCAPE = 'L'; - const PORTRAIT = 'P'; - - const MAX_ROW_COUNT = 28; - const TABLE_HEADER_ROW_COUNT = 6; - const NO_DATA_ROW_COUNT = 6; - const MAX_GRAPH_REPORTS = 3; - const MAX_2COL_TABLE_REPORTS = 2; - - const PDF_CONTENT_TYPE = 'pdf'; - - private $reportFontStyle = ''; - private $reportSimpleFontSize = 9; - private $reportHeaderFontSize = 16; - private $cellHeight = 6; - private $bottomMargin = 17; - private $reportWidthPortrait = 195; - private $reportWidthLandscape = 270; - private $minWidthLabelCell = 100; - private $maxColumnCountPortraitOrientation = 6; - private $logoWidth = 16; - private $logoHeight = 16; - private $totalWidth; - private $cellWidth; - private $labelCellWidth; - private $truncateAfter = 55; - private $leftSpacesBeforeLogo = 7; - private $logoImagePosition = array(10, 40); - private $headerTextColor; - private $reportTextColor; - private $tableHeaderBackgroundColor; - private $tableHeaderTextColor; - private $tableCellBorderColor; - private $tableBackgroundColor; - private $rowTopBottomBorder = array(231, 231, 231); - private $report; - private $reportMetadata; - private $displayGraph; - private $evolutionGraph; - private $displayTable; - private $reportColumns; - private $reportRowsMetadata; - private $currentPage = 0; - private $reportFont = Piwik_ReportRenderer::DEFAULT_REPORT_FONT; - private $TCPDF; - private $orientation = self::PORTRAIT; - - public function __construct() - { - $this->TCPDF = new Piwik_TCPDF(); - $this->headerTextColor = preg_split("/,/", Piwik_ReportRenderer::REPORT_TITLE_TEXT_COLOR); - $this->reportTextColor = preg_split("/,/", Piwik_ReportRenderer::REPORT_TEXT_COLOR); - $this->tableHeaderBackgroundColor = preg_split("/,/", Piwik_ReportRenderer::TABLE_HEADER_BG_COLOR); - $this->tableHeaderTextColor = preg_split("/,/", Piwik_ReportRenderer::TABLE_HEADER_TEXT_COLOR); - $this->tableCellBorderColor = preg_split("/,/", Piwik_ReportRenderer::TABLE_CELL_BORDER_COLOR); - $this->tableBackgroundColor = preg_split("/,/", Piwik_ReportRenderer::TABLE_BG_COLOR); - } - - public function setLocale($locale) - { - switch ($locale) - { - case 'zh-tw': - $reportFont = 'msungstdlight'; - break; - - case 'ja': - $reportFont = 'kozgopromedium'; - break; - - case 'zh-cn': - $reportFont = 'stsongstdlight'; - break; - - case 'ko': - $reportFont = 'hysmyeongjostdmedium'; - break; - - case 'ar': - $reportFont = 'almohanad'; - break; - - case 'en': - default: - $reportFont = Piwik_ReportRenderer::DEFAULT_REPORT_FONT; - break; - } - $this->reportFont = $reportFont; - } - - public function sendToDisk($filename) - { - $filename = Piwik_ReportRenderer::appendExtension($filename, self::PDF_CONTENT_TYPE); - $outputFilename = Piwik_ReportRenderer::getOutputPath($filename); - - $this->TCPDF->Output($outputFilename, 'F'); - - return $outputFilename; - } - - public function sendToBrowserDownload($filename) - { - $filename = Piwik_ReportRenderer::appendExtension($filename, self::PDF_CONTENT_TYPE); - $this->TCPDF->Output($filename, 'D'); - } - - public function sendToBrowserInline($filename) - { - $filename = Piwik_ReportRenderer::appendExtension($filename, self::PDF_CONTENT_TYPE); - $this->TCPDF->Output($filename, 'I'); - } - - public function getRenderedReport() - { - return $this->TCPDF->Output(null, 'S'); - } - - public function renderFrontPage($websiteName, $prettyDate, $description, $reportMetadata) - { - $websiteTitle = $this->formatText($websiteName); - $dateRange = $this->formatText(Piwik_Translate('General_DateRange') . " " . $prettyDate); - - //Setup Footer font and data - $this->TCPDF->SetFooterFont(array($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize)); - $this->TCPDF->SetFooterContent($websiteTitle . " | " . $dateRange . " | "); - - $this->TCPDF->setPrintHeader(false); - // $this->SetMargins($left = , $top, $right=-1, $keepmargins=true) - $this->TCPDF->AddPage(self::PORTRAIT); - $this->TCPDF->AddFont($this->reportFont, '', '', false); - $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize); - //Image($file, $x='', $y='', $w=0, $h=0, $type='', $link='', $align='', $resize=false, $dpi=300, $palign='', $ismask=false, $imgmask=false, $border=0, $fitbox=false, $hidden=false, $fitonpage=false) { - $this->TCPDF->Bookmark(Piwik_Translate('PDFReports_FrontPage')); - $this->TCPDF->Image(Piwik_API_API::getInstance()->getLogoUrl(true), $this->logoImagePosition[0], $this->logoImagePosition[1], 180 / $factor = 2, 0, $type = '', $link = '', $align = '', $resize = false, $dpi = 300); - $this->TCPDF->Ln(8); - - $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize + 5); - $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]); - $this->TCPDF->Cell(40, 210, $websiteTitle); - $this->TCPDF->Ln(8 * 4); - - $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize); - $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); - $this->TCPDF->Cell(40, 210, $dateRange); - $this->TCPDF->Ln(8 * 20); - $this->TCPDF->Write(1, $this->formatText($description)); - $this->TCPDF->Ln(8); - $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize); - $this->TCPDF->Ln(); - } - - /** - * Generate a header of page. - */ - private function paintReportHeader() - { - $isAggregateReport = !empty($this->reportMetadata['dimension']); - - // Graph-only report - static $graphOnlyReportCount = 0; - $graphOnlyReport = $isAggregateReport && $this->displayGraph && !$this->displayTable; - - // Table-only report - $tableOnlyReport = $isAggregateReport - && !$this->displayGraph - && $this->displayTable; - - $columnCount = count($this->reportColumns); - - // Table-only 2-column report - static $tableOnly2ColumnReportCount = 0; - $tableOnly2ColumnReport = $tableOnlyReport - && $columnCount == 2; - - // Table-only report with more than 2 columns - static $tableOnlyManyColumnReportRowCount = 0; - $tableOnlyManyColumnReport = $tableOnlyReport - && $columnCount > 3; - - $reportHasData = $this->reportHasData(); - - $rowCount = $reportHasData ? $this->report->getRowsCount() + self::TABLE_HEADER_ROW_COUNT : self::NO_DATA_ROW_COUNT; - - // Only a page break before if the current report has some data - if ($reportHasData && - // and - ( - // it is the first report - $this->currentPage == 0 - // or, it is a graph-only report and it is the first of a series of self::MAX_GRAPH_REPORTS - || ($graphOnlyReport && $graphOnlyReportCount == 0) - // or, it is a table-only 2-column report and it is the first of a series of self::MAX_2COL_TABLE_REPORTS - || ($tableOnly2ColumnReport && $tableOnly2ColumnReportCount == 0) - // or it is a table-only report with more than 2 columns and it is the first of its series or there isn't enough space left on the page - || ($tableOnlyManyColumnReport && ($tableOnlyManyColumnReportRowCount == 0 || $tableOnlyManyColumnReportRowCount + $rowCount >= self::MAX_ROW_COUNT)) - // or it is a report with both a table and a graph - || !$graphOnlyReport && !$tableOnlyReport - ) - ) - { - $this->currentPage++; - $this->TCPDF->AddPage(); - - // Table-only reports with more than 2 columns are always landscape - if ($tableOnlyManyColumnReport) - { - $tableOnlyManyColumnReportRowCount = 0; - $this->orientation = self::LANDSCAPE; - } - else - { - // Graph-only reports are always portrait - $this->orientation = $graphOnlyReport ? self::PORTRAIT : ($columnCount > $this->maxColumnCountPortraitOrientation ? self::LANDSCAPE : self::PORTRAIT); - } - - $this->TCPDF->setPageOrientation($this->orientation, '', $this->bottomMargin); - } - - $graphOnlyReportCount = ($graphOnlyReport && $reportHasData) ? ($graphOnlyReportCount + 1) % self::MAX_GRAPH_REPORTS : 0; - $tableOnly2ColumnReportCount = ($tableOnly2ColumnReport && $reportHasData) ? ($tableOnly2ColumnReportCount + 1) % self::MAX_2COL_TABLE_REPORTS : 0; - $tableOnlyManyColumnReportRowCount = $tableOnlyManyColumnReport ? ($tableOnlyManyColumnReportRowCount + $rowCount) : 0; - - $title = $this->formatText($this->reportMetadata['name']); - $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportHeaderFontSize); - $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]); - $this->TCPDF->Bookmark($title); - $this->TCPDF->Cell(40, 15, $title); - $this->TCPDF->Ln(); - $this->TCPDF->SetFont($this->reportFont, '', $this->reportSimpleFontSize); - $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); - } - - private function reportHasData() - { - return $this->report->getRowsCount() > 0; - } - - private function setBorderColor() - { - $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]); - } - - public function renderReport($processedReport) - { - $this->reportMetadata = $processedReport['metadata']; - $this->reportRowsMetadata = $processedReport['reportMetadata']; - $this->displayGraph = $processedReport['displayGraph']; - $this->evolutionGraph = $processedReport['evolutionGraph']; - $this->displayTable = $processedReport['displayTable']; - list($this->report, $this->reportColumns) = self::processTableFormat($this->reportMetadata, $processedReport['reportData'], $processedReport['columns']); - - $this->paintReportHeader(); - - if (!$this->reportHasData()) { - $this->paintMessage(Piwik_Translate('CoreHome_ThereIsNoDataForThisReport')); - return; - } - - if($this->displayGraph) - { - $this->paintGraph(); - } - - if($this->displayGraph && $this->displayTable) - { - $this->TCPDF->Ln(5); - } - - if($this->displayTable) - { - $this->paintReportTableHeader(); - $this->paintReportTable(); - } - } - - private function formatText($text) - { - return Piwik_Common::unsanitizeInputValue($text); - } - - private function paintReportTable() - { - //Color and font restoration - $this->TCPDF->SetFillColor($this->tableBackgroundColor[0], $this->tableBackgroundColor[1], $this->tableBackgroundColor[2]); - $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); - $this->TCPDF->SetFont(''); - - $fill = false; - $url = false; - $leftSpacesBeforeLogo = str_repeat(' ', $this->leftSpacesBeforeLogo); - - $logoWidth = $this->logoWidth; - $logoHeight = $this->logoHeight; - - $rowsMetadata = $this->reportRowsMetadata->getRows(); - - // Draw a body of report table - foreach ($this->report->getRows() as $rowId => $row) - { - $rowMetrics = $row->getColumns(); - $rowMetadata = isset($rowsMetadata[$rowId]) ? $rowsMetadata[$rowId]->getColumns() : array(); - if (isset($rowMetadata['url'])) { - $url = $rowMetadata['url']; - } - foreach ($this->reportColumns as $columnId => $columnName) - { - // Label column - if ($columnId == 'label') { - $isLogoDisplayable = isset($rowMetadata['logo']); - $text = ''; - $posX = $this->TCPDF->GetX(); - $posY = $this->TCPDF->GetY(); - if (isset($rowMetrics[$columnId])) { - $text = substr($rowMetrics[$columnId], 0, $this->truncateAfter); - if ($isLogoDisplayable) { - $text = $leftSpacesBeforeLogo . $text; - } - } - $text = $this->formatText($text); - - $this->TCPDF->Cell($this->labelCellWidth, $this->cellHeight, $text, 'LR', 0, 'L', $fill, $url); - - if ($isLogoDisplayable) { - if (isset($rowMetadata['logoWidth'])) { - $logoWidth = $rowMetadata['logoWidth']; - } - if (isset($rowMetadata['logoHeight'])) { - $logoHeight = $rowMetadata['logoHeight']; - } - $restoreY = $this->TCPDF->getY(); - $restoreX = $this->TCPDF->getX(); - $this->TCPDF->SetY($posY); - $this->TCPDF->SetX($posX); - $topMargin = 1.3; - // Country flags are not very high, force a bigger top margin - if ($logoHeight < 16) { - $topMargin = 2; - } - $path = Piwik_Common::getPathToPiwikRoot() . "/" . $rowMetadata['logo']; - if(file_exists($path)) - { - $this->TCPDF->Image($path, $posX + ($leftMargin = 2), $posY + $topMargin, $logoWidth / 4); - } - $this->TCPDF->SetXY($restoreX, $restoreY); - } - } - // metrics column - else - { - // No value means 0 - if (empty($rowMetrics[$columnId])) { - $rowMetrics[$columnId] = 0; - } - $this->TCPDF->Cell($this->cellWidth, $this->cellHeight, $rowMetrics[$columnId], 'LR', 0, 'L', $fill); - } - } - - $this->TCPDF->Ln(); - - // Top/Bottom grey border for all cells - $this->TCPDF->SetDrawColor($this->rowTopBottomBorder[0], $this->rowTopBottomBorder[1], $this->rowTopBottomBorder[2]); - $this->TCPDF->Cell($this->totalWidth, 0, '', 'T'); - $this->setBorderColor(); - $this->TCPDF->Ln(0.2); - - $fill = !$fill; - } - } - private function paintGraph() - { - $imageGraph = parent::getStaticGraph( - $this->reportMetadata, - $this->orientation == self::PORTRAIT ? self::IMAGE_GRAPH_WIDTH_PORTRAIT : self::IMAGE_GRAPH_WIDTH_LANDSCAPE, - self::IMAGE_GRAPH_HEIGHT, - $this->evolutionGraph - ); - - $this->TCPDF->Image( - '@'.$imageGraph, - $x = '', - $y = '', - $w = 0, - $h = 0, - $type = '', - $link = '', - $align = 'N', - $resize = false, - $dpi = 72, - $palign = '', - $ismask = false, - $imgmask = false, - $order = 0, - $fitbox = false, - $hidden = false, - $fitonpage = true, - $alt = false, - $altimgs = array() - ); - - unset($imageGraph); - } - - /** - * Draw the table header (first row) - */ - private function paintReportTableHeader() - { - $initPosX = 10; - - // Get the longest column name - $longestColumnName = ''; - foreach ($this->reportColumns as $columnName) - { - if (strlen($columnName) > strlen($longestColumnName)) { - $longestColumnName = $columnName; - } - } - - $columnsCount = count($this->reportColumns); - // Computes available column width - if ($this->orientation == self::PORTRAIT - && $columnsCount <= 3) { - $totalWidth = $this->reportWidthPortrait * 2 / 3; - } - else if ($this->orientation == self::LANDSCAPE) { - $totalWidth = $this->reportWidthLandscape; - } - else - { - $totalWidth = $this->reportWidthPortrait; - } - $this->totalWidth = $totalWidth; - $this->labelCellWidth = max(round(($this->totalWidth / $columnsCount) ), $this->minWidthLabelCell); - $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / ($columnsCount - 1)); - $this->totalWidth = $this->labelCellWidth + ($columnsCount - 1) * $this->cellWidth; - - $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]); - $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]); - $this->TCPDF->SetLineWidth(.3); - $this->setBorderColor(); - $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle); - $this->TCPDF->SetFillColor(255); - $this->TCPDF->SetTextColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]); - $this->TCPDF->SetDrawColor(255); - - $posY = $this->TCPDF->GetY(); - $this->TCPDF->MultiCell($this->cellWidth, $this->cellHeight, $longestColumnName, 1, 'C', true); - $maxCellHeight = $this->TCPDF->GetY() - $posY; - - $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]); - $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]); - $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]); - - $this->TCPDF->SetXY($initPosX, $posY); - - $countColumns = 0; - $posX = $initPosX; - foreach ($this->reportColumns as $columnName) - { - $columnName = $this->formatText($columnName); - //Label column - if ($countColumns == 0) { - $this->TCPDF->MultiCell($this->labelCellWidth, $maxCellHeight, $columnName, 1, 'C', true); - $this->TCPDF->SetXY($posX + $this->labelCellWidth, $posY); - } - else - { - $this->TCPDF->MultiCell($this->cellWidth, $maxCellHeight, $columnName, 1, 'C', true); - $this->TCPDF->SetXY($posX + $this->cellWidth, $posY); - } - $countColumns++; - $posX = $this->TCPDF->GetX(); - } - $this->TCPDF->Ln(); - $this->TCPDF->SetXY($initPosX, $posY + $maxCellHeight); - } - - /** - * Prints a message - * - * @param string $message - * @return void - */ - private function paintMessage($message) - { - $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize); - $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); - $message = $this->formatText($message); - $this->TCPDF->Write("1em", $message); - $this->TCPDF->Ln(); - } + const IMAGE_GRAPH_WIDTH_LANDSCAPE = 1050; + const IMAGE_GRAPH_WIDTH_PORTRAIT = 760; + const IMAGE_GRAPH_HEIGHT = 220; + + const LANDSCAPE = 'L'; + const PORTRAIT = 'P'; + + const MAX_ROW_COUNT = 28; + const TABLE_HEADER_ROW_COUNT = 6; + const NO_DATA_ROW_COUNT = 6; + const MAX_GRAPH_REPORTS = 3; + const MAX_2COL_TABLE_REPORTS = 2; + + const PDF_CONTENT_TYPE = 'pdf'; + + private $reportFontStyle = ''; + private $reportSimpleFontSize = 9; + private $reportHeaderFontSize = 16; + private $cellHeight = 6; + private $bottomMargin = 17; + private $reportWidthPortrait = 195; + private $reportWidthLandscape = 270; + private $minWidthLabelCell = 100; + private $maxColumnCountPortraitOrientation = 6; + private $logoWidth = 16; + private $logoHeight = 16; + private $totalWidth; + private $cellWidth; + private $labelCellWidth; + private $truncateAfter = 55; + private $leftSpacesBeforeLogo = 7; + private $logoImagePosition = array(10, 40); + private $headerTextColor; + private $reportTextColor; + private $tableHeaderBackgroundColor; + private $tableHeaderTextColor; + private $tableCellBorderColor; + private $tableBackgroundColor; + private $rowTopBottomBorder = array(231, 231, 231); + private $report; + private $reportMetadata; + private $displayGraph; + private $evolutionGraph; + private $displayTable; + private $reportColumns; + private $reportRowsMetadata; + private $currentPage = 0; + private $reportFont = Piwik_ReportRenderer::DEFAULT_REPORT_FONT; + private $TCPDF; + private $orientation = self::PORTRAIT; + + public function __construct() + { + $this->TCPDF = new Piwik_TCPDF(); + $this->headerTextColor = preg_split("/,/", Piwik_ReportRenderer::REPORT_TITLE_TEXT_COLOR); + $this->reportTextColor = preg_split("/,/", Piwik_ReportRenderer::REPORT_TEXT_COLOR); + $this->tableHeaderBackgroundColor = preg_split("/,/", Piwik_ReportRenderer::TABLE_HEADER_BG_COLOR); + $this->tableHeaderTextColor = preg_split("/,/", Piwik_ReportRenderer::TABLE_HEADER_TEXT_COLOR); + $this->tableCellBorderColor = preg_split("/,/", Piwik_ReportRenderer::TABLE_CELL_BORDER_COLOR); + $this->tableBackgroundColor = preg_split("/,/", Piwik_ReportRenderer::TABLE_BG_COLOR); + } + + public function setLocale($locale) + { + switch ($locale) { + case 'zh-tw': + $reportFont = 'msungstdlight'; + break; + + case 'ja': + $reportFont = 'kozgopromedium'; + break; + + case 'zh-cn': + $reportFont = 'stsongstdlight'; + break; + + case 'ko': + $reportFont = 'hysmyeongjostdmedium'; + break; + + case 'ar': + $reportFont = 'almohanad'; + break; + + case 'en': + default: + $reportFont = Piwik_ReportRenderer::DEFAULT_REPORT_FONT; + break; + } + $this->reportFont = $reportFont; + } + + public function sendToDisk($filename) + { + $filename = Piwik_ReportRenderer::appendExtension($filename, self::PDF_CONTENT_TYPE); + $outputFilename = Piwik_ReportRenderer::getOutputPath($filename); + + $this->TCPDF->Output($outputFilename, 'F'); + + return $outputFilename; + } + + public function sendToBrowserDownload($filename) + { + $filename = Piwik_ReportRenderer::appendExtension($filename, self::PDF_CONTENT_TYPE); + $this->TCPDF->Output($filename, 'D'); + } + + public function sendToBrowserInline($filename) + { + $filename = Piwik_ReportRenderer::appendExtension($filename, self::PDF_CONTENT_TYPE); + $this->TCPDF->Output($filename, 'I'); + } + + public function getRenderedReport() + { + return $this->TCPDF->Output(null, 'S'); + } + + public function renderFrontPage($websiteName, $prettyDate, $description, $reportMetadata) + { + $websiteTitle = $this->formatText($websiteName); + $dateRange = $this->formatText(Piwik_Translate('General_DateRange') . " " . $prettyDate); + + //Setup Footer font and data + $this->TCPDF->SetFooterFont(array($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize)); + $this->TCPDF->SetFooterContent($websiteTitle . " | " . $dateRange . " | "); + + $this->TCPDF->setPrintHeader(false); + // $this->SetMargins($left = , $top, $right=-1, $keepmargins=true) + $this->TCPDF->AddPage(self::PORTRAIT); + $this->TCPDF->AddFont($this->reportFont, '', '', false); + $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize); + //Image($file, $x='', $y='', $w=0, $h=0, $type='', $link='', $align='', $resize=false, $dpi=300, $palign='', $ismask=false, $imgmask=false, $border=0, $fitbox=false, $hidden=false, $fitonpage=false) { + $this->TCPDF->Bookmark(Piwik_Translate('PDFReports_FrontPage')); + $this->TCPDF->Image(Piwik_API_API::getInstance()->getLogoUrl(true), $this->logoImagePosition[0], $this->logoImagePosition[1], 180 / $factor = 2, 0, $type = '', $link = '', $align = '', $resize = false, $dpi = 300); + $this->TCPDF->Ln(8); + + $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize + 5); + $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]); + $this->TCPDF->Cell(40, 210, $websiteTitle); + $this->TCPDF->Ln(8 * 4); + + $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize); + $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); + $this->TCPDF->Cell(40, 210, $dateRange); + $this->TCPDF->Ln(8 * 20); + $this->TCPDF->Write(1, $this->formatText($description)); + $this->TCPDF->Ln(8); + $this->TCPDF->SetFont($this->reportFont, '', $this->reportHeaderFontSize); + $this->TCPDF->Ln(); + } + + /** + * Generate a header of page. + */ + private function paintReportHeader() + { + $isAggregateReport = !empty($this->reportMetadata['dimension']); + + // Graph-only report + static $graphOnlyReportCount = 0; + $graphOnlyReport = $isAggregateReport && $this->displayGraph && !$this->displayTable; + + // Table-only report + $tableOnlyReport = $isAggregateReport + && !$this->displayGraph + && $this->displayTable; + + $columnCount = count($this->reportColumns); + + // Table-only 2-column report + static $tableOnly2ColumnReportCount = 0; + $tableOnly2ColumnReport = $tableOnlyReport + && $columnCount == 2; + + // Table-only report with more than 2 columns + static $tableOnlyManyColumnReportRowCount = 0; + $tableOnlyManyColumnReport = $tableOnlyReport + && $columnCount > 3; + + $reportHasData = $this->reportHasData(); + + $rowCount = $reportHasData ? $this->report->getRowsCount() + self::TABLE_HEADER_ROW_COUNT : self::NO_DATA_ROW_COUNT; + + // Only a page break before if the current report has some data + if ($reportHasData && + // and + ( + // it is the first report + $this->currentPage == 0 + // or, it is a graph-only report and it is the first of a series of self::MAX_GRAPH_REPORTS + || ($graphOnlyReport && $graphOnlyReportCount == 0) + // or, it is a table-only 2-column report and it is the first of a series of self::MAX_2COL_TABLE_REPORTS + || ($tableOnly2ColumnReport && $tableOnly2ColumnReportCount == 0) + // or it is a table-only report with more than 2 columns and it is the first of its series or there isn't enough space left on the page + || ($tableOnlyManyColumnReport && ($tableOnlyManyColumnReportRowCount == 0 || $tableOnlyManyColumnReportRowCount + $rowCount >= self::MAX_ROW_COUNT)) + // or it is a report with both a table and a graph + || !$graphOnlyReport && !$tableOnlyReport + ) + ) { + $this->currentPage++; + $this->TCPDF->AddPage(); + + // Table-only reports with more than 2 columns are always landscape + if ($tableOnlyManyColumnReport) { + $tableOnlyManyColumnReportRowCount = 0; + $this->orientation = self::LANDSCAPE; + } else { + // Graph-only reports are always portrait + $this->orientation = $graphOnlyReport ? self::PORTRAIT : ($columnCount > $this->maxColumnCountPortraitOrientation ? self::LANDSCAPE : self::PORTRAIT); + } + + $this->TCPDF->setPageOrientation($this->orientation, '', $this->bottomMargin); + } + + $graphOnlyReportCount = ($graphOnlyReport && $reportHasData) ? ($graphOnlyReportCount + 1) % self::MAX_GRAPH_REPORTS : 0; + $tableOnly2ColumnReportCount = ($tableOnly2ColumnReport && $reportHasData) ? ($tableOnly2ColumnReportCount + 1) % self::MAX_2COL_TABLE_REPORTS : 0; + $tableOnlyManyColumnReportRowCount = $tableOnlyManyColumnReport ? ($tableOnlyManyColumnReportRowCount + $rowCount) : 0; + + $title = $this->formatText($this->reportMetadata['name']); + $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportHeaderFontSize); + $this->TCPDF->SetTextColor($this->headerTextColor[0], $this->headerTextColor[1], $this->headerTextColor[2]); + $this->TCPDF->Bookmark($title); + $this->TCPDF->Cell(40, 15, $title); + $this->TCPDF->Ln(); + $this->TCPDF->SetFont($this->reportFont, '', $this->reportSimpleFontSize); + $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); + } + + private function reportHasData() + { + return $this->report->getRowsCount() > 0; + } + + private function setBorderColor() + { + $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]); + } + + public function renderReport($processedReport) + { + $this->reportMetadata = $processedReport['metadata']; + $this->reportRowsMetadata = $processedReport['reportMetadata']; + $this->displayGraph = $processedReport['displayGraph']; + $this->evolutionGraph = $processedReport['evolutionGraph']; + $this->displayTable = $processedReport['displayTable']; + list($this->report, $this->reportColumns) = self::processTableFormat($this->reportMetadata, $processedReport['reportData'], $processedReport['columns']); + + $this->paintReportHeader(); + + if (!$this->reportHasData()) { + $this->paintMessage(Piwik_Translate('CoreHome_ThereIsNoDataForThisReport')); + return; + } + + if ($this->displayGraph) { + $this->paintGraph(); + } + + if ($this->displayGraph && $this->displayTable) { + $this->TCPDF->Ln(5); + } + + if ($this->displayTable) { + $this->paintReportTableHeader(); + $this->paintReportTable(); + } + } + + private function formatText($text) + { + return Piwik_Common::unsanitizeInputValue($text); + } + + private function paintReportTable() + { + //Color and font restoration + $this->TCPDF->SetFillColor($this->tableBackgroundColor[0], $this->tableBackgroundColor[1], $this->tableBackgroundColor[2]); + $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); + $this->TCPDF->SetFont(''); + + $fill = false; + $url = false; + $leftSpacesBeforeLogo = str_repeat(' ', $this->leftSpacesBeforeLogo); + + $logoWidth = $this->logoWidth; + $logoHeight = $this->logoHeight; + + $rowsMetadata = $this->reportRowsMetadata->getRows(); + + // Draw a body of report table + foreach ($this->report->getRows() as $rowId => $row) { + $rowMetrics = $row->getColumns(); + $rowMetadata = isset($rowsMetadata[$rowId]) ? $rowsMetadata[$rowId]->getColumns() : array(); + if (isset($rowMetadata['url'])) { + $url = $rowMetadata['url']; + } + foreach ($this->reportColumns as $columnId => $columnName) { + // Label column + if ($columnId == 'label') { + $isLogoDisplayable = isset($rowMetadata['logo']); + $text = ''; + $posX = $this->TCPDF->GetX(); + $posY = $this->TCPDF->GetY(); + if (isset($rowMetrics[$columnId])) { + $text = substr($rowMetrics[$columnId], 0, $this->truncateAfter); + if ($isLogoDisplayable) { + $text = $leftSpacesBeforeLogo . $text; + } + } + $text = $this->formatText($text); + + $this->TCPDF->Cell($this->labelCellWidth, $this->cellHeight, $text, 'LR', 0, 'L', $fill, $url); + + if ($isLogoDisplayable) { + if (isset($rowMetadata['logoWidth'])) { + $logoWidth = $rowMetadata['logoWidth']; + } + if (isset($rowMetadata['logoHeight'])) { + $logoHeight = $rowMetadata['logoHeight']; + } + $restoreY = $this->TCPDF->getY(); + $restoreX = $this->TCPDF->getX(); + $this->TCPDF->SetY($posY); + $this->TCPDF->SetX($posX); + $topMargin = 1.3; + // Country flags are not very high, force a bigger top margin + if ($logoHeight < 16) { + $topMargin = 2; + } + $path = Piwik_Common::getPathToPiwikRoot() . "/" . $rowMetadata['logo']; + if (file_exists($path)) { + $this->TCPDF->Image($path, $posX + ($leftMargin = 2), $posY + $topMargin, $logoWidth / 4); + } + $this->TCPDF->SetXY($restoreX, $restoreY); + } + } // metrics column + else { + // No value means 0 + if (empty($rowMetrics[$columnId])) { + $rowMetrics[$columnId] = 0; + } + $this->TCPDF->Cell($this->cellWidth, $this->cellHeight, $rowMetrics[$columnId], 'LR', 0, 'L', $fill); + } + } + + $this->TCPDF->Ln(); + + // Top/Bottom grey border for all cells + $this->TCPDF->SetDrawColor($this->rowTopBottomBorder[0], $this->rowTopBottomBorder[1], $this->rowTopBottomBorder[2]); + $this->TCPDF->Cell($this->totalWidth, 0, '', 'T'); + $this->setBorderColor(); + $this->TCPDF->Ln(0.2); + + $fill = !$fill; + } + } + + private function paintGraph() + { + $imageGraph = parent::getStaticGraph( + $this->reportMetadata, + $this->orientation == self::PORTRAIT ? self::IMAGE_GRAPH_WIDTH_PORTRAIT : self::IMAGE_GRAPH_WIDTH_LANDSCAPE, + self::IMAGE_GRAPH_HEIGHT, + $this->evolutionGraph + ); + + $this->TCPDF->Image( + '@' . $imageGraph, + $x = '', + $y = '', + $w = 0, + $h = 0, + $type = '', + $link = '', + $align = 'N', + $resize = false, + $dpi = 72, + $palign = '', + $ismask = false, + $imgmask = false, + $order = 0, + $fitbox = false, + $hidden = false, + $fitonpage = true, + $alt = false, + $altimgs = array() + ); + + unset($imageGraph); + } + + /** + * Draw the table header (first row) + */ + private function paintReportTableHeader() + { + $initPosX = 10; + + // Get the longest column name + $longestColumnName = ''; + foreach ($this->reportColumns as $columnName) { + if (strlen($columnName) > strlen($longestColumnName)) { + $longestColumnName = $columnName; + } + } + + $columnsCount = count($this->reportColumns); + // Computes available column width + if ($this->orientation == self::PORTRAIT + && $columnsCount <= 3 + ) { + $totalWidth = $this->reportWidthPortrait * 2 / 3; + } else if ($this->orientation == self::LANDSCAPE) { + $totalWidth = $this->reportWidthLandscape; + } else { + $totalWidth = $this->reportWidthPortrait; + } + $this->totalWidth = $totalWidth; + $this->labelCellWidth = max(round(($this->totalWidth / $columnsCount)), $this->minWidthLabelCell); + $this->cellWidth = round(($this->totalWidth - $this->labelCellWidth) / ($columnsCount - 1)); + $this->totalWidth = $this->labelCellWidth + ($columnsCount - 1) * $this->cellWidth; + + $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]); + $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]); + $this->TCPDF->SetLineWidth(.3); + $this->setBorderColor(); + $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle); + $this->TCPDF->SetFillColor(255); + $this->TCPDF->SetTextColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]); + $this->TCPDF->SetDrawColor(255); + + $posY = $this->TCPDF->GetY(); + $this->TCPDF->MultiCell($this->cellWidth, $this->cellHeight, $longestColumnName, 1, 'C', true); + $maxCellHeight = $this->TCPDF->GetY() - $posY; + + $this->TCPDF->SetFillColor($this->tableHeaderBackgroundColor[0], $this->tableHeaderBackgroundColor[1], $this->tableHeaderBackgroundColor[2]); + $this->TCPDF->SetTextColor($this->tableHeaderTextColor[0], $this->tableHeaderTextColor[1], $this->tableHeaderTextColor[2]); + $this->TCPDF->SetDrawColor($this->tableCellBorderColor[0], $this->tableCellBorderColor[1], $this->tableCellBorderColor[2]); + + $this->TCPDF->SetXY($initPosX, $posY); + + $countColumns = 0; + $posX = $initPosX; + foreach ($this->reportColumns as $columnName) { + $columnName = $this->formatText($columnName); + //Label column + if ($countColumns == 0) { + $this->TCPDF->MultiCell($this->labelCellWidth, $maxCellHeight, $columnName, 1, 'C', true); + $this->TCPDF->SetXY($posX + $this->labelCellWidth, $posY); + } else { + $this->TCPDF->MultiCell($this->cellWidth, $maxCellHeight, $columnName, 1, 'C', true); + $this->TCPDF->SetXY($posX + $this->cellWidth, $posY); + } + $countColumns++; + $posX = $this->TCPDF->GetX(); + } + $this->TCPDF->Ln(); + $this->TCPDF->SetXY($initPosX, $posY + $maxCellHeight); + } + + /** + * Prints a message + * + * @param string $message + * @return void + */ + private function paintMessage($message) + { + $this->TCPDF->SetFont($this->reportFont, $this->reportFontStyle, $this->reportSimpleFontSize); + $this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]); + $message = $this->formatText($message); + $this->TCPDF->Write("1em", $message); + $this->TCPDF->Ln(); + } } diff --git a/core/ScheduledTask.php b/core/ScheduledTask.php index 689f835d8e..585d793b91 100644 --- a/core/ScheduledTask.php +++ b/core/ScheduledTask.php @@ -1,153 +1,152 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Piwik_ScheduledTask is used by the task scheduler and by plugins to configure runnable tasks. - * + * * @package Piwik * @subpackage Piwik_ScheduledTask */ class Piwik_ScheduledTask { - const LOWEST_PRIORITY = 12; - const LOW_PRIORITY = 9; - const NORMAL_PRIORITY = 6; - const HIGH_PRIORITY = 3; - const HIGHEST_PRIORITY = 0; - - /** - * Object instance on which the method will be executed by the task scheduler - * @var string - */ - var $objectInstance; - - /** - * Class name where the specified method is located - * @var string - */ - var $className; - - /** - * Class method to run when task is scheduled - * @var string - */ - var $methodName; - - /** - * Parameter to pass to the executed method - * @var string - */ - var $methodParameter; - - /** - * The scheduled time policy - * @var Piwik_ScheduledTime - */ - var $scheduledTime; - - /** - * The priority of a task. Affects the order in which this task will be run. - * @var int - */ - var $priority; - - function __construct( $_objectInstance, $_methodName, $_methodParameter, $_scheduledTime, $_priority = self::NORMAL_PRIORITY ) - { - $this->className = get_class($_objectInstance); - - if ($_priority < self::HIGHEST_PRIORITY || $_priority > self::LOWEST_PRIORITY) - { - throw new Exception("Invalid priority for ScheduledTask '$this->className.$_methodName': $_priority"); - } - - $this->objectInstance = $_objectInstance; - $this->methodName = $_methodName; - $this->scheduledTime = $_scheduledTime; - $this->methodParameter = $_methodParameter; - $this->priority = $_priority; - } - - /** - * Return the object instance on which the method should be executed - * @return string - */ - public function getObjectInstance() - { - return $this->objectInstance; - } - - /** - * Return class name - * @return string - */ - public function getClassName() - { - return $this->className; - } - - /** - * Return method name - * @return string - */ - public function getMethodName() - { - return $this->methodName; - } - - /** - * Return method parameter - * @return string - */ - public function getMethodParameter() - { - return $this->methodParameter; - } - - - /** - * Return scheduled time - * @return Piwik_ScheduledTime - */ - public function getScheduledTime() - { - return $this->scheduledTime; - } - - /** - * Return the rescheduled time in milliseconds - * @return int - */ - public function getRescheduledTime() - { - return $this->getScheduledTime()->getRescheduledTime(); - } - - /** - * Return the task priority. The priority will be an integer whose value is - * between Piwik_ScheduledTask::HIGH_PRIORITY and Piwik_ScheduledTask::LOW_PRIORITY. - * - * @return int - */ - public function getPriority() - { - return $this->priority; - } - - public function getName() - { - return self::getTaskName($this->getClassName(), $this->getMethodName(), $this->getMethodParameter()); - } - - static public function getTaskName($className, $methodName, $methodParameter = null) - { - return $className . '.' . $methodName . ($methodParameter == null ? '' : '_' . $methodParameter); - } + const LOWEST_PRIORITY = 12; + const LOW_PRIORITY = 9; + const NORMAL_PRIORITY = 6; + const HIGH_PRIORITY = 3; + const HIGHEST_PRIORITY = 0; + + /** + * Object instance on which the method will be executed by the task scheduler + * @var string + */ + var $objectInstance; + + /** + * Class name where the specified method is located + * @var string + */ + var $className; + + /** + * Class method to run when task is scheduled + * @var string + */ + var $methodName; + + /** + * Parameter to pass to the executed method + * @var string + */ + var $methodParameter; + + /** + * The scheduled time policy + * @var Piwik_ScheduledTime + */ + var $scheduledTime; + + /** + * The priority of a task. Affects the order in which this task will be run. + * @var int + */ + var $priority; + + function __construct($_objectInstance, $_methodName, $_methodParameter, $_scheduledTime, $_priority = self::NORMAL_PRIORITY) + { + $this->className = get_class($_objectInstance); + + if ($_priority < self::HIGHEST_PRIORITY || $_priority > self::LOWEST_PRIORITY) { + throw new Exception("Invalid priority for ScheduledTask '$this->className.$_methodName': $_priority"); + } + + $this->objectInstance = $_objectInstance; + $this->methodName = $_methodName; + $this->scheduledTime = $_scheduledTime; + $this->methodParameter = $_methodParameter; + $this->priority = $_priority; + } + + /** + * Return the object instance on which the method should be executed + * @return string + */ + public function getObjectInstance() + { + return $this->objectInstance; + } + + /** + * Return class name + * @return string + */ + public function getClassName() + { + return $this->className; + } + + /** + * Return method name + * @return string + */ + public function getMethodName() + { + return $this->methodName; + } + + /** + * Return method parameter + * @return string + */ + public function getMethodParameter() + { + return $this->methodParameter; + } + + + /** + * Return scheduled time + * @return Piwik_ScheduledTime + */ + public function getScheduledTime() + { + return $this->scheduledTime; + } + + /** + * Return the rescheduled time in milliseconds + * @return int + */ + public function getRescheduledTime() + { + return $this->getScheduledTime()->getRescheduledTime(); + } + + /** + * Return the task priority. The priority will be an integer whose value is + * between Piwik_ScheduledTask::HIGH_PRIORITY and Piwik_ScheduledTask::LOW_PRIORITY. + * + * @return int + */ + public function getPriority() + { + return $this->priority; + } + + public function getName() + { + return self::getTaskName($this->getClassName(), $this->getMethodName(), $this->getMethodParameter()); + } + + static public function getTaskName($className, $methodName, $methodParameter = null) + { + return $className . '.' . $methodName . ($methodParameter == null ? '' : '_' . $methodParameter); + } } diff --git a/core/ScheduledTime.php b/core/ScheduledTime.php index 8dc8a483ad..0edcd839fd 100644 --- a/core/ScheduledTime.php +++ b/core/ScheduledTime.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -19,97 +19,98 @@ */ abstract class Piwik_ScheduledTime { - const PERIOD_NEVER = 'never'; - const PERIOD_DAY = 'day'; - const PERIOD_WEEK = 'week'; - const PERIOD_MONTH = 'month'; + const PERIOD_NEVER = 'never'; + const PERIOD_DAY = 'day'; + const PERIOD_WEEK = 'week'; + const PERIOD_MONTH = 'month'; + + /** + * @link http://php.net/manual/en/function.date.php, format string : 'G' + * Defaults to midnight + * @var integer + */ + public $hour = 0; - /** - * @link http://php.net/manual/en/function.date.php, format string : 'G' - * Defaults to midnight - * @var integer - */ - public $hour = 0; - - /** - * For weekly scheduling : http://php.net/manual/en/function.date.php, format string : 'N', defaults to Monday - * For monthly scheduling : day of the month (1 to 31) (note: will be capped at the latest day available the - * month), defaults to first day of the month - * @var integer - */ - public $day = 1; + /** + * For weekly scheduling : http://php.net/manual/en/function.date.php, format string : 'N', defaults to Monday + * For monthly scheduling : day of the month (1 to 31) (note: will be capped at the latest day available the + * month), defaults to first day of the month + * @var integer + */ + public $day = 1; - static public function getScheduledTimeForPeriod($period) - { - switch($period) - { - case self::PERIOD_MONTH: return new Piwik_ScheduledTime_Monthly(); - case self::PERIOD_WEEK: return new Piwik_ScheduledTime_Weekly(); - case self::PERIOD_DAY: return new Piwik_ScheduledTime_Daily(); + static public function getScheduledTimeForPeriod($period) + { + switch ($period) { + case self::PERIOD_MONTH: + return new Piwik_ScheduledTime_Monthly(); + case self::PERIOD_WEEK: + return new Piwik_ScheduledTime_Weekly(); + case self::PERIOD_DAY: + return new Piwik_ScheduledTime_Daily(); - default: throw new Exception('period ' . $period . 'is undefined.'); - } - } + default: + throw new Exception('period ' . $period . 'is undefined.'); + } + } - /** - * Returns the system time used by subclasses to compute schedulings. - * This method has been introduced so unit tests can override the current system time. + /** + * Returns the system time used by subclasses to compute schedulings. + * This method has been introduced so unit tests can override the current system time. * @return int - */ - protected function getTime() - { - return time(); - } + */ + protected function getTime() + { + return time(); + } - /** - * Computes the next scheduled time based on the system time at which the method has been called and - * the underlying scheduling interval. - * - * @abstract - * @return integer Returns the rescheduled time measured in the number of seconds since the Unix Epoch - */ - abstract public function getRescheduledTime(); + /** + * Computes the next scheduled time based on the system time at which the method has been called and + * the underlying scheduling interval. + * + * @abstract + * @return integer Returns the rescheduled time measured in the number of seconds since the Unix Epoch + */ + abstract public function getRescheduledTime(); - /** + /** * @abstract - * @param int $_day the day to set - * @throws Exception if method not supported by subclass or parameter _day is invalid - */ - abstract public function setDay($_day); + * @param int $_day the day to set + * @throws Exception if method not supported by subclass or parameter _day is invalid + */ + abstract public function setDay($_day); + + /** + * @param int $_hour the hour to set, has to be >= 0 and < 24 + * @throws Exception if method not supported by subclass or parameter _hour is invalid + */ + public function setHour($_hour) + { + if (!($_hour >= 0 && $_hour < 24)) { + throw new Exception ("Invalid hour parameter, must be >=0 and < 24"); + } - /** - * @param int $_hour the hour to set, has to be >= 0 and < 24 - * @throws Exception if method not supported by subclass or parameter _hour is invalid - */ - public function setHour($_hour) - { - if (!($_hour >=0 && $_hour < 24)) - { - throw new Exception ("Invalid hour parameter, must be >=0 and < 24"); - } + $this->hour = $_hour; + } - $this->hour = $_hour; - } - - /** - * Computes the delta in seconds needed to adjust the rescheduled time to the required hour. - * - * @param int $rescheduledTime The rescheduled time to be adjusted - * @return int adjusted rescheduled time - */ - protected function adjustHour ($rescheduledTime) - { - if ( $this->hour !== null ) - { - // Reset the number of minutes and set the scheduled hour to the one specified with setHour() - $rescheduledTime = mktime ( $this->hour, - 0, - date('s', $rescheduledTime), - date('n', $rescheduledTime), - date('j', $rescheduledTime), - date('Y', $rescheduledTime) - ); - } - return $rescheduledTime; - } + /** + * Computes the delta in seconds needed to adjust the rescheduled time to the required hour. + * + * @param int $rescheduledTime The rescheduled time to be adjusted + * @return int adjusted rescheduled time + */ + protected function adjustHour($rescheduledTime) + { + if ($this->hour !== null) { + // Reset the number of minutes and set the scheduled hour to the one specified with setHour() + $rescheduledTime = mktime($this->hour, + 0, + date('s', $rescheduledTime), + date('n', $rescheduledTime), + date('j', $rescheduledTime), + date('Y', $rescheduledTime) + ); + } + return $rescheduledTime; + } } diff --git a/core/ScheduledTime/Daily.php b/core/ScheduledTime/Daily.php index ed228fe8cc..46efdd0ddf 100644 --- a/core/ScheduledTime/Daily.php +++ b/core/ScheduledTime/Daily.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -17,28 +17,28 @@ * @subpackage Piwik_ScheduledTime */ class Piwik_ScheduledTime_Daily extends Piwik_ScheduledTime -{ - public function getRescheduledTime() - { - $currentTime = $this->getTime(); - - // Add one day - $rescheduledTime = mktime ( date('H', $currentTime), - date('i', $currentTime), - date('s', $currentTime), - date('n', $currentTime), - date('j', $currentTime) + 1, - date('Y', $currentTime) - ); +{ + public function getRescheduledTime() + { + $currentTime = $this->getTime(); + + // Add one day + $rescheduledTime = mktime(date('H', $currentTime), + date('i', $currentTime), + date('s', $currentTime), + date('n', $currentTime), + date('j', $currentTime) + 1, + date('Y', $currentTime) + ); + + // Adjusts the scheduled hour + $rescheduledTime = $this->adjustHour($rescheduledTime); - // Adjusts the scheduled hour - $rescheduledTime = $this->adjustHour($rescheduledTime); + return $rescheduledTime; + } - return $rescheduledTime; - } - - public function setDay($_day) - { - throw new Exception ("Method not supported"); - } + public function setDay($_day) + { + throw new Exception ("Method not supported"); + } } diff --git a/core/ScheduledTime/Hourly.php b/core/ScheduledTime/Hourly.php index 6c85d15964..fa5a4a3230 100644 --- a/core/ScheduledTime/Hourly.php +++ b/core/ScheduledTime/Hourly.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -18,28 +18,28 @@ */ class Piwik_ScheduledTime_Hourly extends Piwik_ScheduledTime { - public function getRescheduledTime() - { - $currentTime = $this->getTime(); - - // Adds one hour and reset the number of minutes - $rescheduledTime = mktime ( date('H', $currentTime) + 1, - 0, - date('s', $currentTime), - date('n', $currentTime), - date('j', $currentTime), - date('Y', $currentTime) - ); - return $rescheduledTime; - } + public function getRescheduledTime() + { + $currentTime = $this->getTime(); + + // Adds one hour and reset the number of minutes + $rescheduledTime = mktime(date('H', $currentTime) + 1, + 0, + date('s', $currentTime), + date('n', $currentTime), + date('j', $currentTime), + date('Y', $currentTime) + ); + return $rescheduledTime; + } + + public function setHour($_hour) + { + throw new Exception ("Method not supported"); + } - public function setHour($_hour) - { - throw new Exception ("Method not supported"); - } - - public function setDay($_day) - { - throw new Exception ("Method not supported"); - } + public function setDay($_day) + { + throw new Exception ("Method not supported"); + } } diff --git a/core/ScheduledTime/Monthly.php b/core/ScheduledTime/Monthly.php index c259af0051..e09777a23a 100644 --- a/core/ScheduledTime/Monthly.php +++ b/core/ScheduledTime/Monthly.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -18,106 +18,101 @@ */ class Piwik_ScheduledTime_Monthly extends Piwik_ScheduledTime { - /** - * Day of the week for scheduled time. - * - * @var int - */ - private $dayOfWeek = null; - - /** - * Week number for scheduled time. - * - * @var int - */ - private $week = null; - + /** + * Day of the week for scheduled time. + * + * @var int + */ + private $dayOfWeek = null; + + /** + * Week number for scheduled time. + * + * @var int + */ + private $week = null; + /** * @return int */ public function getRescheduledTime() - { - $currentTime = $this->getTime(); + { + $currentTime = $this->getTime(); - // Adds one month - $rescheduledTime = mktime ( date('H', $currentTime), - date('i', $currentTime), - date('s', $currentTime), - date('n', $currentTime) + 1, - 1, - date('Y', $currentTime) - ); + // Adds one month + $rescheduledTime = mktime(date('H', $currentTime), + date('i', $currentTime), + date('s', $currentTime), + date('n', $currentTime) + 1, + 1, + date('Y', $currentTime) + ); - $nextMonthLength = date('t', $rescheduledTime); + $nextMonthLength = date('t', $rescheduledTime); - // Sets scheduled day + // Sets scheduled day $scheduledDay = date('j', $currentTime); - if ( $this->day !== null ) - { - $scheduledDay = $this->day; - } - - if ($this->dayOfWeek !== null - && $this->week !== null) - { - $newTime = $rescheduledTime + $this->week * 7 * 86400; - while (date("w", $newTime) != $this->dayOfWeek % 7) // modulus for sanity check - { - $newTime += 86400; - } - $scheduledDay = ($newTime - $rescheduledTime) / 86400 + 1; - } - - // Caps scheduled day - if ( $scheduledDay > $nextMonthLength ) - { - $scheduledDay = $nextMonthLength; - } - - // Adjusts the scheduled day - $rescheduledTime += ($scheduledDay - 1) * 86400; - - // Adjusts the scheduled hour - $rescheduledTime = $this->adjustHour($rescheduledTime); - - return $rescheduledTime; - } - - /** - * @param int $_day the day to set, has to be >= 1 and < 32 - * @throws Exception if parameter _day is invalid - */ - public function setDay($_day) - { - if (!($_day >=1 && $_day < 32)) - { - throw new Exception ("Invalid day parameter, must be >=1 and < 32"); - } - - $this->day = $_day; - } - - /** - * Makes this scheduled time execute on a particular day of the week on each month. - * - * @param int $_day the day of the week to use, between 0-6 (inclusive). 0 -> Sunday - * @param int $_week the week to use, between 0-3 (inclusive) - * @throws Exception if either parameter is invalid - */ - public function setDayOfWeek($_day, $_week) - { - if (!($_day >= 0 && $_day < 7)) - { - throw new Exception("Invalid day of week parameter, must be >= 0 & < 7"); - } - - if (!($_week >= 0 && $_week < 4)) - { - throw new Exception("Invalid week number, must be >= 1 & < 4"); - } - - $this->dayOfWeek = $_day; - $this->week = $_week; - } + if ($this->day !== null) { + $scheduledDay = $this->day; + } + + if ($this->dayOfWeek !== null + && $this->week !== null + ) { + $newTime = $rescheduledTime + $this->week * 7 * 86400; + while (date("w", $newTime) != $this->dayOfWeek % 7) // modulus for sanity check + { + $newTime += 86400; + } + $scheduledDay = ($newTime - $rescheduledTime) / 86400 + 1; + } + + // Caps scheduled day + if ($scheduledDay > $nextMonthLength) { + $scheduledDay = $nextMonthLength; + } + + // Adjusts the scheduled day + $rescheduledTime += ($scheduledDay - 1) * 86400; + + // Adjusts the scheduled hour + $rescheduledTime = $this->adjustHour($rescheduledTime); + + return $rescheduledTime; + } + + /** + * @param int $_day the day to set, has to be >= 1 and < 32 + * @throws Exception if parameter _day is invalid + */ + public function setDay($_day) + { + if (!($_day >= 1 && $_day < 32)) { + throw new Exception ("Invalid day parameter, must be >=1 and < 32"); + } + + $this->day = $_day; + } + + /** + * Makes this scheduled time execute on a particular day of the week on each month. + * + * @param int $_day the day of the week to use, between 0-6 (inclusive). 0 -> Sunday + * @param int $_week the week to use, between 0-3 (inclusive) + * @throws Exception if either parameter is invalid + */ + public function setDayOfWeek($_day, $_week) + { + if (!($_day >= 0 && $_day < 7)) { + throw new Exception("Invalid day of week parameter, must be >= 0 & < 7"); + } + + if (!($_week >= 0 && $_week < 4)) { + throw new Exception("Invalid week number, must be >= 1 & < 4"); + } + + $this->dayOfWeek = $_day; + $this->week = $_week; + } } diff --git a/core/ScheduledTime/Weekly.php b/core/ScheduledTime/Weekly.php index fe2f0eee0f..d47cf53350 100644 --- a/core/ScheduledTime/Weekly.php +++ b/core/ScheduledTime/Weekly.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -18,50 +18,48 @@ */ class Piwik_ScheduledTime_Weekly extends Piwik_ScheduledTime { - - public function getRescheduledTime() - { - $currentTime = $this->getTime(); - - // Adds 7 days - $rescheduledTime = mktime ( date('H', $currentTime), - date('i', $currentTime), - date('s', $currentTime), - date('n', $currentTime), - date('j', $currentTime) + 7, - date('Y', $currentTime) - ); - - // Adjusts the scheduled hour - $rescheduledTime = $this->adjustHour($rescheduledTime); - - // Adjusts the scheduled day - if ( $this->day !== null ) - { - // Removes or adds a number of days to set the scheduled day to the one specified with setDay() - $rescheduledTime = mktime ( date('H', $rescheduledTime), - date('i', $rescheduledTime), - date('s', $rescheduledTime), - date('n', $rescheduledTime), - date('j', $rescheduledTime) - (date('N', $rescheduledTime) - $this->day), - date('Y', $rescheduledTime) - ); - } - - return $rescheduledTime; - } - - /** - * @param int $_day the day to set, has to be >= 1 and < 8 - * @throws Exception if parameter _day is invalid - */ - public function setDay($_day) - { - if (!($_day >=1 && $_day < 8)) - { - throw new Exception ("Invalid day parameter, must be >=1 and < 8"); - } - $this->day = $_day; - } + public function getRescheduledTime() + { + $currentTime = $this->getTime(); + + // Adds 7 days + $rescheduledTime = mktime(date('H', $currentTime), + date('i', $currentTime), + date('s', $currentTime), + date('n', $currentTime), + date('j', $currentTime) + 7, + date('Y', $currentTime) + ); + + // Adjusts the scheduled hour + $rescheduledTime = $this->adjustHour($rescheduledTime); + + // Adjusts the scheduled day + if ($this->day !== null) { + // Removes or adds a number of days to set the scheduled day to the one specified with setDay() + $rescheduledTime = mktime(date('H', $rescheduledTime), + date('i', $rescheduledTime), + date('s', $rescheduledTime), + date('n', $rescheduledTime), + date('j', $rescheduledTime) - (date('N', $rescheduledTime) - $this->day), + date('Y', $rescheduledTime) + ); + } + + return $rescheduledTime; + } + + /** + * @param int $_day the day to set, has to be >= 1 and < 8 + * @throws Exception if parameter _day is invalid + */ + public function setDay($_day) + { + if (!($_day >= 1 && $_day < 8)) { + throw new Exception ("Invalid day parameter, must be >=1 and < 8"); + } + + $this->day = $_day; + } } diff --git a/core/Segment.php b/core/Segment.php index a72c7b0797..7802ba440d 100644 --- a/core/Segment.php +++ b/core/Segment.php @@ -19,24 +19,24 @@ class Piwik_Segment * @var Piwik_SegmentExpression */ protected $segment = null; - + /** * Truncate the Segments to 4k */ const SEGMENT_TRUNCATE_LIMIT = 4096; - + public function __construct($string, $idSites) { - $string = Piwik_Common::unsanitizeInputValue($string); + $string = Piwik_Common::unsanitizeInputValue($string); $string = trim($string); - if( !Piwik_Archive::isSegmentationEnabled() - && !empty($string)) - { - throw new Exception("The Super User has disabled the Segmentation feature."); - } + if (!Piwik_Archive::isSegmentationEnabled() + && !empty($string) + ) { + throw new Exception("The Super User has disabled the Segmentation feature."); + } // As a preventive measure, we restrict the filter size to a safe limit $string = substr($string, 0, self::SEGMENT_TRUNCATE_LIMIT); - + $this->string = $string; $this->idSites = $idSites; $segment = new Piwik_SegmentExpression($string); @@ -44,13 +44,12 @@ class Piwik_Segment // parse segments $expressions = $segment->parseSubExpressions(); - + // convert segments name to sql segment // check that user is allowed to view this segment // and apply a filter to the value to match if necessary (to map DB fields format) $cleanedExpressions = array(); - foreach($expressions as $expression) - { + foreach ($expressions as $expression) { $operand = $expression[Piwik_SegmentExpression::INDEX_OPERAND]; $cleanedExpression = $this->getCleanedExpression($operand); $expression[Piwik_SegmentExpression::INDEX_OPERAND] = $cleanedExpression; @@ -58,16 +57,17 @@ class Piwik_Segment } $segment->setSubExpressionsAfterCleanup($cleanedExpressions); } - + public function getPrettyString() { - //@TODO segment.getPrettyString + //@TODO segment.getPrettyString } - + public function isEmpty() { return empty($this->string); } + protected $availableSegments = array(); protected $segmentsHumanReadable = ''; @@ -75,353 +75,313 @@ class Piwik_Segment { $expressions = $this->segment->parsedSubExpressions; $uniqueFields = array(); - foreach($expressions as $expression) - { - $uniqueFields[] = $expression[Piwik_SegmentExpression::INDEX_OPERAND][0]; + foreach ($expressions as $expression) { + $uniqueFields[] = $expression[Piwik_SegmentExpression::INDEX_OPERAND][0]; } return $uniqueFields; } - + protected function getCleanedExpression($expression) { - if(empty($this->availableSegments)) - { + if (empty($this->availableSegments)) { $this->availableSegments = Piwik_API_API::getInstance()->getSegmentsMetadata($this->idSites, $_hideImplementationData = false); } - + $name = $expression[0]; $matchType = $expression[1]; $value = $expression[2]; $sqlName = ''; - - foreach($this->availableSegments as $segment) - { - if($segment['segment'] != $name) - { + + foreach ($this->availableSegments as $segment) { + if ($segment['segment'] != $name) { continue; } - + $sqlName = $segment['sqlSegment']; - + // check permission - if(isset($segment['permission']) - && $segment['permission'] != 1) - { - throw new Exception("You do not have enough permission to access the segment ".$name); + if (isset($segment['permission']) + && $segment['permission'] != 1 + ) { + throw new Exception("You do not have enough permission to access the segment " . $name); } - + // $this->segmentsHumanReadable[] = $segment['name'] . " " . // $this->getNameForMatchType($matchType) . // $value; - + // apply presentation filter - if(isset($segment['sqlFilter']) - && !empty($segment['sqlFilter'])) - { + if (isset($segment['sqlFilter']) + && !empty($segment['sqlFilter']) + ) { $value = call_user_func($segment['sqlFilter'], $value, $segment['sqlSegment'], $matchType); - + // sqlFilter-callbacks might return arrays for more complex cases // e.g. see Piwik_Actions::getIdActionFromSegment() if (is_array($value) - && isset($value['SQL'])) - { + && isset($value['SQL']) + ) { // Special case: returned value is a sub sql expression! $matchType = Piwik_SegmentExpression::MATCH_ACTIONS_CONTAINS; } } break; } - - if(empty($sqlName)) - { + + if (empty($sqlName)) { throw new Exception("Segment '$name' is not a supported segment."); } - - return array( $sqlName, $matchType, $value ); + + return array($sqlName, $matchType, $value); } - + public function getString() { return $this->string; } - + public function getHash() { - if(empty($this->string)) - { + if (empty($this->string)) { return ''; } return md5($this->string); } - /** - * Extend SQL query with segment expressions - * - * @param string $select select clause - * @param array $from array of table names (without prefix) - * @param bool|string $where (optional )where clause - * @param array|string $bind (optional) params to bind - * @param bool|string $orderBy (optional) order by clause - * @param bool|string $groupBy (optional) group by clause - * @return string entire select query - */ - public function getSelectQuery($select, $from, $where=false, $bind=array(), $orderBy=false, $groupBy=false) + /** + * Extend SQL query with segment expressions + * + * @param string $select select clause + * @param array $from array of table names (without prefix) + * @param bool|string $where (optional )where clause + * @param array|string $bind (optional) params to bind + * @param bool|string $orderBy (optional) order by clause + * @param bool|string $groupBy (optional) group by clause + * @return string entire select query + */ + public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false) { - $joinWithSubSelect = false; - - if (!is_array($from)) - { - $from = array($from); - } - - if (!$this->isEmpty()) - { - $this->segment->parseSubExpressionsIntoSqlExpressions($from); - - $joins = $this->generateJoins($from); - $from = $joins['sql']; - $joinWithSubSelect = $joins['joinWithSubSelect']; - - $segmentSql = $this->segment->getSql(); - $segmentWhere = $segmentSql['where']; - if (!empty($segmentWhere)) - { - if (!empty($where)) - { - $where = "( $where ) + $joinWithSubSelect = false; + + if (!is_array($from)) { + $from = array($from); + } + + if (!$this->isEmpty()) { + $this->segment->parseSubExpressionsIntoSqlExpressions($from); + + $joins = $this->generateJoins($from); + $from = $joins['sql']; + $joinWithSubSelect = $joins['joinWithSubSelect']; + + $segmentSql = $this->segment->getSql(); + $segmentWhere = $segmentSql['where']; + if (!empty($segmentWhere)) { + if (!empty($where)) { + $where = "( $where ) AND ($segmentWhere)"; - } - else - { - $where = $segmentWhere; - } - } - - $bind = array_merge($bind, $segmentSql['bind']); - } - else - { - $joins = $this->generateJoins($from); - $from = $joins['sql']; - $joinWithSubSelect = $joins['joinWithSubSelect']; - } - - if ($joinWithSubSelect) - { - $sql = $this->buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy); - } - else - { - $sql = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); - } - - return array( - 'sql' => $sql, - 'bind' => $bind - ); + } else { + $where = $segmentWhere; + } + } + + $bind = array_merge($bind, $segmentSql['bind']); + } else { + $joins = $this->generateJoins($from); + $from = $joins['sql']; + $joinWithSubSelect = $joins['joinWithSubSelect']; + } + + if ($joinWithSubSelect) { + $sql = $this->buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy); + } else { + $sql = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); + } + + return array( + 'sql' => $sql, + 'bind' => $bind + ); } - /** - * Generate the join sql based on the needed tables - * @param array $tables tables to join - * @throws Exception if tables can't be joined - * @return array - */ + /** + * Generate the join sql based on the needed tables + * @param array $tables tables to join + * @throws Exception if tables can't be joined + * @return array + */ private function generateJoins($tables) { - $knownTables = array("log_visit", "log_link_visit_action", "log_conversion"); - $visitsAvailable = $actionsAvailable = $conversionsAvailable = false; - $joinWithSubSelect = false; - $sql = ''; - - // make sure the tables are joined in the right order - // base table first, then action before conversion - // this way, conversions can be joined on idlink_va - $actionIndex = array_search("log_link_visit_action", $tables); - $conversionIndex = array_search("log_conversion", $tables); - if ($actionIndex > 0 && $conversionIndex > 0 && $actionIndex > $conversionIndex) - { - $tables[$actionIndex] = "log_conversion"; - $tables[$conversionIndex] = "log_link_visit_action"; - } - - // same as above: action before visit - $actionIndex = array_search("log_link_visit_action", $tables); - $visitIndex = array_search("log_visit", $tables); - if ($actionIndex > 0 && $visitIndex > 0 && $actionIndex > $visitIndex) - { - $tables[$actionIndex] = "log_visit"; - $tables[$visitIndex] = "log_link_visit_action"; - } - - foreach ($tables as $i => $table) - { - if (is_array($table)) - { - // join condition provided - $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table']; - $sql .= " - LEFT JOIN ".Piwik_Common::prefixTable($table['table'])." AS ".$alias - ." ON ".$table['joinOn']; - continue; - } - - if (!in_array($table, $knownTables)) - { - throw new Exception("Table '$table' can't be used for segmentation"); - } - - $tableSql = Piwik_Common::prefixTable($table)." AS $table"; - - if ($i == 0) - { - // first table - $sql .= $tableSql; - } - else - { - $join = ""; - - if ($actionsAvailable && $table == "log_conversion") - { - // have actions, need conversions => join on idlink_va - $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va " - ."AND log_conversion.idsite = log_link_visit_action.idsite"; - } - else if ($actionsAvailable && $table == "log_visit") - { - // have actions, need visits => join on idvisit - $join = "log_visit.idvisit = log_link_visit_action.idvisit"; - } - else if ($visitsAvailable && $table == "log_link_visit_action") - { - // have visits, need actions => we have to use a more complex join - // we don't hande this here, we just return joinWithSubSelect=true in this case - $joinWithSubSelect = true; - $join = "log_link_visit_action.idvisit = log_visit.idvisit"; - } - else if ($conversionsAvailable && $table == "log_link_visit_action") - { - // have conversions, need actions => join on idlink_va - $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va"; - } - else if (($visitsAvailable && $table == "log_conversion") - ||($conversionsAvailable && $table == "log_visit")) - { - // have visits, need conversion (or vice versa) => join on idvisit - // notice that joining conversions on visits has lower priority than joining it on actions - $join = "log_conversion.idvisit = log_visit.idvisit"; - - // if conversions are joined on visits, we need a complex join - if ($table == "log_conversion") - { - $joinWithSubSelect = true; - } - } - else - { - throw new Exception("Table '$table', can't be joined for segmentation"); - } - - // the join sql the default way - $sql .= " + $knownTables = array("log_visit", "log_link_visit_action", "log_conversion"); + $visitsAvailable = $actionsAvailable = $conversionsAvailable = false; + $joinWithSubSelect = false; + $sql = ''; + + // make sure the tables are joined in the right order + // base table first, then action before conversion + // this way, conversions can be joined on idlink_va + $actionIndex = array_search("log_link_visit_action", $tables); + $conversionIndex = array_search("log_conversion", $tables); + if ($actionIndex > 0 && $conversionIndex > 0 && $actionIndex > $conversionIndex) { + $tables[$actionIndex] = "log_conversion"; + $tables[$conversionIndex] = "log_link_visit_action"; + } + + // same as above: action before visit + $actionIndex = array_search("log_link_visit_action", $tables); + $visitIndex = array_search("log_visit", $tables); + if ($actionIndex > 0 && $visitIndex > 0 && $actionIndex > $visitIndex) { + $tables[$actionIndex] = "log_visit"; + $tables[$visitIndex] = "log_link_visit_action"; + } + + foreach ($tables as $i => $table) { + if (is_array($table)) { + // join condition provided + $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table']; + $sql .= " + LEFT JOIN " . Piwik_Common::prefixTable($table['table']) . " AS " . $alias + . " ON " . $table['joinOn']; + continue; + } + + if (!in_array($table, $knownTables)) { + throw new Exception("Table '$table' can't be used for segmentation"); + } + + $tableSql = Piwik_Common::prefixTable($table) . " AS $table"; + + if ($i == 0) { + // first table + $sql .= $tableSql; + } else { + $join = ""; + + if ($actionsAvailable && $table == "log_conversion") { + // have actions, need conversions => join on idlink_va + $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va " + . "AND log_conversion.idsite = log_link_visit_action.idsite"; + } else if ($actionsAvailable && $table == "log_visit") { + // have actions, need visits => join on idvisit + $join = "log_visit.idvisit = log_link_visit_action.idvisit"; + } else if ($visitsAvailable && $table == "log_link_visit_action") { + // have visits, need actions => we have to use a more complex join + // we don't hande this here, we just return joinWithSubSelect=true in this case + $joinWithSubSelect = true; + $join = "log_link_visit_action.idvisit = log_visit.idvisit"; + } else if ($conversionsAvailable && $table == "log_link_visit_action") { + // have conversions, need actions => join on idlink_va + $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va"; + } else if (($visitsAvailable && $table == "log_conversion") + || ($conversionsAvailable && $table == "log_visit") + ) { + // have visits, need conversion (or vice versa) => join on idvisit + // notice that joining conversions on visits has lower priority than joining it on actions + $join = "log_conversion.idvisit = log_visit.idvisit"; + + // if conversions are joined on visits, we need a complex join + if ($table == "log_conversion") { + $joinWithSubSelect = true; + } + } else { + throw new Exception("Table '$table', can't be joined for segmentation"); + } + + // the join sql the default way + $sql .= " LEFT JOIN $tableSql ON $join"; - } - - // remember which tables are available - $visitsAvailable = ($visitsAvailable || $table == "log_visit"); - $actionsAvailable = ($actionsAvailable || $table == "log_link_visit_action"); - $conversionsAvailable = ($conversionsAvailable || $table == "log_conversion"); - } - - return array( - 'sql' => $sql, - 'joinWithSubSelect' => $joinWithSubSelect - ); + } + + // remember which tables are available + $visitsAvailable = ($visitsAvailable || $table == "log_visit"); + $actionsAvailable = ($actionsAvailable || $table == "log_link_visit_action"); + $conversionsAvailable = ($conversionsAvailable || $table == "log_conversion"); + } + + return array( + 'sql' => $sql, + 'joinWithSubSelect' => $joinWithSubSelect + ); } - /** - * Build select query the normal way - * @param string $select fieldlist to be selected - * @param string $from tablelist to select from - * @param string $where where clause - * @param string $orderBy order by clause - * @param string $groupBy group by clause - * @return string - */ + /** + * Build select query the normal way + * @param string $select fieldlist to be selected + * @param string $from tablelist to select from + * @param string $where where clause + * @param string $orderBy order by clause + * @param string $groupBy group by clause + * @return string + */ private function buildSelectQuery($select, $from, $where, $orderBy, $groupBy) { - $sql = " + $sql = " SELECT $select FROM $from"; - - if ($where) - { - $sql .= " + + if ($where) { + $sql .= " WHERE $where"; - } + } - if ($groupBy) - { - $sql .= " + if ($groupBy) { + $sql .= " GROUP BY $groupBy"; - } - - if ($orderBy) - { - $sql .= " + } + + if ($orderBy) { + $sql .= " ORDER BY $orderBy"; - } - - return $sql; + } + + return $sql; } - /** - * Build a select query where actions have to be joined on visits (or conversions) - * In this case, the query gets wrapped in another query so that grouping by visit is possible - * @param string $select - * @param string $from - * @param string $where - * @param string $orderBy - * @param string $groupBy - * @throws Exception - * @return string - */ + /** + * Build a select query where actions have to be joined on visits (or conversions) + * In this case, the query gets wrapped in another query so that grouping by visit is possible + * @param string $select + * @param string $from + * @param string $where + * @param string $orderBy + * @param string $groupBy + * @throws Exception + * @return string + */ private function buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy) - { - preg_match_all("/(log_visit|log_conversion|log_action).[a-z0-9_\*]+/", $select, $matches); - $neededFields = array_unique($matches[0]); - - if (count($neededFields) == 0) - { - throw new Exception("No needed fields found in select expression. " - ."Please use a table prefix."); - } - - $select = preg_replace('/(log_visit|log_conversion|log_action)\./', 'log_inner.', $select); - $orderBy = preg_replace('/(log_visit|log_conversion|log_action)\./', 'log_inner.', $orderBy); - $groupBy = preg_replace('/(log_visit|log_conversion|log_action)\./', 'log_inner.', $groupBy); - - $from = "( + { + preg_match_all("/(log_visit|log_conversion|log_action).[a-z0-9_\*]+/", $select, $matches); + $neededFields = array_unique($matches[0]); + + if (count($neededFields) == 0) { + throw new Exception("No needed fields found in select expression. " + . "Please use a table prefix."); + } + + $select = preg_replace('/(log_visit|log_conversion|log_action)\./', 'log_inner.', $select); + $orderBy = preg_replace('/(log_visit|log_conversion|log_action)\./', 'log_inner.', $orderBy); + $groupBy = preg_replace('/(log_visit|log_conversion|log_action)\./', 'log_inner.', $groupBy); + + $from = "( SELECT - ".implode(", - ", $neededFields)." + " . implode(", + ", $neededFields) . " FROM $from WHERE $where GROUP BY log_visit.idvisit ) AS log_inner"; - - $where = false; - return $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); + + $where = false; + return $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy); } - + } diff --git a/core/SegmentExpression.php b/core/SegmentExpression.php index aaae328462..9340aec4bd 100644 --- a/core/SegmentExpression.php +++ b/core/SegmentExpression.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -17,7 +17,7 @@ class Piwik_SegmentExpression { const AND_DELIMITER = ';'; const OR_DELIMITER = ','; - + const MATCH_EQUAL = '=='; const MATCH_NOT_EQUAL = '!='; const MATCH_GREATER_OR_EQUAL = '>='; @@ -26,226 +26,222 @@ class Piwik_SegmentExpression const MATCH_LESS = '<'; const MATCH_CONTAINS = '=@'; const MATCH_DOES_NOT_CONTAIN = '!@'; - + // Special case, since we look up Page URLs/Page titles in a sub SQL query const MATCH_ACTIONS_CONTAINS = 'IN'; - + const INDEX_BOOL_OPERATOR = 0; const INDEX_OPERAND = 1; - + function __construct($string) { $this->string = $string; $this->tree = $this->parseTree(); } + protected $joins = array(); protected $valuesBind = array(); protected $parsedTree = array(); protected $tree = array(); - protected $parsedSubExpressions = array(); - - /** - * Given the array of parsed filters containing, for each filter, - * the boolean operator (AND/OR) and the operand, - * Will return the array where the filters are in SQL representation - * - * @throws Exception - * @return array - */ + protected $parsedSubExpressions = array(); + + /** + * Given the array of parsed filters containing, for each filter, + * the boolean operator (AND/OR) and the operand, + * Will return the array where the filters are in SQL representation + * + * @throws Exception + * @return array + */ public function parseSubExpressions() { $parsedSubExpressions = array(); - foreach($this->tree as $id => $leaf) - { + foreach ($this->tree as $id => $leaf) { $operand = $leaf[self::INDEX_OPERAND]; $operator = $leaf[self::INDEX_BOOL_OPERATOR]; - $pattern = '/^(.+?)(' .self::MATCH_EQUAL.'|' - .self::MATCH_NOT_EQUAL.'|' - .self::MATCH_GREATER_OR_EQUAL.'|' - .self::MATCH_GREATER.'|' - .self::MATCH_LESS_OR_EQUAL.'|' - .self::MATCH_LESS.'|' - .self::MATCH_CONTAINS.'|' - .self::MATCH_DOES_NOT_CONTAIN - .'){1}(.+)/'; - $match = preg_match( $pattern, $operand, $matches ); - if($match == 0) - { - throw new Exception('Segment parameter \''.$operand.'\' does not appear to have a valid format.'); + $pattern = '/^(.+?)(' . self::MATCH_EQUAL . '|' + . self::MATCH_NOT_EQUAL . '|' + . self::MATCH_GREATER_OR_EQUAL . '|' + . self::MATCH_GREATER . '|' + . self::MATCH_LESS_OR_EQUAL . '|' + . self::MATCH_LESS . '|' + . self::MATCH_CONTAINS . '|' + . self::MATCH_DOES_NOT_CONTAIN + . '){1}(.+)/'; + $match = preg_match($pattern, $operand, $matches); + if ($match == 0) { + throw new Exception('Segment parameter \'' . $operand . '\' does not appear to have a valid format.'); } $leftMember = $matches[1]; $operation = $matches[2]; $valueRightMember = $matches[3]; - $parsedSubExpressions[] = array( + $parsedSubExpressions[] = array( self::INDEX_BOOL_OPERATOR => $operator, - self::INDEX_OPERAND => array( + self::INDEX_OPERAND => array( $leftMember, - $operation, - $valueRightMember, - )); + $operation, + $valueRightMember, + )); } $this->parsedSubExpressions = $parsedSubExpressions; return $parsedSubExpressions; } - /** - * Set the given expression - * @param $parsedSubExpressions - */ + /** + * Set the given expression + * @param $parsedSubExpressions + */ public function setSubExpressionsAfterCleanup($parsedSubExpressions) { $this->parsedSubExpressions = $parsedSubExpressions; } - /** - * Returns the current sub expression - * @return array - */ + /** + * Returns the current sub expression + * @return array + */ public function getSubExpressions() { return $this->parsedSubExpressions; } - /** - * @param array $availableTables - */ - public function parseSubExpressionsIntoSqlExpressions(&$availableTables=array()) + /** + * @param array $availableTables + */ + public function parseSubExpressionsIntoSqlExpressions(&$availableTables = array()) { $sqlSubExpressions = array(); $this->valuesBind = array(); $this->joins = array(); - - foreach($this->parsedSubExpressions as $leaf) - { + + foreach ($this->parsedSubExpressions as $leaf) { $operator = $leaf[self::INDEX_BOOL_OPERATOR]; $operandDefinition = $leaf[self::INDEX_OPERAND]; - + $operand = $this->getSqlMatchFromDefinition($operandDefinition, $availableTables); - + if ($operand[1] !== null) { $this->valuesBind[] = $operand[1]; } $operand = $operand[0]; $sqlSubExpressions[] = array( self::INDEX_BOOL_OPERATOR => $operator, - self::INDEX_OPERAND => $operand, - ); + self::INDEX_OPERAND => $operand, + ); } - + $this->tree = $sqlSubExpressions; } - /** - * Given an array representing one filter operand ( left member , operation , right member) - * Will return an array containing - * - the SQL substring, - * - the values to bind to this substring - * - * @param array $def - * @param array $availableTables - * @throws Exception - * @return array - */ + /** + * Given an array representing one filter operand ( left member , operation , right member) + * Will return an array containing + * - the SQL substring, + * - the values to bind to this substring + * + * @param array $def + * @param array $availableTables + * @throws Exception + * @return array + */ // @todo case insensitive? protected function getSqlMatchFromDefinition($def, &$availableTables) { - $field = $def[0]; - $matchType = $def[1]; + $field = $def[0]; + $matchType = $def[1]; $value = $def[2]; - - switch($matchType) - { - case self::MATCH_EQUAL: - $sqlMatch = '='; - break; - case self::MATCH_NOT_EQUAL: - $sqlMatch = '<>'; - break; - case self::MATCH_GREATER: - $sqlMatch = '>'; - break; - case self::MATCH_LESS: - $sqlMatch = '<'; - break; - case self::MATCH_GREATER_OR_EQUAL: - $sqlMatch = '>='; - break; - case self::MATCH_LESS_OR_EQUAL: - $sqlMatch = '<='; - break; - case self::MATCH_CONTAINS: - $sqlMatch = 'LIKE'; - $value = '%'.$this->escapeLikeString($value).'%'; - break; - case self::MATCH_DOES_NOT_CONTAIN: - $sqlMatch = 'NOT LIKE'; - $value = '%'.$this->escapeLikeString($value).'%'; - break; - + + switch ($matchType) { + case self::MATCH_EQUAL: + $sqlMatch = '='; + break; + case self::MATCH_NOT_EQUAL: + $sqlMatch = '<>'; + break; + case self::MATCH_GREATER: + $sqlMatch = '>'; + break; + case self::MATCH_LESS: + $sqlMatch = '<'; + break; + case self::MATCH_GREATER_OR_EQUAL: + $sqlMatch = '>='; + break; + case self::MATCH_LESS_OR_EQUAL: + $sqlMatch = '<='; + break; + case self::MATCH_CONTAINS: + $sqlMatch = 'LIKE'; + $value = '%' . $this->escapeLikeString($value) . '%'; + break; + case self::MATCH_DOES_NOT_CONTAIN: + $sqlMatch = 'NOT LIKE'; + $value = '%' . $this->escapeLikeString($value) . '%'; + break; + case self::MATCH_ACTIONS_CONTAINS: // this match type is not accessible from the outside // (it won't be matched in self::parseSubExpressions()) // it can be used internally to inject sub-expressions into the query. // see Piwik_Segment::getCleanedExpression() - $sqlMatch = 'IN ('.$value['SQL'].')'; + $sqlMatch = 'IN (' . $value['SQL'] . ')'; $value = $this->escapeLikeString($value['bind']); break; - default: - throw new Exception("Filter contains the match type '".$matchType."' which is not supported"); - break; + default: + throw new Exception("Filter contains the match type '" . $matchType . "' which is not supported"); + break; } - + if ($matchType === self::MATCH_ACTIONS_CONTAINS) { $sqlExpression = "$field $sqlMatch"; } else { $sqlExpression = "$field $sqlMatch ?"; } - + $this->checkFieldIsAvailable($field, $availableTables); - + return array($sqlExpression, $value); } - /** - * Check whether the field is available - * If not, add it to the available tables - * - * @param string $field - * @param array $availableTables - */ + /** + * Check whether the field is available + * If not, add it to the available tables + * + * @param string $field + * @param array $availableTables + */ private function checkFieldIsAvailable($field, &$availableTables) { $fieldParts = explode('.', $field); - + $table = count($fieldParts) == 2 ? $fieldParts[0] : false; - + // remove sql functions from field name // example: `HOUR(log_visit.visit_last_action_time)` gets `HOUR(log_visit` => remove `HOUR(` $table = preg_replace('/^[A-Z_]+\(/', '', $table); $tableExists = !$table || in_array($table, $availableTables); - - if (!$tableExists) - { - $availableTables[] = $table; + + if (!$tableExists) { + $availableTables[] = $table; } } - /** - * Escape the characters % and _ in the given string - * @param string $str - * @return string - */ + /** + * Escape the characters % and _ in the given string + * @param string $str + * @return string + */ private function escapeLikeString($str) { - $str = str_replace("%", "\%", $str); - $str = str_replace("_", "\_", $str); - return $str; + $str = str_replace("%", "\%", $str); + $str = str_replace("_", "\_", $str); + return $str; } - + /** - * Given a filter string, - * will parse it into an array where each row contains the boolean operator applied to it, + * Given a filter string, + * will parse it into an array where each row contains the boolean operator applied to it, * and the operand * * @return array @@ -253,7 +249,7 @@ class Piwik_SegmentExpression protected function parseTree() { $string = $this->string; - if(empty($string)) { + if (empty($string)) { return array(); } $tree = array(); @@ -261,101 +257,87 @@ class Piwik_SegmentExpression $length = strlen($string); $isBackslash = false; $operand = ''; - while($i <= $length) - { + while ($i <= $length) { $char = $string[$i]; $isAND = ($char == self::AND_DELIMITER); $isOR = ($char == self::OR_DELIMITER); - $isEnd = ($length == $i+1); - - if($isEnd) - { - if($isBackslash && ($isAND || $isOR)) - { - $operand = substr($operand, 0, -1); - } + $isEnd = ($length == $i + 1); + + if ($isEnd) { + if ($isBackslash && ($isAND || $isOR)) { + $operand = substr($operand, 0, -1); + } $operand .= $char; $tree[] = array(self::INDEX_BOOL_OPERATOR => '', self::INDEX_OPERAND => $operand); break; } - - if($isAND && !$isBackslash) - { - $tree[] = array(self::INDEX_BOOL_OPERATOR => 'AND', self::INDEX_OPERAND => $operand); - $operand = ''; - } - elseif($isOR && !$isBackslash) - { - $tree[] = array(self::INDEX_BOOL_OPERATOR => 'OR', self::INDEX_OPERAND => $operand); - $operand = ''; - } - else - { - if($isBackslash && ($isAND || $isOR)) - { - $operand = substr($operand, 0, -1); - } - $operand .= $char; - } + + if ($isAND && !$isBackslash) { + $tree[] = array(self::INDEX_BOOL_OPERATOR => 'AND', self::INDEX_OPERAND => $operand); + $operand = ''; + } elseif ($isOR && !$isBackslash) { + $tree[] = array(self::INDEX_BOOL_OPERATOR => 'OR', self::INDEX_OPERAND => $operand); + $operand = ''; + } else { + if ($isBackslash && ($isAND || $isOR)) { + $operand = substr($operand, 0, -1); + } + $operand .= $char; + } $isBackslash = ($char == "\\"); $i++; } return $tree; } - /** - * Given the array of parsed boolean logic, will return - * an array containing the full SQL string representing the filter, - * the needed joins and the values to bind to the query - * - * @throws Exception - * @return array SQL Query, Joins and Bind parameters - */ + /** + * Given the array of parsed boolean logic, will return + * an array containing the full SQL string representing the filter, + * the needed joins and the values to bind to the query + * + * @throws Exception + * @return array SQL Query, Joins and Bind parameters + */ public function getSql() { - if(count($this->tree) == 0) - { + if (count($this->tree) == 0) { throw new Exception("Invalid segment, please specify a valid segment."); } $bind = array(); $sql = ''; $subExpression = false; - foreach($this->tree as $expression) - { + foreach ($this->tree as $expression) { $operator = $expression[self::INDEX_BOOL_OPERATOR]; $operand = $expression[self::INDEX_OPERAND]; - - if($operator == 'OR' - && !$subExpression) - { + + if ($operator == 'OR' + && !$subExpression + ) { $sql .= ' ('; $subExpression = true; - } - else - { + } else { $sql .= ' '; } - + $sql .= $operand; - - if($operator == 'AND' - && $subExpression) - { + + if ($operator == 'AND' + && $subExpression + ) { $sql .= ')'; $subExpression = false; } - + $sql .= " $operator"; } - if($subExpression) - { + if ($subExpression) { $sql .= ')'; } return array( - 'where' => $sql, - 'bind' => $this->valuesBind, - 'join' => implode(' ', $this->joins) + 'where' => $sql, + 'bind' => $this->valuesBind, + 'join' => implode(' ', $this->joins) ); } } diff --git a/core/Session.php b/core/Session.php index 8f5f5912ab..e102ef605e 100644 --- a/core/Session.php +++ b/core/Session.php @@ -1,133 +1,127 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Session initialization. - * + * * @package Piwik * @subpackage Piwik_Session */ class Piwik_Session extends Zend_Session { - protected static $sessionStarted = false; - - /** - * Are we using file-based session store? - * - * @return bool True if file-based; false otherwise - */ - public static function isFileBasedSessions() - { - $config = Piwik_Config::getInstance(); - return !isset($config->General['session_save_handler']) - || $config->General['session_save_handler'] === 'files'; - } - - /** - * Start the session - * - * @param array|bool $options An array of configuration options; the auto-start (bool) setting is ignored - * @return void - */ - public static function start($options = false) - { - if(Piwik_Common::isPhpCliMode() - || self::$sessionStarted - || (defined('PIWIK_ENABLE_SESSION_START') && !PIWIK_ENABLE_SESSION_START)) - { - return; - } - self::$sessionStarted = true; - - // use cookies to store session id on the client side - @ini_set('session.use_cookies', '1'); - - // prevent attacks involving session ids passed in URLs - @ini_set('session.use_only_cookies', '1'); - - // advise browser that session cookie should only be sent over secure connection - if(Piwik::isHttps()) - { - @ini_set('session.cookie_secure', '1'); - } - - // advise browser that session cookie should only be accessible through the HTTP protocol (i.e., not JavaScript) - @ini_set('session.cookie_httponly', '1'); - - // don't use the default: PHPSESSID - $sessionName = defined('PIWIK_SESSION_NAME') ? PIWIK_SESSION_NAME : 'PIWIK_SESSID'; - @ini_set('session.name', $sessionName); - - // proxies may cause the referer check to fail and - // incorrectly invalidate the session - @ini_set('session.referer_check', ''); - - $currentSaveHandler = ini_get('session.save_handler'); - $config = Piwik_Config::getInstance(); - - if (self::isFileBasedSessions()) - { - // Note: this handler doesn't work well in load-balanced environments and may have a concurrency issue with locked session files - - // for "files", use our own folder to prevent local session file hijacking - $sessionPath = self::getSessionsDirectory(); - // We always call mkdir since it also chmods the directory which might help when permissions were reverted for some reasons - Piwik_Common::mkdir($sessionPath); - - @ini_set('session.save_handler', 'files'); - @ini_set('session.save_path', $sessionPath); - } - else if ($config->General['session_save_handler'] === 'dbtable' - || in_array($currentSaveHandler, array('user', 'mm'))) - { - // We consider these to be misconfigurations, in that: - // - user - we can't verify that user-defined session handler functions have already been set via session_set_save_handler() - // - mm - this handler is not recommended, unsupported, not available for Windows, and has a potential concurrency issue - - $db = Zend_Registry::get('db'); - - $config = array( - 'name' => Piwik_Common::prefixTable('session'), - 'primary' => 'id', - 'modifiedColumn' => 'modified', - 'dataColumn' => 'data', - 'lifetimeColumn' => 'lifetime', - 'db' => $db, - ); - - $saveHandler = new Piwik_Session_SaveHandler_DbTable($config); - if($saveHandler) - { - self::setSaveHandler($saveHandler); - } - } - - // garbage collection may disabled by default (e.g., Debian) - if(ini_get('session.gc_probability') == 0) - { - @ini_set('session.gc_probability', 1); - } - - try { - Zend_Session::start(); - register_shutdown_function(array('Zend_Session', 'writeClose'), true); - } catch(Exception $e) { - Piwik::log('Unable to start session: ' . $e->getMessage()); - - $enableDbSessions = ''; - if(Piwik::isInstalled()) - { - $enableDbSessions = "<br/>If you still experience issues after trying these changes, + protected static $sessionStarted = false; + + /** + * Are we using file-based session store? + * + * @return bool True if file-based; false otherwise + */ + public static function isFileBasedSessions() + { + $config = Piwik_Config::getInstance(); + return !isset($config->General['session_save_handler']) + || $config->General['session_save_handler'] === 'files'; + } + + /** + * Start the session + * + * @param array|bool $options An array of configuration options; the auto-start (bool) setting is ignored + * @return void + */ + public static function start($options = false) + { + if (Piwik_Common::isPhpCliMode() + || self::$sessionStarted + || (defined('PIWIK_ENABLE_SESSION_START') && !PIWIK_ENABLE_SESSION_START) + ) { + return; + } + self::$sessionStarted = true; + + // use cookies to store session id on the client side + @ini_set('session.use_cookies', '1'); + + // prevent attacks involving session ids passed in URLs + @ini_set('session.use_only_cookies', '1'); + + // advise browser that session cookie should only be sent over secure connection + if (Piwik::isHttps()) { + @ini_set('session.cookie_secure', '1'); + } + + // advise browser that session cookie should only be accessible through the HTTP protocol (i.e., not JavaScript) + @ini_set('session.cookie_httponly', '1'); + + // don't use the default: PHPSESSID + $sessionName = defined('PIWIK_SESSION_NAME') ? PIWIK_SESSION_NAME : 'PIWIK_SESSID'; + @ini_set('session.name', $sessionName); + + // proxies may cause the referer check to fail and + // incorrectly invalidate the session + @ini_set('session.referer_check', ''); + + $currentSaveHandler = ini_get('session.save_handler'); + $config = Piwik_Config::getInstance(); + + if (self::isFileBasedSessions()) { + // Note: this handler doesn't work well in load-balanced environments and may have a concurrency issue with locked session files + + // for "files", use our own folder to prevent local session file hijacking + $sessionPath = self::getSessionsDirectory(); + // We always call mkdir since it also chmods the directory which might help when permissions were reverted for some reasons + Piwik_Common::mkdir($sessionPath); + + @ini_set('session.save_handler', 'files'); + @ini_set('session.save_path', $sessionPath); + } else if ($config->General['session_save_handler'] === 'dbtable' + || in_array($currentSaveHandler, array('user', 'mm')) + ) { + // We consider these to be misconfigurations, in that: + // - user - we can't verify that user-defined session handler functions have already been set via session_set_save_handler() + // - mm - this handler is not recommended, unsupported, not available for Windows, and has a potential concurrency issue + + $db = Zend_Registry::get('db'); + + $config = array( + 'name' => Piwik_Common::prefixTable('session'), + 'primary' => 'id', + 'modifiedColumn' => 'modified', + 'dataColumn' => 'data', + 'lifetimeColumn' => 'lifetime', + 'db' => $db, + ); + + $saveHandler = new Piwik_Session_SaveHandler_DbTable($config); + if ($saveHandler) { + self::setSaveHandler($saveHandler); + } + } + + // garbage collection may disabled by default (e.g., Debian) + if (ini_get('session.gc_probability') == 0) { + @ini_set('session.gc_probability', 1); + } + + try { + Zend_Session::start(); + register_shutdown_function(array('Zend_Session', 'writeClose'), true); + } catch (Exception $e) { + Piwik::log('Unable to start session: ' . $e->getMessage()); + + $enableDbSessions = ''; + if (Piwik::isInstalled()) { + $enableDbSessions = "<br/>If you still experience issues after trying these changes, we recommend that you <a href='http://piwik.org/faq/how-to-install/#faq_133' target='_blank'>enable database session storage</a>."; - } + } $message = sprintf("Error: %s %s %s\n<pre>Debug: the original error was \n%s</pre>", Piwik_Translate('General_ExceptionUnableToStartSession'), @@ -136,17 +130,17 @@ class Piwik_Session extends Zend_Session $e->getMessage() ); - Piwik_ExitWithMessage($message); - } - } - - /** - * Returns the directory session files are stored in. - * - * @return string - */ - public static function getSessionsDirectory() - { - return PIWIK_USER_PATH . '/tmp/sessions'; - } + Piwik_ExitWithMessage($message); + } + } + + /** + * Returns the directory session files are stored in. + * + * @return string + */ + public static function getSessionsDirectory() + { + return PIWIK_USER_PATH . '/tmp/sessions'; + } } diff --git a/core/Session/Namespace.php b/core/Session/Namespace.php index 582363de4c..3c18aa06db 100644 --- a/core/Session/Namespace.php +++ b/core/Session/Namespace.php @@ -1,34 +1,33 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Session namespace. - * + * * @package Piwik * @subpackage Piwik_Session */ class Piwik_Session_Namespace extends Zend_Session_Namespace { - /** - * @param string $namespace - * @param bool $singleInstance - */ - public function __construct($namespace = 'Default', $singleInstance = false) - { - if(Piwik_Common::isPhpCliMode()) - { - self::$_readable = true; - return; - } + /** + * @param string $namespace + * @param bool $singleInstance + */ + public function __construct($namespace = 'Default', $singleInstance = false) + { + if (Piwik_Common::isPhpCliMode()) { + self::$_readable = true; + return; + } - parent::__construct($namespace, $singleInstance); - } + parent::__construct($namespace, $singleInstance); + } } diff --git a/core/Session/SaveHandler/DbTable.php b/core/Session/SaveHandler/DbTable.php index 7582d0ef19..df00a920ea 100644 --- a/core/Session/SaveHandler/DbTable.php +++ b/core/Session/SaveHandler/DbTable.php @@ -17,127 +17,127 @@ */ class Piwik_Session_SaveHandler_DbTable implements Zend_Session_SaveHandler_Interface { - protected $config; - protected $maxLifetime; - - /** - * @param array $config - */ - function __construct($config) - { - $this->config = $config; - $this->maxLifetime = ini_get('session.gc_maxlifetime'); - } - - /** - * Destructor - * - * @return void - */ - public function __destruct() - { - Zend_Session::writeClose(); - } - - /** - * Open Session - retrieve resources - * - * @param string $save_path - * @param string $name - * @return boolean - */ - public function open($save_path, $name) - { - $this->config['db']->getConnection(); - - return true; - } - - /** - * Close Session - free resources - * - * @return boolean - */ - public function close() - { - return true; - } - - /** - * Read session data - * - * @param string $id - * @return string - */ - public function read($id) - { - $sql = 'SELECT '.$this->config['dataColumn'].' FROM '.$this->config['name'] - .' WHERE '.$this->config['primary'].' = ?' - .' AND '.$this->config['modifiedColumn'].' + '.$this->config['lifetimeColumn'].' >= ?'; - - $result = $this->config['db']->fetchOne($sql, array($id, time())); - if(!$result) - $result = ''; - - return $result; - } - - /** - * Write Session - commit data to resource - * - * @param string $id - * @param mixed $data - * @return boolean - */ - public function write($id, $data) - { - $sql = 'INSERT INTO '.$this->config['name'] - .' ('.$this->config['primary'].',' - .$this->config['modifiedColumn'].',' - .$this->config['lifetimeColumn'].',' - .$this->config['dataColumn'].')' - .' VALUES (?,?,?,?)' - .' ON DUPLICATE KEY UPDATE ' - .$this->config['modifiedColumn'].' = ?,' - .$this->config['lifetimeColumn'].' = ?,' - .$this->config['dataColumn'].' = ?'; - - $this->config['db']->query($sql, array($id, time(), $this->maxLifetime, $data, time(), $this->maxLifetime, $data)); - - return true; - } - - /** - * Destroy Session - remove data from resource for - * given session id - * - * @param string $id - * @return boolean - */ - public function destroy($id) - { - $sql = 'DELETE FROM '.$this->config['name'] - .' WHERE '.$this->config['primary'].' = ?'; - - $this->config['db']->query($sql, array($id)); - - return true; - } - - /** - * Garbage Collection - remove old session data older - * than $maxlifetime (in seconds) - * - * @param int $maxlifetime timestamp in seconds - * @return true - */ - public function gc($maxlifetime) - { - $sql = 'DELETE FROM '.$this->config['name'] - .' WHERE '.$this->config['modifiedColumn'].' + '.$this->config['lifetimeColumn'].' < ?'; - - $this->config['db']->query($sql, array(time())); - - return true; - } + protected $config; + protected $maxLifetime; + + /** + * @param array $config + */ + function __construct($config) + { + $this->config = $config; + $this->maxLifetime = ini_get('session.gc_maxlifetime'); + } + + /** + * Destructor + * + * @return void + */ + public function __destruct() + { + Zend_Session::writeClose(); + } + + /** + * Open Session - retrieve resources + * + * @param string $save_path + * @param string $name + * @return boolean + */ + public function open($save_path, $name) + { + $this->config['db']->getConnection(); + + return true; + } + + /** + * Close Session - free resources + * + * @return boolean + */ + public function close() + { + return true; + } + + /** + * Read session data + * + * @param string $id + * @return string + */ + public function read($id) + { + $sql = 'SELECT ' . $this->config['dataColumn'] . ' FROM ' . $this->config['name'] + . ' WHERE ' . $this->config['primary'] . ' = ?' + . ' AND ' . $this->config['modifiedColumn'] . ' + ' . $this->config['lifetimeColumn'] . ' >= ?'; + + $result = $this->config['db']->fetchOne($sql, array($id, time())); + if (!$result) + $result = ''; + + return $result; + } + + /** + * Write Session - commit data to resource + * + * @param string $id + * @param mixed $data + * @return boolean + */ + public function write($id, $data) + { + $sql = 'INSERT INTO ' . $this->config['name'] + . ' (' . $this->config['primary'] . ',' + . $this->config['modifiedColumn'] . ',' + . $this->config['lifetimeColumn'] . ',' + . $this->config['dataColumn'] . ')' + . ' VALUES (?,?,?,?)' + . ' ON DUPLICATE KEY UPDATE ' + . $this->config['modifiedColumn'] . ' = ?,' + . $this->config['lifetimeColumn'] . ' = ?,' + . $this->config['dataColumn'] . ' = ?'; + + $this->config['db']->query($sql, array($id, time(), $this->maxLifetime, $data, time(), $this->maxLifetime, $data)); + + return true; + } + + /** + * Destroy Session - remove data from resource for + * given session id + * + * @param string $id + * @return boolean + */ + public function destroy($id) + { + $sql = 'DELETE FROM ' . $this->config['name'] + . ' WHERE ' . $this->config['primary'] . ' = ?'; + + $this->config['db']->query($sql, array($id)); + + return true; + } + + /** + * Garbage Collection - remove old session data older + * than $maxlifetime (in seconds) + * + * @param int $maxlifetime timestamp in seconds + * @return true + */ + public function gc($maxlifetime) + { + $sql = 'DELETE FROM ' . $this->config['name'] + . ' WHERE ' . $this->config['modifiedColumn'] . ' + ' . $this->config['lifetimeColumn'] . ' < ?'; + + $this->config['db']->query($sql, array(time())); + + return true; + } } diff --git a/core/Site.php b/core/Site.php index b5ef654117..ae62ae0519 100644 --- a/core/Site.php +++ b/core/Site.php @@ -1,365 +1,357 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * + * * @package Piwik */ class Piwik_Site { - /** - * @var int|null - */ - protected $id = null; - - /** - * @var array - */ - public static $infoSites = array(); - - /** - * @param int $idsite - */ - function __construct($idsite) - { - $this->id = (int)$idsite; - if(!isset(self::$infoSites[$this->id])) - { - self::$infoSites[$this->id] = Piwik_SitesManager_API::getInstance()->getSiteFromId($this->id); - } - } - - /** - * Sets the cached Site data with an array that associates site IDs with - * individual site data. - * - * @param array $sites The array of sites data. Indexed by site ID. - */ - public static function setSites($sites) - { - self::$infoSites = $sites; - } - - /** - * Sets the cached Site data with a non-associated array of site data. - * - * @param array $sites The array of sites data. - */ - public static function setSitesFromArray($sites) - { - $sitesById = array(); - foreach($sites as $site) - { - $sitesById[$site['idsite']] = $site; - } - self::setSites($sitesById); - } - - /** - * @return string - */ - function __toString() - { - 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(); - } - - /** - * Returns the name of the site - * - * @return string - */ - function getName() - { - return $this->get('name'); - } - - /** - * Returns the main url of the site - * - * @return string - */ - function getMainUrl() - { - return $this->get('main_url'); - } - - /** - * Returns the id of the site - * - * @return int - */ - function getId() - { - return $this->id; - } - - /** - * Returns a site property - * @param string $name property to return - * @return mixed - * @throws Exception - */ - protected function get( $name) - { - if(!isset(self::$infoSites[$this->id][$name])) - { - throw new Exception('The requested website id = '.(int)$this->id.' (or its property '.$name.') couldn\'t be found'); - } - return self::$infoSites[$this->id][$name]; - } - - /** - * Returns the creation date of the site - * - * @return Piwik_Date - */ - function getCreationDate() - { - $date = $this->get('ts_created'); - return Piwik_Date::factory($date); - } - - /** - * Returns the timezone of the size - * - * @return string - */ - function getTimezone() - { - return $this->get('timezone'); - } - - /** - * Returns the currency of the site - * - * @return string - */ - function getCurrency() - { - return $this->get('currency'); - } - - /** - * Returns the excluded ips of the site - * - * @return string - */ - function getExcludedIps() - { - return $this->get('excluded_ips'); - } - - /** - * Returns the excluded query parameters of the site - * - * @return string - */ - function getExcludedQueryParameters() - { - return $this->get('excluded_parameters'); - } - - /** - * Returns whether ecommerce id enabled for the site - * - * @return bool - */ - function isEcommerceEnabled() - { - return $this->get('ecommerce') == 1; - } - - function getSearchKeywordParameters() - { - return $this->get('sitesearch_keyword_parameters'); - } - - function getSearchCategoryParameters() - { - return $this->get('sitesearch_category_parameters'); - } - - /** - * Returns whether Site Search Tracking is enabled for the site - * - * @return bool - */ - function isSiteSearchEnabled() - { - return $this->get('sitesearch') == 1; - } - - /** - * Checks the given string for valid site ids and returns them as an array - * - * @param string $ids comma separated idSite list - * @return array of valid integer - */ - static public function getIdSitesFromIdSitesString( $ids ) - { - if ($ids === 'all') - { - return Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess(); - } - - if(!is_array($ids)) - { - $ids = explode(',', $ids); - } - $validIds = array(); - foreach($ids as $id) - { - $id = trim($id); - if(!empty($id) && is_numeric($id) && $id > 0) - { - $validIds[] = $id; - } - } - $validIds = array_filter($validIds); - $validIds = array_unique($validIds); - - return $validIds; - } - - /** - * Clears the site cache - */ - static public function clearCache() - { - self::$infoSites = array(); - } - - /** - * Utility function. Returns the value of the specified field for the - * site with the specified ID. - * - * @param int|string $idsite The ID of the site whose data is being - * accessed. - * @param string $field The name of the field to get. - * @return mixed - */ - static protected function getFor($idsite, $field) - { - $idsite = (int)$idsite; - - if (!isset(self::$infoSites[$idsite])) - { - self::$infoSites[$idsite] = Piwik_SitesManager_API::getInstance()->getSiteFromId($idsite); - } - - return self::$infoSites[$idsite][$field]; - } - - /** - * Returns the name of the site with the specified ID. - * - * @param int $idsite The site ID. - * @return string - */ - static public function getNameFor($idsite) - { - return self::getFor($idsite, 'name'); - } - - /** - * Returns the timezone of the site with the specified ID. - * - * @param int $idsite The site ID. - * @return string - */ - static public function getTimezoneFor($idsite) - { - return self::getFor($idsite, 'timezone'); - } - - /** - * Returns the creation date of the site with the specified ID. - * - * @param int $idsite The site ID. - * @return string - */ - static public function getCreationDateFor($idsite) - { - return self::getFor($idsite, 'ts_created'); - } - - /** - * Returns the url for the site with the specified ID. - * - * @param int $idsite The site ID. - * @return string - */ - static public function getMainUrlFor($idsite) - { - return self::getFor($idsite, 'main_url'); - } - - /** - * Returns whether the site with the specified ID is ecommerce enabled - * - * @param int $idsite The site ID. - * @return string - */ - static public function isEcommerceEnabledFor($idsite) - { - return self::getFor($idsite, 'ecommerce') == 1; - } - - /** - * Returns whether the site with the specified ID is Site Search enabled - * - * @param int $idsite The site ID. - * @return string - */ - static public function isSiteSearchEnabledFor($idsite) - { - return self::getFor($idsite, 'sitesearch') == 1; - } - - /** - * Returns the currency of the site with the specified ID. - * - * @param int $idsite The site ID. - * @return string - */ - static public function getCurrencyFor($idsite) - { - return self::getFor($idsite, 'currency'); - } - - /** - * Returns the excluded IP addresses of the site with the specified ID. - * - * @param int $idsite The site ID. - * @return string - */ - static public function getExcludedIpsFor($idsite) - { - return self::getFor($idsite, 'excluded_ips'); - } - - /** - * Returns the excluded query parameters for the site with the specified ID. - * - * @param int $idsite The site ID. - * @return string - */ - static public function getExcludedQueryParametersFor($idsite) - { - return self::getFor($idsite, 'excluded_parameters'); - } + /** + * @var int|null + */ + protected $id = null; + + /** + * @var array + */ + public static $infoSites = array(); + + /** + * @param int $idsite + */ + function __construct($idsite) + { + $this->id = (int)$idsite; + if (!isset(self::$infoSites[$this->id])) { + self::$infoSites[$this->id] = Piwik_SitesManager_API::getInstance()->getSiteFromId($this->id); + } + } + + /** + * Sets the cached Site data with an array that associates site IDs with + * individual site data. + * + * @param array $sites The array of sites data. Indexed by site ID. + */ + public static function setSites($sites) + { + self::$infoSites = $sites; + } + + /** + * Sets the cached Site data with a non-associated array of site data. + * + * @param array $sites The array of sites data. + */ + public static function setSitesFromArray($sites) + { + $sitesById = array(); + foreach ($sites as $site) { + $sitesById[$site['idsite']] = $site; + } + self::setSites($sitesById); + } + + /** + * @return string + */ + function __toString() + { + 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(); + } + + /** + * Returns the name of the site + * + * @return string + */ + function getName() + { + return $this->get('name'); + } + + /** + * Returns the main url of the site + * + * @return string + */ + function getMainUrl() + { + return $this->get('main_url'); + } + + /** + * Returns the id of the site + * + * @return int + */ + function getId() + { + return $this->id; + } + + /** + * Returns a site property + * @param string $name property to return + * @return mixed + * @throws Exception + */ + protected function get($name) + { + if (!isset(self::$infoSites[$this->id][$name])) { + throw new Exception('The requested website id = ' . (int)$this->id . ' (or its property ' . $name . ') couldn\'t be found'); + } + return self::$infoSites[$this->id][$name]; + } + + /** + * Returns the creation date of the site + * + * @return Piwik_Date + */ + function getCreationDate() + { + $date = $this->get('ts_created'); + return Piwik_Date::factory($date); + } + + /** + * Returns the timezone of the size + * + * @return string + */ + function getTimezone() + { + return $this->get('timezone'); + } + + /** + * Returns the currency of the site + * + * @return string + */ + function getCurrency() + { + return $this->get('currency'); + } + + /** + * Returns the excluded ips of the site + * + * @return string + */ + function getExcludedIps() + { + return $this->get('excluded_ips'); + } + + /** + * Returns the excluded query parameters of the site + * + * @return string + */ + function getExcludedQueryParameters() + { + return $this->get('excluded_parameters'); + } + + /** + * Returns whether ecommerce id enabled for the site + * + * @return bool + */ + function isEcommerceEnabled() + { + return $this->get('ecommerce') == 1; + } + + function getSearchKeywordParameters() + { + return $this->get('sitesearch_keyword_parameters'); + } + + function getSearchCategoryParameters() + { + return $this->get('sitesearch_category_parameters'); + } + + /** + * Returns whether Site Search Tracking is enabled for the site + * + * @return bool + */ + function isSiteSearchEnabled() + { + return $this->get('sitesearch') == 1; + } + + /** + * Checks the given string for valid site ids and returns them as an array + * + * @param string $ids comma separated idSite list + * @return array of valid integer + */ + static public function getIdSitesFromIdSitesString($ids) + { + if ($ids === 'all') { + return Piwik_SitesManager_API::getInstance()->getSitesIdWithAtLeastViewAccess(); + } + + if (!is_array($ids)) { + $ids = explode(',', $ids); + } + $validIds = array(); + foreach ($ids as $id) { + $id = trim($id); + if (!empty($id) && is_numeric($id) && $id > 0) { + $validIds[] = $id; + } + } + $validIds = array_filter($validIds); + $validIds = array_unique($validIds); + + return $validIds; + } + + /** + * Clears the site cache + */ + static public function clearCache() + { + self::$infoSites = array(); + } + + /** + * Utility function. Returns the value of the specified field for the + * site with the specified ID. + * + * @param int|string $idsite The ID of the site whose data is being + * accessed. + * @param string $field The name of the field to get. + * @return mixed + */ + static protected function getFor($idsite, $field) + { + $idsite = (int)$idsite; + + if (!isset(self::$infoSites[$idsite])) { + self::$infoSites[$idsite] = Piwik_SitesManager_API::getInstance()->getSiteFromId($idsite); + } + + return self::$infoSites[$idsite][$field]; + } + + /** + * Returns the name of the site with the specified ID. + * + * @param int $idsite The site ID. + * @return string + */ + static public function getNameFor($idsite) + { + return self::getFor($idsite, 'name'); + } + + /** + * Returns the timezone of the site with the specified ID. + * + * @param int $idsite The site ID. + * @return string + */ + static public function getTimezoneFor($idsite) + { + return self::getFor($idsite, 'timezone'); + } + + /** + * Returns the creation date of the site with the specified ID. + * + * @param int $idsite The site ID. + * @return string + */ + static public function getCreationDateFor($idsite) + { + return self::getFor($idsite, 'ts_created'); + } + + /** + * Returns the url for the site with the specified ID. + * + * @param int $idsite The site ID. + * @return string + */ + static public function getMainUrlFor($idsite) + { + return self::getFor($idsite, 'main_url'); + } + + /** + * Returns whether the site with the specified ID is ecommerce enabled + * + * @param int $idsite The site ID. + * @return string + */ + static public function isEcommerceEnabledFor($idsite) + { + return self::getFor($idsite, 'ecommerce') == 1; + } + + /** + * Returns whether the site with the specified ID is Site Search enabled + * + * @param int $idsite The site ID. + * @return string + */ + static public function isSiteSearchEnabledFor($idsite) + { + return self::getFor($idsite, 'sitesearch') == 1; + } + + /** + * Returns the currency of the site with the specified ID. + * + * @param int $idsite The site ID. + * @return string + */ + static public function getCurrencyFor($idsite) + { + return self::getFor($idsite, 'currency'); + } + + /** + * Returns the excluded IP addresses of the site with the specified ID. + * + * @param int $idsite The site ID. + * @return string + */ + static public function getExcludedIpsFor($idsite) + { + return self::getFor($idsite, 'excluded_ips'); + } + + /** + * Returns the excluded query parameters for the site with the specified ID. + * + * @param int $idsite The site ID. + * @return string + */ + static public function getExcludedQueryParametersFor($idsite) + { + return self::getFor($idsite, 'excluded_parameters'); + } } diff --git a/core/Smarty.php b/core/Smarty.php index 3adbebd49d..91363d1cbc 100644 --- a/core/Smarty.php +++ b/core/Smarty.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -23,116 +23,117 @@ require_once PIWIK_INCLUDE_PATH . '/libs/Smarty/Smarty.class.php'; * @see Smarty, libs/Smarty/Smarty.class.php * @link http://smarty.net/manual/en/ */ -class Piwik_Smarty extends Smarty +class Piwik_Smarty extends Smarty { - function trigger_error($error_msg, $error_type = E_USER_WARNING) - { - throw new SmartyException($error_msg); - } - - public function __construct($smConf = array(), $filter = true) - { - parent::__construct(); - - $this->init($smConf, $filter); - } - - public function init($smConf, $filter) - { - $this->initSettings($smConf); - if ($filter) { - $this->initFilters(); - } - } - - protected function initSettings($smConf) - { - if (count($smConf) == 0) { - $smConf = Piwik_Config::getInstance()->smarty; - } - foreach ($smConf as $key => $value) { - $this->$key = $value; - } - - $this->template_dir = $smConf['template_dir']; - array_walk($this->template_dir, array("Piwik_Smarty", "addPiwikPath"), PIWIK_INCLUDE_PATH); - - $this->plugins_dir = $smConf['plugins_dir']; - array_walk($this->plugins_dir, array("Piwik_Smarty", "addPiwikPath"), PIWIK_INCLUDE_PATH); - - $this->compile_dir = $smConf['compile_dir']; - Piwik_Smarty::addPiwikPath($this->compile_dir, null, PIWIK_USER_PATH); - - $this->cache_dir = $smConf['cache_dir']; - Piwik_Smarty::addPiwikPath($this->cache_dir, null, PIWIK_USER_PATH); - - $error_reporting = $smConf['error_reporting']; - if ($error_reporting != (string)(int)$error_reporting) { - $error_reporting = self::bitwise_eval($error_reporting); - } - $this->error_reporting = $error_reporting; - - Piwik_PostEvent('Smarty.initSettings', $this); - - } - - public function initFilters() - { - $this->load_filter('output', 'cachebuster'); - - $use_ajax_cdn = Piwik_Config::getInstance()->General['use_ajax_cdn']; - if ($use_ajax_cdn) { - $this->load_filter('output', 'ajaxcdn'); - } - - $this->load_filter('output', 'trimwhitespace'); - } - - /** - * Evaluate expression containing only bitwise operators. - * Replaces defined constants with corresponding values. - * Does not use eval(). - * - * @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) - // note: 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); - } - - /** - * 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) - { - $value = $path ."/$value"; - } - } + function trigger_error($error_msg, $error_type = E_USER_WARNING) + { + throw new SmartyException($error_msg); + } + + public function __construct($smConf = array(), $filter = true) + { + parent::__construct(); + + $this->init($smConf, $filter); + } + + public function init($smConf, $filter) + { + $this->initSettings($smConf); + if ($filter) { + $this->initFilters(); + } + } + + protected function initSettings($smConf) + { + if (count($smConf) == 0) { + $smConf = Piwik_Config::getInstance()->smarty; + } + foreach ($smConf as $key => $value) { + $this->$key = $value; + } + + $this->template_dir = $smConf['template_dir']; + array_walk($this->template_dir, array("Piwik_Smarty", "addPiwikPath"), PIWIK_INCLUDE_PATH); + + $this->plugins_dir = $smConf['plugins_dir']; + array_walk($this->plugins_dir, array("Piwik_Smarty", "addPiwikPath"), PIWIK_INCLUDE_PATH); + + $this->compile_dir = $smConf['compile_dir']; + Piwik_Smarty::addPiwikPath($this->compile_dir, null, PIWIK_USER_PATH); + + $this->cache_dir = $smConf['cache_dir']; + Piwik_Smarty::addPiwikPath($this->cache_dir, null, PIWIK_USER_PATH); + + $error_reporting = $smConf['error_reporting']; + if ($error_reporting != (string)(int)$error_reporting) { + $error_reporting = self::bitwise_eval($error_reporting); + } + $this->error_reporting = $error_reporting; + + Piwik_PostEvent('Smarty.initSettings', $this); + + } + + public function initFilters() + { + $this->load_filter('output', 'cachebuster'); + + $use_ajax_cdn = Piwik_Config::getInstance()->General['use_ajax_cdn']; + if ($use_ajax_cdn) { + $this->load_filter('output', 'ajaxcdn'); + } + + $this->load_filter('output', 'trimwhitespace'); + } + + /** + * Evaluate expression containing only bitwise operators. + * Replaces defined constants with corresponding values. + * Does not use eval(). + * + * @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) + // note: 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); + } + + /** + * 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) { + $value = $path . "/$value"; + } + } } /** * @package Piwik * @subpackage Piwik_Smarty */ -class SmartyException extends Exception {} +class SmartyException extends Exception +{ +} diff --git a/core/SmartyPlugins/block.purify.php b/core/SmartyPlugins/block.purify.php index 8768b5aea8..be168dbcdd 100644 --- a/core/SmartyPlugins/block.purify.php +++ b/core/SmartyPlugins/block.purify.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -31,28 +31,25 @@ */ function smarty_block_purify($params, $content, &$smarty) { - if (is_null($content)) - { - return; - } + if (is_null($content)) { + return; + } - $assign = null; + $assign = null; - foreach ($params as $_key => $_val) - { - switch ($_key) - { - case 'assign': - $$_key = (string)$_val; - break; + foreach ($params as $_key => $_val) { + switch ($_key) { + case 'assign': + $$_key = (string)$_val; + break; - default: - $smarty->trigger_error("purify: unknown attribute '$_key'"); - } - } + default: + $smarty->trigger_error("purify: unknown attribute '$_key'"); + } + } - $purifier = Piwik_HTMLPurifier::getInstance(); - $output = $purifier->purify($content); + $purifier = Piwik_HTMLPurifier::getInstance(); + $output = $purifier->purify($content); - return $assign ? $smarty->assign($assign, $output) : $output; + return $assign ? $smarty->assign($assign, $output) : $output; } diff --git a/core/SmartyPlugins/function.ajaxErrorDiv.php b/core/SmartyPlugins/function.ajaxErrorDiv.php index 79521d18af..886873f518 100644 --- a/core/SmartyPlugins/function.ajaxErrorDiv.php +++ b/core/SmartyPlugins/function.ajaxErrorDiv.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -18,13 +18,10 @@ */ 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>'; + 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 index 776f928c97..bd64a4f000 100644 --- a/core/SmartyPlugins/function.ajaxLoadingDiv.php +++ b/core/SmartyPlugins/function.ajaxLoadingDiv.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -18,17 +18,14 @@ */ function smarty_function_ajaxLoadingDiv($params, &$smarty) { - if(empty($params['id'])) - { - $id = 'ajaxLoading'; - } - else - { - $id = $params['id']; - } - return '<div id="'.$id.'" style="display:none">'. - '<div class="loadingPiwik"><img src="themes/default/images/loading-blue.gif" alt="" /> '. - Piwik_Translate('General_LoadingData') . - ' </div>'. - '</div>'; + if (empty($params['id'])) { + $id = 'ajaxLoading'; + } else { + $id = $params['id']; + } + return '<div id="' . $id . '" style="display:none">' . + '<div class="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 index 5636f5c431..a1d697010e 100644 --- a/core/SmartyPlugins/function.ajaxRequestErrorDiv.php +++ b/core/SmartyPlugins/function.ajaxRequestErrorDiv.php @@ -1,21 +1,21 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ /** - * Outputs the generic Ajax request error div + * 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 + * + * @return string Html of the div */ function smarty_function_ajaxRequestErrorDiv() { - return '<div id="loadingError">'.Piwik_Translate('General_ErrorRequest').'</div>'; + return '<div id="loadingError">' . Piwik_Translate('General_ErrorRequest') . '</div>'; } diff --git a/core/SmartyPlugins/function.hiddenurl.php b/core/SmartyPlugins/function.hiddenurl.php index 54beff93d3..91b1bbd89b 100644 --- a/core/SmartyPlugins/function.hiddenurl.php +++ b/core/SmartyPlugins/function.hiddenurl.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -12,32 +12,31 @@ /** * Smarty {hiddenurl} function plugin. * Writes an input Hidden field for every parameter in the URL. - * Useful when using GET forms because we need to print the current parameters + * Useful when using GET forms because we need to print the current parameters * in hidden input so they are to the next URL after the form is submitted. * - * + * * Examples: * <pre> * {hiddenurl module="API"} with a URL 'index.php?action=test&module=CoreHome' will output * <input type=hidden name=action value=test> * <input type=hidden name=module value=API> * </pre> - * + * * Set a value to null if you want this value not to be passed in the submitted form. - * - * @param array - * @param Smarty - * @return string + * + * @param array + * @param Smarty + * @return string */ function smarty_function_hiddenurl($params, &$smarty) { - $queryStringModified = Piwik_Url::getCurrentQueryStringWithParametersModified( $params ); - $urlValues = Piwik_Common::getArrayFromQueryString($queryStringModified); - - $out = ''; - foreach($urlValues as $name => $value) - { - $out .= '<input type="hidden" name="'.$name.'" value="'.$value.'" />'; - } - return $out; + $queryStringModified = Piwik_Url::getCurrentQueryStringWithParametersModified($params); + $urlValues = Piwik_Common::getArrayFromQueryString($queryStringModified); + + $out = ''; + foreach ($urlValues as $name => $value) { + $out .= '<input type="hidden" name="' . $name . '" value="' . $value . '" />'; + } + return $out; } diff --git a/core/SmartyPlugins/function.includeAssets.php b/core/SmartyPlugins/function.includeAssets.php index 4d0169790e..168e1ad5ef 100644 --- a/core/SmartyPlugins/function.includeAssets.php +++ b/core/SmartyPlugins/function.includeAssets.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -24,23 +24,21 @@ */ function smarty_function_includeAssets($params, &$smarty) { - if(!isset($params['type'])) - { - throw new Exception("The smarty function includeAssets needs a 'type' parameter."); - } - - $assetType = strtolower($params['type']); - switch ( $assetType ) - { - case 'css': - - return Piwik_AssetManager::getCssAssets(); - - case 'js': - - return Piwik_AssetManager::getJsAssets(); - - default: - throw new Exception("The smarty function includeAssets 'type' parameter needs to be either 'css' or 'js'."); - } + if (!isset($params['type'])) { + throw new Exception("The smarty function includeAssets needs a 'type' parameter."); + } + + $assetType = strtolower($params['type']); + switch ($assetType) { + case 'css': + + return Piwik_AssetManager::getCssAssets(); + + case 'js': + + return Piwik_AssetManager::getJsAssets(); + + default: + throw new Exception("The smarty function includeAssets 'type' parameter needs to be either 'css' or 'js'."); + } } diff --git a/core/SmartyPlugins/function.loadJavascriptTranslations.php b/core/SmartyPlugins/function.loadJavascriptTranslations.php index 0152e16cf6..38e851ee1e 100644 --- a/core/SmartyPlugins/function.loadJavascriptTranslations.php +++ b/core/SmartyPlugins/function.loadJavascriptTranslations.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -33,29 +33,24 @@ * @throws Exception * @return string */ -function smarty_function_loadJavascriptTranslations($params, &$smarty) +function smarty_function_loadJavascriptTranslations($params, &$smarty) { - static $pluginTranslationsAlreadyLoaded = array(); - if(!isset($params['plugins'])) - { - throw new Exception("The smarty function loadJavascriptTranslations needs a 'plugins' parameter."); - } - if(in_array($params['plugins'], $pluginTranslationsAlreadyLoaded)) - { - return; - } - $pluginTranslationsAlreadyLoaded[] = $params['plugins']; - $jsTranslations = Piwik_Translate::getInstance()->getJavascriptTranslations(explode(' ',$params['plugins'])); - $jsCode = ''; - if( isset($params['disableOutputScriptTag']) ) - { - $jsCode .= $jsTranslations; - } - else - { - $jsCode .= '<script type="text/javascript">'; - $jsCode .= $jsTranslations; - $jsCode .= '</script>'; - } - return $jsCode; + static $pluginTranslationsAlreadyLoaded = array(); + if (!isset($params['plugins'])) { + throw new Exception("The smarty function loadJavascriptTranslations needs a 'plugins' parameter."); + } + if (in_array($params['plugins'], $pluginTranslationsAlreadyLoaded)) { + return; + } + $pluginTranslationsAlreadyLoaded[] = $params['plugins']; + $jsTranslations = Piwik_Translate::getInstance()->getJavascriptTranslations(explode(' ', $params['plugins'])); + $jsCode = ''; + if (isset($params['disableOutputScriptTag'])) { + $jsCode .= $jsTranslations; + } else { + $jsCode .= '<script type="text/javascript">'; + $jsCode .= $jsTranslations; + $jsCode .= '</script>'; + } + return $jsCode; } diff --git a/core/SmartyPlugins/function.logoHtml.php b/core/SmartyPlugins/function.logoHtml.php index acff49b34c..5de794330c 100644 --- a/core/SmartyPlugins/function.logoHtml.php +++ b/core/SmartyPlugins/function.logoHtml.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -24,22 +24,18 @@ */ function smarty_function_logoHtml($params, &$smarty) { - if(!isset($params['metadata']['logo'])) - { - return; - } - $width = $height = $alt = ''; - if(isset($params['metadata']['logoWidth'])) - { - $width = "width=".$params['metadata']['logoWidth']; - } - if(isset($params['metadata']['logoHeight'])) - { - $height = "height=".$params['metadata']['logoHeight']; - } - if(isset($params['alt'])) - { - $alt = "title='".$params['alt']."' alt='".$params['alt']."'"; - } - return " <img $alt $width $height src='".$params['metadata']['logo']."' />"; + if (!isset($params['metadata']['logo'])) { + return; + } + $width = $height = $alt = ''; + if (isset($params['metadata']['logoWidth'])) { + $width = "width=" . $params['metadata']['logoWidth']; + } + if (isset($params['metadata']['logoHeight'])) { + $height = "height=" . $params['metadata']['logoHeight']; + } + if (isset($params['alt'])) { + $alt = "title='" . $params['alt'] . "' alt='" . $params['alt'] . "'"; + } + return " <img $alt $width $height src='" . $params['metadata']['logo'] . "' />"; } diff --git a/core/SmartyPlugins/function.postEvent.php b/core/SmartyPlugins/function.postEvent.php index ff43fd91fe..079822e530 100644 --- a/core/SmartyPlugins/function.postEvent.php +++ b/core/SmartyPlugins/function.postEvent.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -30,13 +30,12 @@ */ function smarty_function_postEvent($params, &$smarty) { - if(!isset($params['name'])) - { - throw new Exception("The smarty function postEvent needs a 'name' parameter."); - } - $eventName = $params['name']; - - $str = ''; - Piwik_PostEvent($eventName, $str); - return $str; + if (!isset($params['name'])) { + throw new Exception("The smarty function postEvent needs a 'name' parameter."); + } + $eventName = $params['name']; + + $str = ''; + Piwik_PostEvent($eventName, $str); + return $str; } diff --git a/core/SmartyPlugins/function.sparkline.php b/core/SmartyPlugins/function.sparkline.php index af1cab0914..af339efc88 100644 --- a/core/SmartyPlugins/function.sparkline.php +++ b/core/SmartyPlugins/function.sparkline.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -16,9 +16,9 @@ */ function smarty_function_sparkline($params, &$smarty = false) { - $src = $params['src']; - $graph = new Piwik_Visualization_Sparkline(); - $width = $graph->getWidth(); - $height = $graph->getHeight(); - return "<img class=\"sparkline\" alt=\"\" src=\"$src\" width=\"$width\" height=\"$height\" />"; + $src = $params['src']; + $graph = new Piwik_Visualization_Sparkline(); + $width = $graph->getWidth(); + $height = $graph->getHeight(); + return "<img class=\"sparkline\" alt=\"\" src=\"$src\" width=\"$width\" height=\"$height\" />"; } diff --git a/core/SmartyPlugins/function.url.php b/core/SmartyPlugins/function.url.php index 9e20e93d1e..ef8ca0bf6c 100644 --- a/core/SmartyPlugins/function.url.php +++ b/core/SmartyPlugins/function.url.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -18,14 +18,14 @@ * {url module="API"} will rewrite the URL modifying the module GET parameter * {url module="API" method="getKeywords"} will rewrite the URL modifying the parameters module=API method=getKeywords * </pre> - * + * * @see Piwik_Url::getCurrentQueryStringWithParametersModified() * * @param array $params $name=>$value pairs of the parameters to modify in the generated URL * @param Smarty &smarty Smarty object - * @return string Something like index.php?module=X&action=Y + * @return string Something like index.php?module=X&action=Y */ function smarty_function_url($params, &$smarty) { - return Piwik_Common::sanitizeInputValue('index.php' . Piwik_Url::getCurrentQueryStringWithParametersModified( $params )); + return Piwik_Common::sanitizeInputValue('index.php' . Piwik_Url::getCurrentQueryStringWithParametersModified($params)); } diff --git a/core/SmartyPlugins/modifier.inlineHelp.php b/core/SmartyPlugins/modifier.inlineHelp.php index 7cc7531f4a..a0ad93bed4 100644 --- a/core/SmartyPlugins/modifier.inlineHelp.php +++ b/core/SmartyPlugins/modifier.inlineHelp.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -16,11 +16,11 @@ */ function smarty_modifier_inlineHelp($text) { - return - '<div class="ui-widget">'. - '<div class="ui-inline-help ui-state-highlight ui-corner-all">'. - '<span class="ui-icon ui-icon-info" style="float:left;margin-right:.3em;"></span>'. - $text. - '</div>'. - '</div>'; + return + '<div class="ui-widget">' . + '<div class="ui-inline-help ui-state-highlight ui-corner-all">' . + '<span class="ui-icon ui-icon-info" style="float:left;margin-right:.3em;"></span>' . + $text . + '</div>' . + '</div>'; } diff --git a/core/SmartyPlugins/modifier.money.php b/core/SmartyPlugins/modifier.money.php index 7d9eaa7afe..29bd853566 100644 --- a/core/SmartyPlugins/modifier.money.php +++ b/core/SmartyPlugins/modifier.money.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -18,11 +18,10 @@ */ 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); + 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 index aa8bdbbce4..754ce4df06 100644 --- a/core/SmartyPlugins/modifier.stripeol.php +++ b/core/SmartyPlugins/modifier.stripeol.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ diff --git a/core/SmartyPlugins/modifier.sumtime.php b/core/SmartyPlugins/modifier.sumtime.php index cbd50f4618..2573719039 100644 --- a/core/SmartyPlugins/modifier.sumtime.php +++ b/core/SmartyPlugins/modifier.sumtime.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -26,5 +26,5 @@ */ function smarty_modifier_sumtime($numberOfSeconds) { - return Piwik::getPrettyTimeFromSeconds($numberOfSeconds); + return Piwik::getPrettyTimeFromSeconds($numberOfSeconds); } diff --git a/core/SmartyPlugins/modifier.translate.php b/core/SmartyPlugins/modifier.translate.php index 0b25c714dc..6c2fcefca0 100644 --- a/core/SmartyPlugins/modifier.translate.php +++ b/core/SmartyPlugins/modifier.translate.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -27,20 +27,17 @@ */ function smarty_modifier_translate($stringToken) { - if(func_num_args() <= 1) - { - $aValues = array(); - } - else - { - $aValues = func_get_args(); - array_shift($aValues); - } - - try { - $stringTranslated = Piwik_Translate($stringToken, $aValues); - } catch( Exception $e) { - $stringTranslated = $stringToken; - } - return $stringTranslated; + if (func_num_args() <= 1) { + $aValues = array(); + } else { + $aValues = func_get_args(); + array_shift($aValues); + } + + try { + $stringTranslated = Piwik_Translate($stringToken, $aValues); + } catch (Exception $e) { + $stringTranslated = $stringToken; + } + return $stringTranslated; } diff --git a/core/SmartyPlugins/modifier.unescape.php b/core/SmartyPlugins/modifier.unescape.php index 860841422d..971e9eabcc 100644 --- a/core/SmartyPlugins/modifier.unescape.php +++ b/core/SmartyPlugins/modifier.unescape.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -21,7 +21,7 @@ */ function smarty_modifier_unescape($string, $char_set = 'UTF-8') { - return html_entity_decode($string, ENT_QUOTES, $char_set); + return html_entity_decode($string, ENT_QUOTES, $char_set); } /* vim: set expandtab: */ diff --git a/core/SmartyPlugins/modifier.urlRewriteBasicView.php b/core/SmartyPlugins/modifier.urlRewriteBasicView.php index 4de05453e8..023897dcd3 100644 --- a/core/SmartyPlugins/modifier.urlRewriteBasicView.php +++ b/core/SmartyPlugins/modifier.urlRewriteBasicView.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -18,23 +18,20 @@ */ function smarty_modifier_urlRewriteBasicView($parameters) { - // replace module=X by moduleToLoad=X - // replace action=Y by actionToLoad=Y - $parameters['moduleToLoad'] = $parameters['module']; - unset($parameters['module']); + // replace module=X by moduleToLoad=X + // replace action=Y by actionToLoad=Y + $parameters['moduleToLoad'] = $parameters['module']; + unset($parameters['module']); - if(isset( $parameters['action'])) - { - $parameters['actionToLoad'] = $parameters['action']; - unset($parameters['action']); - } - else - { - $parameters['actionToLoad'] = null; - } - $url = Piwik_Url::getCurrentQueryStringWithParametersModified($parameters); + if (isset($parameters['action'])) { + $parameters['actionToLoad'] = $parameters['action']; + unset($parameters['action']); + } else { + $parameters['actionToLoad'] = null; + } + $url = Piwik_Url::getCurrentQueryStringWithParametersModified($parameters); - // add module=CoreHome&action=showInContext - $url = $url . '&module=CoreHome&action=showInContext'; - return Piwik_Common::sanitizeInputValue($url); + // add module=CoreHome&action=showInContext + $url = $url . '&module=CoreHome&action=showInContext'; + return Piwik_Common::sanitizeInputValue($url); } diff --git a/core/SmartyPlugins/modifier.urlRewriteWithParameters.php b/core/SmartyPlugins/modifier.urlRewriteWithParameters.php index 69d8d37484..50ec31cac4 100644 --- a/core/SmartyPlugins/modifier.urlRewriteWithParameters.php +++ b/core/SmartyPlugins/modifier.urlRewriteWithParameters.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -18,7 +18,7 @@ */ function smarty_modifier_urlRewriteWithParameters($parameters) { - $parameters['updated'] = null; - $url = Piwik_Url::getCurrentQueryStringWithParametersModified($parameters); - return Piwik_Common::sanitizeInputValue($url); + $parameters['updated'] = null; + $url = Piwik_Url::getCurrentQueryStringWithParametersModified($parameters); + return Piwik_Common::sanitizeInputValue($url); } diff --git a/core/SmartyPlugins/outputfilter.ajaxcdn.php b/core/SmartyPlugins/outputfilter.ajaxcdn.php index 2aa7006e48..5852541f04 100644 --- a/core/SmartyPlugins/outputfilter.ajaxcdn.php +++ b/core/SmartyPlugins/outputfilter.ajaxcdn.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -27,24 +27,24 @@ */ function smarty_outputfilter_ajaxcdn($source, &$smarty) { - $jquery_version = Piwik_Config::getInstance()->General['jquery_version']; - $jqueryui_version = Piwik_Config::getInstance()->General['jqueryui_version']; + $jquery_version = Piwik_Config::getInstance()->General['jquery_version']; + $jqueryui_version = Piwik_Config::getInstance()->General['jqueryui_version']; - $pattern = array( - '~<link rel="stylesheet" type="text/css" href="libs/jquery/themes/([^"]*)" />~', - '~<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([^"]*)">~', - ); + $pattern = array( + '~<link rel="stylesheet" type="text/css" href="libs/jquery/themes/([^"]*)" />~', + '~<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([^"]*)">~', + ); - // IE7 and IE8 bug: downloads css twice if scheme not specified - $requestMethod = Piwik_Url::getCurrentScheme(); - $replace = array( - '<link rel="stylesheet" type="text/css" href="'.$requestMethod.'://ajax.googleapis.com/ajax/libs/jqueryui/'.$jqueryui_version.'/themes/\\1" />', - '<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/'.$jquery_version.'/jquery.min.js">', - '<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/'.$jqueryui_version.'/jquery-ui.min.js">', - '<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/'.$jqueryui_version.'/i18n/jquery-ui-18n.min.js">', - ); + // IE7 and IE8 bug: downloads css twice if scheme not specified + $requestMethod = Piwik_Url::getCurrentScheme(); + $replace = array( + '<link rel="stylesheet" type="text/css" href="' . $requestMethod . '://ajax.googleapis.com/ajax/libs/jqueryui/' . $jqueryui_version . '/themes/\\1" />', + '<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/' . $jquery_version . '/jquery.min.js">', + '<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/' . $jqueryui_version . '/jquery-ui.min.js">', + '<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jqueryui/' . $jqueryui_version . '/i18n/jquery-ui-18n.min.js">', + ); - return preg_replace($pattern, $replace, $source); + return preg_replace($pattern, $replace, $source); } diff --git a/core/SmartyPlugins/outputfilter.cachebuster.php b/core/SmartyPlugins/outputfilter.cachebuster.php index 4431ac6230..a51c3af937 100644 --- a/core/SmartyPlugins/outputfilter.cachebuster.php +++ b/core/SmartyPlugins/outputfilter.cachebuster.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ @@ -30,21 +30,21 @@ */ function smarty_outputfilter_cachebuster($source, &$smarty) { - $tag = 'cb=' . $smarty->get_template_vars('cacheBuster'); + $tag = 'cb=' . $smarty->get_template_vars('cacheBuster'); - $pattern = array( - '~<script type=[\'"]text/javascript[\'"] src=[\'"]([^\'"]+)[\'"]>~', - '~<script src=[\'"]([^\'"]+)[\'"] type=[\'"]text/javascript[\'"]>~', - '~<link rel=[\'"]stylesheet[\'"] type=[\'"]text/css[\'"] href=[\'"]([^\'"]+)[\'"] ?/?>~', - '~(src|href)=\"index.php\?module=([A-Za-z0-9_]+)&action=([A-Za-z0-9_]+)\?cb=~', - ); + $pattern = array( + '~<script type=[\'"]text/javascript[\'"] src=[\'"]([^\'"]+)[\'"]>~', + '~<script src=[\'"]([^\'"]+)[\'"] type=[\'"]text/javascript[\'"]>~', + '~<link rel=[\'"]stylesheet[\'"] type=[\'"]text/css[\'"] href=[\'"]([^\'"]+)[\'"] ?/?>~', + '~(src|href)=\"index.php\?module=([A-Za-z0-9_]+)&action=([A-Za-z0-9_]+)\?cb=~', + ); - $replace = array( - '<script type="text/javascript" src="$1?'. $tag .'">', - '<script type="text/javascript" src="$1?'. $tag .'">', - '<link rel="stylesheet" type="text/css" href="$1?'. $tag .'" />', - '$1="index.php?module=$2&action=$3&cb=', - ); + $replace = array( + '<script type="text/javascript" src="$1?' . $tag . '">', + '<script type="text/javascript" src="$1?' . $tag . '">', + '<link rel="stylesheet" type="text/css" href="$1?' . $tag . '" />', + '$1="index.php?module=$2&action=$3&cb=', + ); - return preg_replace($pattern, $replace, $source); + return preg_replace($pattern, $replace, $source); } diff --git a/core/TCPDF.php b/core/TCPDF.php index 77b5a792b4..2be9e09ad9 100644 --- a/core/TCPDF.php +++ b/core/TCPDF.php @@ -4,7 +4,7 @@ * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -21,74 +21,70 @@ require_once PIWIK_INCLUDE_PATH . '/libs/tcpdf/tcpdf.php'; */ class Piwik_TCPDF extends TCPDF { - protected $footerContent = null; - protected $currentPageNo = null; + protected $footerContent = null; + protected $currentPageNo = null; + + /** + * Render page footer + * + * @see TCPDF::Footer() + */ + function Footer() + { + //Don't show footer on the frontPage + if ($this->currentPageNo > 1) { + $this->SetY(-15); + $this->SetFont($this->footer_font[0], $this->footer_font[1], $this->footer_font[2]); + $this->Cell(0, 10, $this->footerContent . Piwik_Translate('PDFReports_Pagination', array($this->getAliasNumPage(), $this->getAliasNbPages())), 0, false, 'C', 0, '', 0, false, 'T', 'M'); + } + } - /** - * Render page footer - * - * @see TCPDF::Footer() - */ - function Footer() - { - //Don't show footer on the frontPage - if ($this->currentPageNo > 1) - { - $this->SetY(-15); - $this->SetFont($this->footer_font[0], $this->footer_font[1], $this->footer_font[2]); - $this->Cell(0, 10, $this->footerContent . Piwik_Translate('PDFReports_Pagination', array($this->getAliasNumPage(), $this->getAliasNbPages())), 0, false, 'C', 0, '', 0, false, 'T', 'M'); - } - } + /** + * @see TCPDF::Error() + * @param $msg + * @throws Exception + */ + function Error($msg) + { + $this->_destroy(true); + throw new Exception($msg); + } - /** - * @see TCPDF::Error() - * @param $msg - * @throws Exception - */ - function Error($msg) - { - $this->_destroy(true); - throw new Exception($msg); - } - - /** - * Set current page number - */ - function setCurrentPageNo() - { - if (empty($this->currentPageNo)) - { - $this->currentPageNo = 1; - } - else - { - $this->currentPageNo++; - } - } + /** + * Set current page number + */ + function setCurrentPageNo() + { + if (empty($this->currentPageNo)) { + $this->currentPageNo = 1; + } else { + $this->currentPageNo++; + } + } - /** - * Add page to document - * - * @see TCPDF::AddPage() - * - * @param string $orientation - * @param mixed $format - * @param bool $keepmargins - * @param bool $tocpage - */ - function AddPage($orientation='', $format='', $keepmargins=false, $tocpage=false) - { - parent::AddPage($orientation); - $this->setCurrentPageNo(); - } + /** + * Add page to document + * + * @see TCPDF::AddPage() + * + * @param string $orientation + * @param mixed $format + * @param bool $keepmargins + * @param bool $tocpage + */ + function AddPage($orientation = '', $format = '', $keepmargins = false, $tocpage = false) + { + parent::AddPage($orientation); + $this->setCurrentPageNo(); + } - /** - * Set footer content - * - * @param string $footerContent - */ - function SetFooterContent($footerContent) - { - $this->footerContent = $footerContent; - } + /** + * Set footer content + * + * @param string $footerContent + */ + function SetFooterContent($footerContent) + { + $this->footerContent = $footerContent; + } } diff --git a/core/TablePartitioning.php b/core/TablePartitioning.php index 0f4df6b777..40bcec40d0 100644 --- a/core/TablePartitioning.php +++ b/core/TablePartitioning.php @@ -1,133 +1,130 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * + * * NB: When a new table is partitionned using this class, we have to update the method * Piwik::getTablesInstalled() to add the new table to the list of tablename_* to fetch - * + * * @package Piwik * @subpackage Piwik_TablePartitioning */ abstract class Piwik_TablePartitioning { - protected $tableName = null; - protected $generatedTableName = null; - protected $timestamp = null; - - static public $tablesAlreadyInstalled = null; - - public function __construct( $tableName ) - { - $this->tableName = $tableName; - } - - abstract protected function generateTableName() ; - - public function setTimestamp( $timestamp ) - { - $this->timestamp = $timestamp; - $this->generatedTableName = null; - $this->getTableName(); - } - - public function getTableName() - { - // table name already processed - if(!is_null($this->generatedTableName)) - { - return $this->generatedTableName; - } - - if(is_null($this->timestamp)) - { - throw new Exception("You have to specify a timestamp for a Table Partitioning by date."); - } - - // generate table name - $this->generatedTableName = $this->generateTableName(); - - // we make sure the table already exists - $this->checkTableExists(); - } - - protected function checkTableExists() - { - if(is_null(self::$tablesAlreadyInstalled)) - { - self::$tablesAlreadyInstalled = Piwik::getTablesInstalled($forceReload = false); - } - - if(!in_array($this->generatedTableName, self::$tablesAlreadyInstalled)) - { - $db = Zend_Registry::get('db'); - $sql = Piwik::getTableCreateSql($this->tableName); - - $config = Piwik_Config::getInstance(); - $prefixTables = $config->database['tables_prefix']; - $sql = str_replace( $prefixTables . $this->tableName, $this->generatedTableName, $sql); - try { - $db->query( $sql ); - } catch(Exception $e) { - // mysql error 1050: table already exists - if(! $db->isErrNo($e, '1050')) - { - // failed for some other reason - throw $e; - } - } - - self::$tablesAlreadyInstalled[] = $this->generatedTableName; - } - } - - public function __toString() - { - return $this->getTableName(); - } + protected $tableName = null; + protected $generatedTableName = null; + protected $timestamp = null; + + static public $tablesAlreadyInstalled = null; + + public function __construct($tableName) + { + $this->tableName = $tableName; + } + + abstract protected function generateTableName(); + + public function setTimestamp($timestamp) + { + $this->timestamp = $timestamp; + $this->generatedTableName = null; + $this->getTableName(); + } + + public function getTableName() + { + // table name already processed + if (!is_null($this->generatedTableName)) { + return $this->generatedTableName; + } + + if (is_null($this->timestamp)) { + throw new Exception("You have to specify a timestamp for a Table Partitioning by date."); + } + + // generate table name + $this->generatedTableName = $this->generateTableName(); + + // we make sure the table already exists + $this->checkTableExists(); + } + + protected function checkTableExists() + { + if (is_null(self::$tablesAlreadyInstalled)) { + self::$tablesAlreadyInstalled = Piwik::getTablesInstalled($forceReload = false); + } + + if (!in_array($this->generatedTableName, self::$tablesAlreadyInstalled)) { + $db = Zend_Registry::get('db'); + $sql = Piwik::getTableCreateSql($this->tableName); + + $config = Piwik_Config::getInstance(); + $prefixTables = $config->database['tables_prefix']; + $sql = str_replace($prefixTables . $this->tableName, $this->generatedTableName, $sql); + try { + $db->query($sql); + } catch (Exception $e) { + // mysql error 1050: table already exists + if (!$db->isErrNo($e, '1050')) { + // failed for some other reason + throw $e; + } + } + + self::$tablesAlreadyInstalled[] = $this->generatedTableName; + } + } + + public function __toString() + { + return $this->getTableName(); + } } /** - * + * * @package Piwik * @subpackage Piwik_TablePartitioning */ class Piwik_TablePartitioning_Monthly extends Piwik_TablePartitioning { - public function __construct( $tableName ) - { - parent::__construct($tableName); - } - protected function generateTableName() - { - $config = Piwik_Config::getInstance(); - return $config->database['tables_prefix'] . $this->tableName . "_" . date("Y_m", $this->timestamp); - } - + public function __construct($tableName) + { + parent::__construct($tableName); + } + + protected function generateTableName() + { + $config = Piwik_Config::getInstance(); + return $config->database['tables_prefix'] . $this->tableName . "_" . date("Y_m", $this->timestamp); + } + } /** - * + * * @package Piwik * @subpackage Piwik_TablePartitioning */ class Piwik_TablePartitioning_Daily extends Piwik_TablePartitioning { - public function __construct( $tableName ) - { - parent::__construct($tableName); - } - protected function generateTableName() - { - $config = Piwik_Config::getInstance(); - return $config->database['tables_prefix'] . $this->tableName . "_" . date("Y_m_d", $this->timestamp); - } + public function __construct($tableName) + { + parent::__construct($tableName); + } + + protected function generateTableName() + { + $config = Piwik_Config::getInstance(); + return $config->database['tables_prefix'] . $this->tableName . "_" . date("Y_m_d", $this->timestamp); + } } diff --git a/core/TaskScheduler.php b/core/TaskScheduler.php index a2f8810930..1a29780f72 100644 --- a/core/TaskScheduler.php +++ b/core/TaskScheduler.php @@ -16,7 +16,7 @@ define('DEBUG_FORCE_SCHEDULED_TASKS', false); * Piwik_TaskScheduler is the class used to manage the execution of periodicaly planned task. * * It performs the following actions : - * - Identifies tasks of Piwik + * - Identifies tasks of Piwik * - Runs tasks * * @package Piwik @@ -24,151 +24,142 @@ define('DEBUG_FORCE_SCHEDULED_TASKS', false); class Piwik_TaskScheduler { - const GET_TASKS_EVENT = "TaskScheduler.getScheduledTasks"; - const TIMETABLE_OPTION_STRING = "TaskScheduler.timetable"; - static private $running = false; - - /** - * runTasks collects tasks defined within piwik plugins, runs them if they are scheduled and reschedules - * the tasks that have been executed. - * - * @return array - */ - static public function runTasks() - { - // get the array where rescheduled timetables are stored - $timetable = self::getTimetableFromOptionTable(); - - // collect tasks - $tasks = array(); - Piwik_PostEvent(self::GET_TASKS_EVENT, $tasks); - - // remove from timetable tasks that are not active anymore - $activeTaskNames = array(); - foreach($tasks as $task) - { - $activeTaskNames[] = $task->getName(); - } - foreach(array_keys($timetable) as $taskName) - { - if(!in_array($taskName, $activeTaskNames)) - { - unset($timetable[$taskName]); - } - } - - // for every priority level, starting with the highest and concluding with the lowest - $executionResults = array(); - for ($priority = Piwik_ScheduledTask::HIGHEST_PRIORITY; - $priority <= Piwik_ScheduledTask::LOWEST_PRIORITY; - ++$priority) - { - // loop through each task - foreach ($tasks as $task) - { - // if the task does not have the current priority level, don't execute it yet - if ($task->getPriority() != $priority) - { - continue; - } - - $taskName = $task->getName(); - if (self::taskShouldBeExecuted($taskName, $timetable)) - { - self::$running = true; - $message = self::executeTask($task); - self::$running = false; - - $executionResults[] = array('task' => $taskName, 'output' => $message); - } - - if(self::taskShouldBeRescheduled($taskName, $timetable)) - { - // update the scheduled time - $timetable[$taskName] = $task->getRescheduledTime(); - Piwik_SetOption(self::TIMETABLE_OPTION_STRING, serialize($timetable)); - } - } - } - - return $executionResults; - } - - static public function isTaskBeingExecuted() - { - return self::$running; - } - + const GET_TASKS_EVENT = "TaskScheduler.getScheduledTasks"; + const TIMETABLE_OPTION_STRING = "TaskScheduler.timetable"; + static private $running = false; + + /** + * runTasks collects tasks defined within piwik plugins, runs them if they are scheduled and reschedules + * the tasks that have been executed. + * + * @return array + */ + static public function runTasks() + { + // get the array where rescheduled timetables are stored + $timetable = self::getTimetableFromOptionTable(); + + // collect tasks + $tasks = array(); + Piwik_PostEvent(self::GET_TASKS_EVENT, $tasks); + + // remove from timetable tasks that are not active anymore + $activeTaskNames = array(); + foreach ($tasks as $task) { + $activeTaskNames[] = $task->getName(); + } + foreach (array_keys($timetable) as $taskName) { + if (!in_array($taskName, $activeTaskNames)) { + unset($timetable[$taskName]); + } + } + + // for every priority level, starting with the highest and concluding with the lowest + $executionResults = array(); + for ($priority = Piwik_ScheduledTask::HIGHEST_PRIORITY; + $priority <= Piwik_ScheduledTask::LOWEST_PRIORITY; + ++$priority) { + // loop through each task + foreach ($tasks as $task) { + // if the task does not have the current priority level, don't execute it yet + if ($task->getPriority() != $priority) { + continue; + } + + $taskName = $task->getName(); + if (self::taskShouldBeExecuted($taskName, $timetable)) { + self::$running = true; + $message = self::executeTask($task); + self::$running = false; + + $executionResults[] = array('task' => $taskName, 'output' => $message); + } + + if (self::taskShouldBeRescheduled($taskName, $timetable)) { + // update the scheduled time + $timetable[$taskName] = $task->getRescheduledTime(); + Piwik_SetOption(self::TIMETABLE_OPTION_STRING, serialize($timetable)); + } + } + } + + return $executionResults; + } + + static public function isTaskBeingExecuted() + { + return self::$running; + } + /** * return the next task schedule for a given class and method name - * + * * @param string $className * @param string $methodName * @param string $methodParameter * @return mixed int|bool the next schedule in miliseconds, false if task has never been run */ - static public function getScheduledTimeForMethod($className, $methodName, $methodParameter = null) { - - // get the array where rescheduled timetables are stored - $timetable = self::getTimetableFromOptionTable(); - - $taskName = Piwik_ScheduledTask::getTaskName($className, $methodName, $methodParameter); - - return self::taskHasBeenScheduledOnce($taskName, $timetable) ? $timetable[$taskName] : false; - } - - /* - * Task has to be executed if : - * - the task has already been scheduled once and the current system time is greater than the scheduled time. - * - execution is forced, see $forceTaskExecution - */ - static private function taskShouldBeExecuted($taskName, $timetable) - { - $forceTaskExecution = - (isset($GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) && $GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) - || DEBUG_FORCE_SCHEDULED_TASKS; - - return $forceTaskExecution || (self::taskHasBeenScheduledOnce($taskName, $timetable) && time() >= $timetable[$taskName]); - } - - /* - * Task has to be rescheduled if : - * - the task has to be executed - * - the task has never been scheduled before - */ - static private function taskShouldBeRescheduled($taskName, $timetable) - { - return !self::taskHasBeenScheduledOnce($taskName, $timetable) || self::taskShouldBeExecuted($taskName, $timetable); - } - - static private function taskHasBeenScheduledOnce($taskName, $timetable) - { - return isset($timetable[$taskName]); - } - - static private function getTimetableFromOptionValue($option) { - $unserializedTimetable = @unserialize($option); - return $unserializedTimetable === false ? array() : $unserializedTimetable; + static public function getScheduledTimeForMethod($className, $methodName, $methodParameter = null) + { + + // get the array where rescheduled timetables are stored + $timetable = self::getTimetableFromOptionTable(); + + $taskName = Piwik_ScheduledTask::getTaskName($className, $methodName, $methodParameter); + + return self::taskHasBeenScheduledOnce($taskName, $timetable) ? $timetable[$taskName] : false; + } + + /* + * Task has to be executed if : + * - the task has already been scheduled once and the current system time is greater than the scheduled time. + * - execution is forced, see $forceTaskExecution + */ + static private function taskShouldBeExecuted($taskName, $timetable) + { + $forceTaskExecution = + (isset($GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) && $GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) + || DEBUG_FORCE_SCHEDULED_TASKS; + + return $forceTaskExecution || (self::taskHasBeenScheduledOnce($taskName, $timetable) && time() >= $timetable[$taskName]); + } + + /* + * Task has to be rescheduled if : + * - the task has to be executed + * - the task has never been scheduled before + */ + static private function taskShouldBeRescheduled($taskName, $timetable) + { + return !self::taskHasBeenScheduledOnce($taskName, $timetable) || self::taskShouldBeExecuted($taskName, $timetable); + } + + static private function taskHasBeenScheduledOnce($taskName, $timetable) + { + return isset($timetable[$taskName]); + } + + static private function getTimetableFromOptionValue($option) + { + $unserializedTimetable = @unserialize($option); + return $unserializedTimetable === false ? array() : $unserializedTimetable; } - static private function getTimetableFromOptionTable() - { - return self::getTimetableFromOptionValue(Piwik_GetOption(self::TIMETABLE_OPTION_STRING)); - } - - static private function executeTask($task) - { - try - { - $timer = new Piwik_Timer(); - call_user_func(array($task->getObjectInstance(), $task->getMethodName()), $task->getMethodParameter()); - $message = $timer->__toString(); - } - catch(Exception $e) - { - $message = 'ERROR: ' . $e->getMessage(); - } - - return $message; - } + static private function getTimetableFromOptionTable() + { + return self::getTimetableFromOptionValue(Piwik_GetOption(self::TIMETABLE_OPTION_STRING)); + } + + static private function executeTask($task) + { + try { + $timer = new Piwik_Timer(); + call_user_func(array($task->getObjectInstance(), $task->getMethodName()), $task->getMethodParameter()); + $message = $timer->__toString(); + } catch (Exception $e) { + $message = 'ERROR: ' . $e->getMessage(); + } + + return $message; + } } diff --git a/core/Timer.php b/core/Timer.php index 7e59cc674f..e747c6d3bd 100644 --- a/core/Timer.php +++ b/core/Timer.php @@ -1,66 +1,65 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * + * * @package Piwik */ class Piwik_Timer { - private $timerStart; - private $memoryStart; + private $timerStart; + private $memoryStart; - public function __construct() - { - $this->init(); - } + public function __construct() + { + $this->init(); + } - public function init() - { - $this->timerStart = $this->getMicrotime(); - $this->memoryStart = $this->getMemoryUsage(); - } + public function init() + { + $this->timerStart = $this->getMicrotime(); + $this->memoryStart = $this->getMemoryUsage(); + } - public function getTime($decimals = 3) - { - return number_format($this->getMicrotime() - $this->timerStart, $decimals, '.', ''); - } - - public function getTimeMs($decimals = 3) - { - return number_format(1000*($this->getMicrotime() - $this->timerStart), $decimals, '.', ''); - } + public function getTime($decimals = 3) + { + return number_format($this->getMicrotime() - $this->timerStart, $decimals, '.', ''); + } - public function getMemoryLeak() - { - return "Memory delta: ".Piwik::getPrettySizeFromBytes($this->getMemoryUsage() - $this->memoryStart); - } - - public function __toString() - { - return "Time elapsed: ". $this->getTime() ."s"; - } - - private function getMicrotime() - { - list($micro_seconds, $seconds) = explode(" ", microtime()); - return ((float)$micro_seconds + (float)$seconds); - } + public function getTimeMs($decimals = 3) + { + return number_format(1000 * ($this->getMicrotime() - $this->timerStart), $decimals, '.', ''); + } - private function getMemoryUsage() - { - if(function_exists('memory_get_usage')) - { - return memory_get_usage(); - } - return 0; - } + public function getMemoryLeak() + { + return "Memory delta: " . Piwik::getPrettySizeFromBytes($this->getMemoryUsage() - $this->memoryStart); + } + + public function __toString() + { + return "Time elapsed: " . $this->getTime() . "s"; + } + + private function getMicrotime() + { + list($micro_seconds, $seconds) = explode(" ", microtime()); + return ((float)$micro_seconds + (float)$seconds); + } + + private function getMemoryUsage() + { + if (function_exists('memory_get_usage')) { + return memory_get_usage(); + } + return 0; + } } diff --git a/core/Tracker.php b/core/Tracker.php index 5255492734..935ffbf3ca 100644 --- a/core/Tracker.php +++ b/core/Tracker.php @@ -21,827 +21,760 @@ */ class Piwik_Tracker { - protected $stateValid = self::STATE_NOTHING_TO_NOTICE; - /** - * @var Piwik_Tracker_Db - */ - protected static $db = null; - - const STATE_NOTHING_TO_NOTICE = 1; - const STATE_LOGGING_DISABLE = 10; - const STATE_EMPTY_REQUEST = 11; - const STATE_NOSCRIPT_REQUEST = 13; - - // We use hex ID that are 16 chars in length, ie. 64 bits IDs - const LENGTH_HEX_ID_STRING = 16; - const LENGTH_BINARY_ID = 8; - - // These are also hardcoded in the Javascript - const MAX_CUSTOM_VARIABLES = 5; - const MAX_LENGTH_CUSTOM_VARIABLE = 200; - - protected $authenticated = false; - static protected $forcedDateTime = null; - static protected $forcedIpString = null; - static protected $forcedVisitorId = null; - - static protected $pluginsNotToLoad = array(); - - /** - * The set of visits to track. - * - * @var array - */ - private $requests = array(); - - /** - * The token auth supplied with a bulk visits POST. - * - * @var string - */ - private $tokenAuth = null; - - /** - * Whether we're currently using bulk tracking or not. - * - * @var bool - */ - private $usingBulkTracking = false; - - /** - * The number of requests that have been successfully logged. - * - * @var int - */ - private $countOfLoggedRequests = 0; - - public function clear() - { - self::$forcedIpString = null; - self::$forcedDateTime = null; - self::$forcedVisitorId = null; - $this->stateValid = self::STATE_NOTHING_TO_NOTICE; - $this->authenticated = false; - } - - public static function setForceIp($ipString) - { - self::$forcedIpString = $ipString; - } - public static function setForceDateTime( $dateTime ) - { - self::$forcedDateTime = $dateTime; - } - - public static function setForceVisitorId($visitorId) - { - self::$forcedVisitorId = $visitorId; - } - - public function getCurrentTimestamp() - { - if(!is_null(self::$forcedDateTime)) - { - return strtotime(self::$forcedDateTime); - } - return time(); - } - - /** - * Do not load the specified plugins (used during testing, to disable Provider plugin) - * @param array $plugins - */ - static public function setPluginsNotToLoad($plugins) - { - self::$pluginsNotToLoad = $plugins; - } - - /** - * Get list of plugins to not load - * - * @return array - */ - static public function getPluginsNotToLoad() - { - return self::$pluginsNotToLoad; - } - - /** - * Update Tracker config - * - * @param string $name Setting name - * @param mixed $value Value - */ - static private function updateTrackerConfig($name, $value) - { - $section = Piwik_Config::getInstance()->Tracker; - $section[$name] = $value; - Piwik_Config::getInstance()->Tracker = $section; - } - - protected function initRequests($args) - { - $rawData = file_get_contents("php://input"); - if (!empty($rawData)) - { - $this->usingBulkTracking = strpos($rawData, '"requests"') || strpos($rawData, "'requests'"); - if($this->usingBulkTracking) - { - return $this->initBulkTrackingRequests($rawData); - } - } - - // Not using bulk tracking - $this->requests = $args ? $args : (!empty($_GET) || !empty($_POST) ? array($_GET + $_POST) : array()); - } - - private function initBulkTrackingRequests($rawData) - { - // POST data can be array of string URLs or array of arrays w/ visit info - $jsonData = Piwik_Common::json_decode($rawData, $assoc = true); - - if (isset($jsonData['requests'])) - { - $this->requests = $jsonData['requests']; - } - $this->tokenAuth = Piwik_Common::getRequestVar('token_auth', false, null, $jsonData); - if(empty($this->tokenAuth)) - { - throw new Exception(" token_auth must be specified when using Bulk Tracking Import. See <a href='http://piwik.org/docs/tracking-api/reference/'>Tracking Doc</a>"); - } - if (!empty($this->requests)) - { - $idSiteForAuthentication = 0; - - foreach ($this->requests as &$request) - { - // if a string is sent, we assume its a URL and try to parse it - if (is_string($request)) - { - $params = array(); - - $url = @parse_url($request); - if (!empty($url)) - { - @parse_str($url['query'], $params); - $request = $params; - if(isset($request['idsite']) && !$idSiteForAuthentication) - { - $idSiteForAuthentication = $request['idsite']; - } - } - } - } - - // a Bulk Tracking request that is not authenticated should fail - if(!$this->authenticateSuperUserOrAdmin(array('idsite' => $idSiteForAuthentication))) - { - throw new Exception(" token_auth specified is not valid for site ". intval($idSiteForAuthentication)); - } - } - } - /** - * Main - tracks the visit/action - * - * @param array $args Optional Request Array - */ - public function main($args = null) - { - $displayedGIF = false; - $this->initRequests($args); - if (!empty($this->requests)) - { - // handle all visits - foreach ($this->requests as $request) - { - $this->init($request); - - if(!$displayedGIF && !$this->authenticated) - { - $this->outputTransparentGif(); - $displayedGIF = true; - } - - try - { - if ($this->isVisitValid()) - { - self::connectDatabaseIfNotConnected(); - - $visit = $this->getNewVisitObject(); - $visit->setRequest($request); - $visit->handle(); - unset($visit); - } - else - { - printDebug("The request is invalid: empty request, or maybe tracking is disabled in the config.ini.php via record_statistics=0"); - } - } catch (Piwik_Tracker_Db_Exception $e) { - printDebug("<b>".$e->getMessage()."</b>"); - $this->exitWithException($e, $this->authenticated); - } catch(Piwik_Tracker_Visit_Excluded $e) { - } catch(Exception $e) { - $this->exitWithException($e, $this->authenticated); - } - $this->clear(); - - // increment successfully logged request count. make sure to do this after try-catch, - // since an excluded visit is considered 'successfully logged' - ++$this->countOfLoggedRequests; - } - - if(!$displayedGIF) - { - $this->outputTransparentGif(); - $displayedGIF = true; - } - } - else - { - $this->handleEmptyRequest($_GET + $_POST); - } - - // run scheduled task - try - { - if($this->shouldRunScheduledTasks()) - { - self::runScheduledTasks($now = $this->getCurrentTimestamp()); - } - } - catch (Exception $e) - { - $this->exitWithException($e, $this->authenticated); - } - - $this->end(); - } - - protected function shouldRunScheduledTasks() - { - // don't run scheduled tasks in CLI mode from Tracker, this is the case - // where we bulk load logs & don't want to lose time with tasks - return !Piwik_Common::isPhpCliMode() - && !$this->authenticated - && $this->getState() != self::STATE_LOGGING_DISABLE; - } - - /** - * Tracker requests will automatically trigger the Scheduled tasks. - * This is useful for users who don't setup the cron, - * but still want daily/weekly/monthly PDF reports emailed automatically. - * - * This is similar to calling the API CoreAdminHome.runScheduledTasks (see misc/cron/archive.php) - * - * @param int $now Current timestamp - */ - protected static function runScheduledTasks($now) - { - // Currently, there is no hourly tasks. When there are some, - // this could be too agressive minimum interval (some hours would be skipped in case of low traffic) - $minimumInterval = Piwik_Config::getInstance()->Tracker['scheduled_tasks_min_interval']; - - // If the user disabled browser archiving, he has already setup a cron - // To avoid parallel requests triggering the Scheduled Tasks, - // Get last time tasks started executing - $cache = Piwik_Tracker_Cache::getCacheGeneral(); - if($minimumInterval <= 0 - || empty($cache['isBrowserTriggerArchivingEnabled'])) - { - printDebug("-> Scheduled tasks not running in Tracker: Browser archiving is disabled."); - return; - } - $nextRunTime = $cache['lastTrackerCronRun'] + $minimumInterval; - if( (isset($GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) && $GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) - || $cache['lastTrackerCronRun'] === false - || $nextRunTime < $now ) - { - $cache['lastTrackerCronRun'] = $now; - Piwik_Tracker_Cache::setCacheGeneral( $cache ); - self::initCorePiwikInTrackerMode(); - Piwik_SetOption('lastTrackerCronRun', $cache['lastTrackerCronRun']); - printDebug('-> Scheduled Tasks: Starting...'); - - // save current user privilege and temporarily assume super user privilege - $isSuperUser = Piwik::isUserIsSuperUser(); - - // Scheduled tasks assume Super User is running - Piwik::setUserIsSuperUser(); - - // While each plugins should ensure that necessary languages are loaded, - // we ensure English translations at least are loaded - Piwik_Translate::getInstance()->loadEnglishTranslation(); - - $resultTasks = Piwik_TaskScheduler::runTasks(); - - // restore original user privilege - Piwik::setUserIsSuperUser($isSuperUser); - - printDebug($resultTasks); - printDebug('Finished Scheduled Tasks.'); - } - else - { - printDebug("-> Scheduled tasks not triggered."); - } - printDebug("Next run will be from: ". date('Y-m-d H:i:s', $nextRunTime) .' UTC'); - } - - static public $initTrackerMode = false; - - /* - * Used to initialize core Piwik components on a piwik.php request - * Eg. when cache is missed and we will be calling some APIs to generate cache - */ - static public function initCorePiwikInTrackerMode() - { - if(!empty($GLOBALS['PIWIK_TRACKER_MODE']) - && self::$initTrackerMode === false) - { - self::$initTrackerMode = true; - require_once PIWIK_INCLUDE_PATH . '/core/Loader.php'; - require_once PIWIK_INCLUDE_PATH . '/core/Option.php'; - try { - $access = Zend_Registry::get('access'); - } catch (Exception $e) { - Piwik::createAccessObject(); - } - try { - $config = Piwik_Config::getInstance(); - } catch (Exception $e) { - Piwik::createConfigObject(); - } - try { - $db = Zend_Registry::get('db'); - } catch (Exception $e) { - Piwik::createDatabaseObject(); - } - - $pluginsManager = Piwik_PluginsManager::getInstance(); - $pluginsToLoad = Piwik_Config::getInstance()->Plugins['Plugins']; - $pluginsForcedNotToLoad = Piwik_Tracker::getPluginsNotToLoad(); - $pluginsToLoad = array_diff($pluginsToLoad, $pluginsForcedNotToLoad); - $pluginsManager->loadPlugins( $pluginsToLoad ); - } - } - - /** - * Echos an error message & other information, then exits. - * - * @param Exception $e - * @param bool $authenticated - */ - protected function exitWithException($e, $authenticated) - { - if ($this->usingBulkTracking) - { - // when doing bulk tracking we return JSON so the caller will know how many succeeded - $result = array('succeeded' => $this->countOfLoggedRequests); - - // send error when in debug mode or when authenticated (which happens when doing log importing, - // for example) - if ((isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) || $authenticated) - { - $result['error'] = Piwik_Tracker_GetErrorMessage($e); - } - - echo Piwik_Common::json_encode($result); - - exit; - } - else - { - Piwik_Tracker_ExitWithException($e, $authenticated); - } - } - - /** - * Returns the date in the "Y-m-d H:i:s" PHP format - * - * @param int $timestamp - * @return string - */ - public static function getDatetimeFromTimestamp($timestamp) - { - return date("Y-m-d H:i:s", $timestamp); - } - - /** - * Initialization - */ - protected function init( $request ) - { - $this->handleTrackingApi($request); - $this->loadTrackerPlugins($request); - $this->handleDisabledTracker(); - $this->handleEmptyRequest($request); - - printDebug("Current datetime: ".date("Y-m-d H:i:s", $this->getCurrentTimestamp())); - } - - /** - * Cleanup - */ - protected function end() - { - switch($this->getState()) - { - case self::STATE_LOGGING_DISABLE: - printDebug("Logging disabled, display transparent logo"); - break; - - case self::STATE_EMPTY_REQUEST: - printDebug("Empty request => Piwik page"); - echo "<a href='/'>Piwik</a> is a free open source web <a href='http://piwik.org'>analytics</a> that lets you keep control of your data."; - break; - - case self::STATE_NOSCRIPT_REQUEST: - case self::STATE_NOTHING_TO_NOTICE: - default: - printDebug("Nothing to notice => default behaviour"); - break; - } - printDebug("End of the page."); - - if($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) - { - if(isset(self::$db)) { - self::$db->recordProfiling(); - Piwik::printSqlProfilingReportTracker(self::$db); - } - } - - self::disconnectDatabase(); - } - - /** - * Factory to create database objects - * - * @param array $configDb Database configuration - * @throws Exception - * @return Piwik_Tracker_Db_Mysqli|Piwik_Tracker_Db_Pdo_Mysql - */ - public static function factory($configDb) - { - switch($configDb['adapter']) - { - case 'PDO_MYSQL': - require_once PIWIK_INCLUDE_PATH .'/core/Tracker/Db/Pdo/Mysql.php'; - return new Piwik_Tracker_Db_Pdo_Mysql($configDb); - - case 'MYSQLI': - require_once PIWIK_INCLUDE_PATH .'/core/Tracker/Db/Mysqli.php'; - return new Piwik_Tracker_Db_Mysqli($configDb); - } - - throw new Exception('Unsupported database adapter '.$configDb['adapter']); - } - - public static function connectPiwikTrackerDb() - { - $db = null; - $configDb = Piwik_Config::getInstance()->database; - - if(!isset($configDb['port'])) - { - // before 0.2.4 there is no port specified in config file - $configDb['port'] = '3306'; - } - - Piwik_PostEvent('Tracker.getDatabaseConfig', $configDb); - - $db = self::factory( $configDb ); - $db->connect(); - - return $db; - } - - public static function connectDatabaseIfNotConnected() - { - if( !is_null(self::$db)) - { - return; - } - - try { - $db = null; - Piwik_PostEvent('Tracker.createDatabase', $db); - if(is_null($db)) - { - $db = self::connectPiwikTrackerDb(); - } - self::$db = $db; - } catch(Exception $e) { - throw new Piwik_Tracker_Db_Exception($e->getMessage(), $e->getCode()); - } - } - - /** - * @return Piwik_Tracker_Db - */ - public static function getDatabase() - { - return self::$db; - } - - public static function disconnectDatabase() - { - if(isset(self::$db)) - { - self::$db->disconnect(); - self::$db = null; - } - } - - /** - * Returns the Tracker_Visit object. - * This method can be overwritten to use a different Tracker_Visit object - * - * @throws Exception - * @return Piwik_Tracker_Visit - */ - protected function getNewVisitObject() - { - $visit = null; - Piwik_PostEvent('Tracker.getNewVisitObject', $visit); - - if(is_null($visit)) - { - $visit = new Piwik_Tracker_Visit( self::$forcedIpString, self::$forcedDateTime, $this->authenticated ); - $visit->setForcedVisitorId(self::$forcedVisitorId); - } - elseif(!($visit instanceof Piwik_Tracker_Visit_Interface )) - { - throw new Exception("The Visit object set in the plugin must implement Piwik_Tracker_Visit_Interface"); - } - return $visit; - } - - protected function outputTransparentGif() - { - if( !isset($GLOBALS['PIWIK_TRACKER_DEBUG']) || !$GLOBALS['PIWIK_TRACKER_DEBUG'] ) - { - $trans_gif_64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="; - $this->sendHeader('Content-Type: image/gif'); - - $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; - - if ($requestMethod !== 'GET') - { - $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*'; - $this->sendHeader('Access-Control-Allow-Origin: ' . $origin); - $this->sendHeader('Access-Control-Allow-Credentials: true'); - } - - print(base64_decode($trans_gif_64)); - } - } - - protected function sendHeader($header) - { - Piwik_Common::sendHeader($header); - } - - protected function isVisitValid() - { - return $this->stateValid !== self::STATE_LOGGING_DISABLE - && $this->stateValid !== self::STATE_EMPTY_REQUEST; - } - - protected function getState() - { - return $this->stateValid; - } - - protected function setState( $value ) - { - $this->stateValid = $value; - } - - protected function loadTrackerPlugins( $request ) - { - // Adding &dp=1 will disable the provider plugin, if token_auth is used (used to speed up bulk imports) - if(isset($request['dp']) - && !empty($request['dp']) - && $this->authenticated) - { - Piwik_Tracker::setPluginsNotToLoad(array('Provider')); - } - - try { - $pluginsTracker = Piwik_Config::getInstance()->Plugins_Tracker; - if(is_array($pluginsTracker) - && count($pluginsTracker) != 0) - { - $pluginsTracker['Plugins_Tracker'] = array_diff($pluginsTracker['Plugins_Tracker'], self::getPluginsNotToLoad()); - Piwik_PluginsManager::getInstance()->doNotLoadAlwaysActivatedPlugins(); - Piwik_PluginsManager::getInstance()->loadPlugins( $pluginsTracker['Plugins_Tracker'] ); - - printDebug("Loading plugins: { ". implode(",", $pluginsTracker['Plugins_Tracker']) . " }"); - } - } catch(Exception $e) { - printDebug("ERROR: ".$e->getMessage()); - } - } - - protected function handleEmptyRequest( $request ) - { - $countParameters = count($request); - if($countParameters == 0) - { - $this->setState(self::STATE_EMPTY_REQUEST); - } - if($countParameters == 1 ) - { - $this->setState(self::STATE_NOSCRIPT_REQUEST); - } - } - - protected function handleDisabledTracker() - { - $saveStats = Piwik_Config::getInstance()->Tracker['record_statistics']; - if($saveStats == 0) - { - $this->setState(self::STATE_LOGGING_DISABLE); - } - } - - protected function authenticateSuperUserOrAdmin( $request ) - { - $tokenAuth = $this->getTokenAuth(); - - if( !$tokenAuth ) - { - return false; - } - $superUserLogin = Piwik_Config::getInstance()->superuser['login']; - $superUserPassword = Piwik_Config::getInstance()->superuser['password']; - if( md5($superUserLogin . $superUserPassword ) == $tokenAuth ) - { - $this->authenticated = true; - return true; - } - - // Now checking the list of admin token_auth cached in the Tracker config file - $idSite = Piwik_Common::getRequestVar('idsite', false, 'int', $request); - if(!empty($idSite) - && $idSite > 0) - { - $website = Piwik_Tracker_Cache::getCacheWebsiteAttributes( $idSite ); - $adminTokenAuth = $website['admin_token_auth']; - if(in_array($tokenAuth, $adminTokenAuth)) - { - $this->authenticated = true; - return true; - } - } - printDebug("WARNING! token_auth = $tokenAuth is not valid, Super User / Admin was NOT authenticated"); - - return false; - } - - protected function getTokenAuth() - { - if (!is_null($this->tokenAuth)) - { - return $this->tokenAuth; - } - - return Piwik_Common::getRequestVar('token_auth', false); - } - - /** - * This method allows to set custom IP + server time + visitor ID, when using Tracking API. - * These two attributes can be only set by the Super User (passing token_auth). - */ - protected function handleTrackingApi( $request ) - { - $shouldAuthenticate = Piwik_Config::getInstance()->Tracker['tracking_requests_require_authentication']; - if($shouldAuthenticate) - { - if(!$this->authenticateSuperUserOrAdmin($request)) - { - return; - } - printDebug("token_auth is authenticated!"); - } - else - { - printDebug("token_auth authentication not required"); - } - - // Custom IP to use for this visitor - $customIp = Piwik_Common::getRequestVar('cip', false, 'string', $request); - if(!empty($customIp)) - { - $this->setForceIp($customIp); - } - - // Custom server date time to use - $customDatetime = Piwik_Common::getRequestVar('cdt', false, 'string', $request); - if(!empty($customDatetime)) - { - $this->setForceDateTime($customDatetime); - } - - // Forced Visitor ID to record the visit / action - $customVisitorId = Piwik_Common::getRequestVar('cid', false, 'string', $request); - if(!empty($customVisitorId)) - { - $this->setForceVisitorId($customVisitorId); - } - } - - public static function setTestEnvironment( $args = null, $requestMethod = null ) - { - if (is_null($args)) - { - $args = $_GET + $_POST; - } - if (is_null($requestMethod)) - { - $requestMethod = $_SERVER['REQUEST_METHOD']; - } - - // Do not run scheduled tasks during tests - self::updateTrackerConfig('scheduled_tasks_min_interval', 0); - - // if nothing found in _GET/_POST and we're doing a POST, assume bulk request. in which case, - // we have to bypass authentication - if (empty($args) && $requestMethod == 'POST') - { - self::updateTrackerConfig('tracking_requests_require_authentication', 0); - } - - // Tests can force the use of 3rd party cookie for ID visitor - if(Piwik_Common::getRequestVar('forceUseThirdPartyCookie', false, null, $args) == 1) - { - self::updateTrackerConfig('use_third_party_id_cookie', 1); - } - - // Tests can force the enabling of IP anonymization - $forceIpAnonymization = false; - if (Piwik_Common::getRequestVar('forceIpAnonymization', false, null, $args) == 1) - { - self::updateTrackerConfig('ip_address_mask_length', 2); - - $section = Piwik_Config::getInstance()->Plugins_Tracker; - $section['Plugins_Tracker'][] = "AnonymizeIP"; - Piwik_Config::getInstance()->Plugins_Tracker = $section; - - $forceIpAnonymization = true; - } - - // Custom IP to use for this visitor - $customIp = Piwik_Common::getRequestVar('cip', false, null, $args); - if(!empty($customIp)) - { - self::setForceIp($customIp); - } - - // Custom server date time to use - $customDatetime = Piwik_Common::getRequestVar('cdt', false, null, $args); - if(!empty($customDatetime)) - { - self::setForceDateTime($customDatetime); - } - - // Custom visitor id - $customVisitorId = Piwik_Common::getRequestVar('cid', false, null, $args); - if(!empty($customVisitorId)) - { - self::setForceVisitorId($customVisitorId); - } - $pluginsDisabled = array('Provider'); - if(!$forceIpAnonymization) - { - $pluginsDisabled[] = 'AnonymizeIP'; - } - - // Disable provider plugin, because it is so slow to do reverse ip lookup in dev environment somehow - self::setPluginsNotToLoad($pluginsDisabled); - } + protected $stateValid = self::STATE_NOTHING_TO_NOTICE; + /** + * @var Piwik_Tracker_Db + */ + protected static $db = null; + + const STATE_NOTHING_TO_NOTICE = 1; + const STATE_LOGGING_DISABLE = 10; + const STATE_EMPTY_REQUEST = 11; + const STATE_NOSCRIPT_REQUEST = 13; + + // We use hex ID that are 16 chars in length, ie. 64 bits IDs + const LENGTH_HEX_ID_STRING = 16; + const LENGTH_BINARY_ID = 8; + + // These are also hardcoded in the Javascript + const MAX_CUSTOM_VARIABLES = 5; + const MAX_LENGTH_CUSTOM_VARIABLE = 200; + + protected $authenticated = false; + static protected $forcedDateTime = null; + static protected $forcedIpString = null; + static protected $forcedVisitorId = null; + + static protected $pluginsNotToLoad = array(); + + /** + * The set of visits to track. + * + * @var array + */ + private $requests = array(); + + /** + * The token auth supplied with a bulk visits POST. + * + * @var string + */ + private $tokenAuth = null; + + /** + * Whether we're currently using bulk tracking or not. + * + * @var bool + */ + private $usingBulkTracking = false; + + /** + * The number of requests that have been successfully logged. + * + * @var int + */ + private $countOfLoggedRequests = 0; + + public function clear() + { + self::$forcedIpString = null; + self::$forcedDateTime = null; + self::$forcedVisitorId = null; + $this->stateValid = self::STATE_NOTHING_TO_NOTICE; + $this->authenticated = false; + } + + public static function setForceIp($ipString) + { + self::$forcedIpString = $ipString; + } + + public static function setForceDateTime($dateTime) + { + self::$forcedDateTime = $dateTime; + } + + public static function setForceVisitorId($visitorId) + { + self::$forcedVisitorId = $visitorId; + } + + public function getCurrentTimestamp() + { + if (!is_null(self::$forcedDateTime)) { + return strtotime(self::$forcedDateTime); + } + return time(); + } + + /** + * Do not load the specified plugins (used during testing, to disable Provider plugin) + * @param array $plugins + */ + static public function setPluginsNotToLoad($plugins) + { + self::$pluginsNotToLoad = $plugins; + } + + /** + * Get list of plugins to not load + * + * @return array + */ + static public function getPluginsNotToLoad() + { + return self::$pluginsNotToLoad; + } + + /** + * Update Tracker config + * + * @param string $name Setting name + * @param mixed $value Value + */ + static private function updateTrackerConfig($name, $value) + { + $section = Piwik_Config::getInstance()->Tracker; + $section[$name] = $value; + Piwik_Config::getInstance()->Tracker = $section; + } + + protected function initRequests($args) + { + $rawData = file_get_contents("php://input"); + if (!empty($rawData)) { + $this->usingBulkTracking = strpos($rawData, '"requests"') || strpos($rawData, "'requests'"); + if ($this->usingBulkTracking) { + return $this->initBulkTrackingRequests($rawData); + } + } + + // Not using bulk tracking + $this->requests = $args ? $args : (!empty($_GET) || !empty($_POST) ? array($_GET + $_POST) : array()); + } + + private function initBulkTrackingRequests($rawData) + { + // POST data can be array of string URLs or array of arrays w/ visit info + $jsonData = Piwik_Common::json_decode($rawData, $assoc = true); + + if (isset($jsonData['requests'])) { + $this->requests = $jsonData['requests']; + } + $this->tokenAuth = Piwik_Common::getRequestVar('token_auth', false, null, $jsonData); + if (empty($this->tokenAuth)) { + throw new Exception(" token_auth must be specified when using Bulk Tracking Import. See <a href='http://piwik.org/docs/tracking-api/reference/'>Tracking Doc</a>"); + } + if (!empty($this->requests)) { + $idSiteForAuthentication = 0; + + foreach ($this->requests as &$request) { + // if a string is sent, we assume its a URL and try to parse it + if (is_string($request)) { + $params = array(); + + $url = @parse_url($request); + if (!empty($url)) { + @parse_str($url['query'], $params); + $request = $params; + if (isset($request['idsite']) && !$idSiteForAuthentication) { + $idSiteForAuthentication = $request['idsite']; + } + } + } + } + + // a Bulk Tracking request that is not authenticated should fail + if (!$this->authenticateSuperUserOrAdmin(array('idsite' => $idSiteForAuthentication))) { + throw new Exception(" token_auth specified is not valid for site " . intval($idSiteForAuthentication)); + } + } + } + + /** + * Main - tracks the visit/action + * + * @param array $args Optional Request Array + */ + public function main($args = null) + { + $displayedGIF = false; + $this->initRequests($args); + if (!empty($this->requests)) { + // handle all visits + foreach ($this->requests as $request) { + $this->init($request); + + if (!$displayedGIF && !$this->authenticated) { + $this->outputTransparentGif(); + $displayedGIF = true; + } + + try { + if ($this->isVisitValid()) { + self::connectDatabaseIfNotConnected(); + + $visit = $this->getNewVisitObject(); + $visit->setRequest($request); + $visit->handle(); + unset($visit); + } else { + printDebug("The request is invalid: empty request, or maybe tracking is disabled in the config.ini.php via record_statistics=0"); + } + } catch (Piwik_Tracker_Db_Exception $e) { + printDebug("<b>" . $e->getMessage() . "</b>"); + $this->exitWithException($e, $this->authenticated); + } catch (Piwik_Tracker_Visit_Excluded $e) { + } catch (Exception $e) { + $this->exitWithException($e, $this->authenticated); + } + $this->clear(); + + // increment successfully logged request count. make sure to do this after try-catch, + // since an excluded visit is considered 'successfully logged' + ++$this->countOfLoggedRequests; + } + + if (!$displayedGIF) { + $this->outputTransparentGif(); + $displayedGIF = true; + } + } else { + $this->handleEmptyRequest($_GET + $_POST); + } + + // run scheduled task + try { + if ($this->shouldRunScheduledTasks()) { + self::runScheduledTasks($now = $this->getCurrentTimestamp()); + } + } catch (Exception $e) { + $this->exitWithException($e, $this->authenticated); + } + + $this->end(); + } + + protected function shouldRunScheduledTasks() + { + // don't run scheduled tasks in CLI mode from Tracker, this is the case + // where we bulk load logs & don't want to lose time with tasks + return !Piwik_Common::isPhpCliMode() + && !$this->authenticated + && $this->getState() != self::STATE_LOGGING_DISABLE; + } + + /** + * Tracker requests will automatically trigger the Scheduled tasks. + * This is useful for users who don't setup the cron, + * but still want daily/weekly/monthly PDF reports emailed automatically. + * + * This is similar to calling the API CoreAdminHome.runScheduledTasks (see misc/cron/archive.php) + * + * @param int $now Current timestamp + */ + protected static function runScheduledTasks($now) + { + // Currently, there is no hourly tasks. When there are some, + // this could be too agressive minimum interval (some hours would be skipped in case of low traffic) + $minimumInterval = Piwik_Config::getInstance()->Tracker['scheduled_tasks_min_interval']; + + // If the user disabled browser archiving, he has already setup a cron + // To avoid parallel requests triggering the Scheduled Tasks, + // Get last time tasks started executing + $cache = Piwik_Tracker_Cache::getCacheGeneral(); + if ($minimumInterval <= 0 + || empty($cache['isBrowserTriggerArchivingEnabled']) + ) { + printDebug("-> Scheduled tasks not running in Tracker: Browser archiving is disabled."); + return; + } + $nextRunTime = $cache['lastTrackerCronRun'] + $minimumInterval; + if ((isset($GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) && $GLOBALS['PIWIK_TRACKER_DEBUG_FORCE_SCHEDULED_TASKS']) + || $cache['lastTrackerCronRun'] === false + || $nextRunTime < $now + ) { + $cache['lastTrackerCronRun'] = $now; + Piwik_Tracker_Cache::setCacheGeneral($cache); + self::initCorePiwikInTrackerMode(); + Piwik_SetOption('lastTrackerCronRun', $cache['lastTrackerCronRun']); + printDebug('-> Scheduled Tasks: Starting...'); + + // save current user privilege and temporarily assume super user privilege + $isSuperUser = Piwik::isUserIsSuperUser(); + + // Scheduled tasks assume Super User is running + Piwik::setUserIsSuperUser(); + + // While each plugins should ensure that necessary languages are loaded, + // we ensure English translations at least are loaded + Piwik_Translate::getInstance()->loadEnglishTranslation(); + + $resultTasks = Piwik_TaskScheduler::runTasks(); + + // restore original user privilege + Piwik::setUserIsSuperUser($isSuperUser); + + printDebug($resultTasks); + printDebug('Finished Scheduled Tasks.'); + } else { + printDebug("-> Scheduled tasks not triggered."); + } + printDebug("Next run will be from: " . date('Y-m-d H:i:s', $nextRunTime) . ' UTC'); + } + + static public $initTrackerMode = false; + + /* + * Used to initialize core Piwik components on a piwik.php request + * Eg. when cache is missed and we will be calling some APIs to generate cache + */ + static public function initCorePiwikInTrackerMode() + { + if (!empty($GLOBALS['PIWIK_TRACKER_MODE']) + && self::$initTrackerMode === false + ) { + self::$initTrackerMode = true; + require_once PIWIK_INCLUDE_PATH . '/core/Loader.php'; + require_once PIWIK_INCLUDE_PATH . '/core/Option.php'; + try { + $access = Zend_Registry::get('access'); + } catch (Exception $e) { + Piwik::createAccessObject(); + } + try { + $config = Piwik_Config::getInstance(); + } catch (Exception $e) { + Piwik::createConfigObject(); + } + try { + $db = Zend_Registry::get('db'); + } catch (Exception $e) { + Piwik::createDatabaseObject(); + } + + $pluginsManager = Piwik_PluginsManager::getInstance(); + $pluginsToLoad = Piwik_Config::getInstance()->Plugins['Plugins']; + $pluginsForcedNotToLoad = Piwik_Tracker::getPluginsNotToLoad(); + $pluginsToLoad = array_diff($pluginsToLoad, $pluginsForcedNotToLoad); + $pluginsManager->loadPlugins($pluginsToLoad); + } + } + + /** + * Echos an error message & other information, then exits. + * + * @param Exception $e + * @param bool $authenticated + */ + protected function exitWithException($e, $authenticated) + { + if ($this->usingBulkTracking) { + // when doing bulk tracking we return JSON so the caller will know how many succeeded + $result = array('succeeded' => $this->countOfLoggedRequests); + + // send error when in debug mode or when authenticated (which happens when doing log importing, + // for example) + if ((isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) || $authenticated) { + $result['error'] = Piwik_Tracker_GetErrorMessage($e); + } + + echo Piwik_Common::json_encode($result); + + exit; + } else { + Piwik_Tracker_ExitWithException($e, $authenticated); + } + } + + /** + * Returns the date in the "Y-m-d H:i:s" PHP format + * + * @param int $timestamp + * @return string + */ + public static function getDatetimeFromTimestamp($timestamp) + { + return date("Y-m-d H:i:s", $timestamp); + } + + /** + * Initialization + */ + protected function init($request) + { + $this->handleTrackingApi($request); + $this->loadTrackerPlugins($request); + $this->handleDisabledTracker(); + $this->handleEmptyRequest($request); + + printDebug("Current datetime: " . date("Y-m-d H:i:s", $this->getCurrentTimestamp())); + } + + /** + * Cleanup + */ + protected function end() + { + switch ($this->getState()) { + case self::STATE_LOGGING_DISABLE: + printDebug("Logging disabled, display transparent logo"); + break; + + case self::STATE_EMPTY_REQUEST: + printDebug("Empty request => Piwik page"); + echo "<a href='/'>Piwik</a> is a free open source web <a href='http://piwik.org'>analytics</a> that lets you keep control of your data."; + break; + + case self::STATE_NOSCRIPT_REQUEST: + case self::STATE_NOTHING_TO_NOTICE: + default: + printDebug("Nothing to notice => default behaviour"); + break; + } + printDebug("End of the page."); + + if ($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) { + if (isset(self::$db)) { + self::$db->recordProfiling(); + Piwik::printSqlProfilingReportTracker(self::$db); + } + } + + self::disconnectDatabase(); + } + + /** + * Factory to create database objects + * + * @param array $configDb Database configuration + * @throws Exception + * @return Piwik_Tracker_Db_Mysqli|Piwik_Tracker_Db_Pdo_Mysql + */ + public static function factory($configDb) + { + switch ($configDb['adapter']) { + case 'PDO_MYSQL': + require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Pdo/Mysql.php'; + return new Piwik_Tracker_Db_Pdo_Mysql($configDb); + + case 'MYSQLI': + require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Mysqli.php'; + return new Piwik_Tracker_Db_Mysqli($configDb); + } + + throw new Exception('Unsupported database adapter ' . $configDb['adapter']); + } + + public static function connectPiwikTrackerDb() + { + $db = null; + $configDb = Piwik_Config::getInstance()->database; + + if (!isset($configDb['port'])) { + // before 0.2.4 there is no port specified in config file + $configDb['port'] = '3306'; + } + + Piwik_PostEvent('Tracker.getDatabaseConfig', $configDb); + + $db = self::factory($configDb); + $db->connect(); + + return $db; + } + + public static function connectDatabaseIfNotConnected() + { + if (!is_null(self::$db)) { + return; + } + + try { + $db = null; + Piwik_PostEvent('Tracker.createDatabase', $db); + if (is_null($db)) { + $db = self::connectPiwikTrackerDb(); + } + self::$db = $db; + } catch (Exception $e) { + throw new Piwik_Tracker_Db_Exception($e->getMessage(), $e->getCode()); + } + } + + /** + * @return Piwik_Tracker_Db + */ + public static function getDatabase() + { + return self::$db; + } + + public static function disconnectDatabase() + { + if (isset(self::$db)) { + self::$db->disconnect(); + self::$db = null; + } + } + + /** + * Returns the Tracker_Visit object. + * This method can be overwritten to use a different Tracker_Visit object + * + * @throws Exception + * @return Piwik_Tracker_Visit + */ + protected function getNewVisitObject() + { + $visit = null; + Piwik_PostEvent('Tracker.getNewVisitObject', $visit); + + if (is_null($visit)) { + $visit = new Piwik_Tracker_Visit(self::$forcedIpString, self::$forcedDateTime, $this->authenticated); + $visit->setForcedVisitorId(self::$forcedVisitorId); + } elseif (!($visit instanceof Piwik_Tracker_Visit_Interface)) { + throw new Exception("The Visit object set in the plugin must implement Piwik_Tracker_Visit_Interface"); + } + return $visit; + } + + protected function outputTransparentGif() + { + if (!isset($GLOBALS['PIWIK_TRACKER_DEBUG']) || !$GLOBALS['PIWIK_TRACKER_DEBUG']) { + $trans_gif_64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="; + $this->sendHeader('Content-Type: image/gif'); + + $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + + if ($requestMethod !== 'GET') { + $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*'; + $this->sendHeader('Access-Control-Allow-Origin: ' . $origin); + $this->sendHeader('Access-Control-Allow-Credentials: true'); + } + + print(base64_decode($trans_gif_64)); + } + } + + protected function sendHeader($header) + { + Piwik_Common::sendHeader($header); + } + + protected function isVisitValid() + { + return $this->stateValid !== self::STATE_LOGGING_DISABLE + && $this->stateValid !== self::STATE_EMPTY_REQUEST; + } + + protected function getState() + { + return $this->stateValid; + } + + protected function setState($value) + { + $this->stateValid = $value; + } + + protected function loadTrackerPlugins($request) + { + // Adding &dp=1 will disable the provider plugin, if token_auth is used (used to speed up bulk imports) + if (isset($request['dp']) + && !empty($request['dp']) + && $this->authenticated + ) { + Piwik_Tracker::setPluginsNotToLoad(array('Provider')); + } + + try { + $pluginsTracker = Piwik_Config::getInstance()->Plugins_Tracker; + if (is_array($pluginsTracker) + && count($pluginsTracker) != 0 + ) { + $pluginsTracker['Plugins_Tracker'] = array_diff($pluginsTracker['Plugins_Tracker'], self::getPluginsNotToLoad()); + Piwik_PluginsManager::getInstance()->doNotLoadAlwaysActivatedPlugins(); + Piwik_PluginsManager::getInstance()->loadPlugins($pluginsTracker['Plugins_Tracker']); + + printDebug("Loading plugins: { " . implode(",", $pluginsTracker['Plugins_Tracker']) . " }"); + } + } catch (Exception $e) { + printDebug("ERROR: " . $e->getMessage()); + } + } + + protected function handleEmptyRequest($request) + { + $countParameters = count($request); + if ($countParameters == 0) { + $this->setState(self::STATE_EMPTY_REQUEST); + } + if ($countParameters == 1) { + $this->setState(self::STATE_NOSCRIPT_REQUEST); + } + } + + protected function handleDisabledTracker() + { + $saveStats = Piwik_Config::getInstance()->Tracker['record_statistics']; + if ($saveStats == 0) { + $this->setState(self::STATE_LOGGING_DISABLE); + } + } + + protected function authenticateSuperUserOrAdmin($request) + { + $tokenAuth = $this->getTokenAuth(); + + if (!$tokenAuth) { + return false; + } + $superUserLogin = Piwik_Config::getInstance()->superuser['login']; + $superUserPassword = Piwik_Config::getInstance()->superuser['password']; + if (md5($superUserLogin . $superUserPassword) == $tokenAuth) { + $this->authenticated = true; + return true; + } + + // Now checking the list of admin token_auth cached in the Tracker config file + $idSite = Piwik_Common::getRequestVar('idsite', false, 'int', $request); + if (!empty($idSite) + && $idSite > 0 + ) { + $website = Piwik_Tracker_Cache::getCacheWebsiteAttributes($idSite); + $adminTokenAuth = $website['admin_token_auth']; + if (in_array($tokenAuth, $adminTokenAuth)) { + $this->authenticated = true; + return true; + } + } + printDebug("WARNING! token_auth = $tokenAuth is not valid, Super User / Admin was NOT authenticated"); + + return false; + } + + protected function getTokenAuth() + { + if (!is_null($this->tokenAuth)) { + return $this->tokenAuth; + } + + return Piwik_Common::getRequestVar('token_auth', false); + } + + /** + * This method allows to set custom IP + server time + visitor ID, when using Tracking API. + * These two attributes can be only set by the Super User (passing token_auth). + */ + protected function handleTrackingApi($request) + { + $shouldAuthenticate = Piwik_Config::getInstance()->Tracker['tracking_requests_require_authentication']; + if ($shouldAuthenticate) { + if (!$this->authenticateSuperUserOrAdmin($request)) { + return; + } + printDebug("token_auth is authenticated!"); + } else { + printDebug("token_auth authentication not required"); + } + + // Custom IP to use for this visitor + $customIp = Piwik_Common::getRequestVar('cip', false, 'string', $request); + if (!empty($customIp)) { + $this->setForceIp($customIp); + } + + // Custom server date time to use + $customDatetime = Piwik_Common::getRequestVar('cdt', false, 'string', $request); + if (!empty($customDatetime)) { + $this->setForceDateTime($customDatetime); + } + + // Forced Visitor ID to record the visit / action + $customVisitorId = Piwik_Common::getRequestVar('cid', false, 'string', $request); + if (!empty($customVisitorId)) { + $this->setForceVisitorId($customVisitorId); + } + } + + public static function setTestEnvironment($args = null, $requestMethod = null) + { + if (is_null($args)) { + $args = $_GET + $_POST; + } + if (is_null($requestMethod)) { + $requestMethod = $_SERVER['REQUEST_METHOD']; + } + + // Do not run scheduled tasks during tests + self::updateTrackerConfig('scheduled_tasks_min_interval', 0); + + // if nothing found in _GET/_POST and we're doing a POST, assume bulk request. in which case, + // we have to bypass authentication + if (empty($args) && $requestMethod == 'POST') { + self::updateTrackerConfig('tracking_requests_require_authentication', 0); + } + + // Tests can force the use of 3rd party cookie for ID visitor + if (Piwik_Common::getRequestVar('forceUseThirdPartyCookie', false, null, $args) == 1) { + self::updateTrackerConfig('use_third_party_id_cookie', 1); + } + + // Tests can force the enabling of IP anonymization + $forceIpAnonymization = false; + if (Piwik_Common::getRequestVar('forceIpAnonymization', false, null, $args) == 1) { + self::updateTrackerConfig('ip_address_mask_length', 2); + + $section = Piwik_Config::getInstance()->Plugins_Tracker; + $section['Plugins_Tracker'][] = "AnonymizeIP"; + Piwik_Config::getInstance()->Plugins_Tracker = $section; + + $forceIpAnonymization = true; + } + + // Custom IP to use for this visitor + $customIp = Piwik_Common::getRequestVar('cip', false, null, $args); + if (!empty($customIp)) { + self::setForceIp($customIp); + } + + // Custom server date time to use + $customDatetime = Piwik_Common::getRequestVar('cdt', false, null, $args); + if (!empty($customDatetime)) { + self::setForceDateTime($customDatetime); + } + + // Custom visitor id + $customVisitorId = Piwik_Common::getRequestVar('cid', false, null, $args); + if (!empty($customVisitorId)) { + self::setForceVisitorId($customVisitorId); + } + $pluginsDisabled = array('Provider'); + if (!$forceIpAnonymization) { + $pluginsDisabled[] = 'AnonymizeIP'; + } + + // Disable provider plugin, because it is so slow to do reverse ip lookup in dev environment somehow + self::setPluginsNotToLoad($pluginsDisabled); + } } /** * Gets the error message to output when a tracking request fails. - * + * * @param Exception $e * @return string */ -function Piwik_Tracker_GetErrorMessage( $e ) +function Piwik_Tracker_GetErrorMessage($e) { - // Note: duplicated from FormDatabaseSetup.isAccessDenied - // Avoid leaking the username/db name when access denied - if($e->getCode() == 1044 || $e->getCode() == 42000) - { - return "Error while connecting to the Piwik database - please check your credentials in config/config.ini.php file"; - } - else - { - return $e->getMessage(); - } + // Note: duplicated from FormDatabaseSetup.isAccessDenied + // Avoid leaking the username/db name when access denied + if ($e->getCode() == 1044 || $e->getCode() == 42000) { + return "Error while connecting to the Piwik database - please check your credentials in config/config.ini.php file"; + } else { + return $e->getMessage(); + } } /** @@ -851,22 +784,19 @@ function Piwik_Tracker_GetErrorMessage( $e ) */ function Piwik_Tracker_ExitWithException($e, $authenticated = false) { - Piwik_Common::sendHeader('Content-Type: text/html; charset=utf-8'); - - if(isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) - { - $trailer = '<span style="color: #888888">Backtrace:<br /><pre>'.$e->getTraceAsString().'</pre></span>'; - $headerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/themes/default/simple_structure_header.tpl'); - $footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/themes/default/simple_structure_footer.tpl'); - $headerPage = str_replace('{$HTML_TITLE}', 'Piwik › Error', $headerPage); - - echo $headerPage . '<p>' . Piwik_Tracker_GetErrorMessage($e) . '</p>' . $trailer . $footerPage; - } - // If not debug, but running authenticated (eg. during log import) then we display raw errors - elseif($authenticated) - { - echo Piwik_Tracker_GetErrorMessage($e); - } - exit; + Piwik_Common::sendHeader('Content-Type: text/html; charset=utf-8'); + + if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) { + $trailer = '<span style="color: #888888">Backtrace:<br /><pre>' . $e->getTraceAsString() . '</pre></span>'; + $headerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/themes/default/simple_structure_header.tpl'); + $footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/themes/default/simple_structure_footer.tpl'); + $headerPage = str_replace('{$HTML_TITLE}', 'Piwik › Error', $headerPage); + + echo $headerPage . '<p>' . Piwik_Tracker_GetErrorMessage($e) . '</p>' . $trailer . $footerPage; + } // If not debug, but running authenticated (eg. during log import) then we display raw errors + elseif ($authenticated) { + echo Piwik_Tracker_GetErrorMessage($e); + } + exit; } diff --git a/core/Tracker/Action.php b/core/Tracker/Action.php index 3b7df131ef..7974fab927 100644 --- a/core/Tracker/Action.php +++ b/core/Tracker/Action.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -12,1106 +12,1081 @@ /** * Interface of the Action object. * New Action classes can be defined in plugins and used instead of the default one. - * + * * @package Piwik * @subpackage Piwik_Tracker */ -interface Piwik_Tracker_Action_Interface { - const TYPE_ACTION_URL = 1; - const TYPE_OUTLINK = 2; - const TYPE_DOWNLOAD = 3; - const TYPE_ACTION_NAME = 4; - const TYPE_ECOMMERCE_ITEM_SKU = 5; - const TYPE_ECOMMERCE_ITEM_NAME = 6; - const TYPE_ECOMMERCE_ITEM_CATEGORY = 7; - const TYPE_SITE_SEARCH = 8; - - public function setRequest($requestArray); - public function setIdSite( $idSite ); - public function init(); - public function getActionUrl(); - public function getActionName(); - public function getActionType(); - public function record( $idVisit, $visitorIdCookie, $idRefererActionUrl, $idRefererActionName, $timeSpentRefererAction ); - public function getIdActionUrl(); - public function getIdActionName(); - public function getIdLinkVisitAction(); +interface Piwik_Tracker_Action_Interface +{ + const TYPE_ACTION_URL = 1; + const TYPE_OUTLINK = 2; + const TYPE_DOWNLOAD = 3; + const TYPE_ACTION_NAME = 4; + const TYPE_ECOMMERCE_ITEM_SKU = 5; + const TYPE_ECOMMERCE_ITEM_NAME = 6; + const TYPE_ECOMMERCE_ITEM_CATEGORY = 7; + const TYPE_SITE_SEARCH = 8; + + public function setRequest($requestArray); + + public function setIdSite($idSite); + + public function init(); + + public function getActionUrl(); + + public function getActionName(); + + public function getActionType(); + + public function record($idVisit, $visitorIdCookie, $idRefererActionUrl, $idRefererActionName, $timeSpentRefererAction); + + public function getIdActionUrl(); + + public function getIdActionName(); + + public function getIdLinkVisitAction(); } /** * Handles an action (page view, download or outlink) by the visitor. * Parses the action name and URL from the request array, then records the action in the log table. - * + * * @package Piwik * @subpackage Piwik_Tracker */ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface { - private $request; - private $idSite; - private $timestamp; - private $idLinkVisitAction; - private $idActionName = false; - private $idActionUrl = false; - - private $actionName; - private $actionType; - private $actionUrl; - - private $searchCategory = false; - private $searchCount = false; - - private $timeGeneration = false; - - /** - * Encoding of HTML page being viewed. See reencodeParameters for more info. - * - * @var string - */ - private $pageEncoding = false; - - static private $queryParametersToExclude = array('phpsessid', 'jsessionid', 'sessionid', 'aspsessionid', 'fb_xd_fragment', 'fb_comment_id', 'doing_wp_cron'); - - /* Custom Variable names & slots used for Site Search metadata (category, results count) */ - const CVAR_KEY_SEARCH_CATEGORY = '_pk_scat'; - const CVAR_KEY_SEARCH_COUNT = '_pk_scount'; - const CVAR_INDEX_SEARCH_CATEGORY = '4'; - const CVAR_INDEX_SEARCH_COUNT = '5'; - - /* Tracking API Parameters used to force a site search request */ - const PARAMETER_NAME_SEARCH_COUNT = 'search_count'; - const PARAMETER_NAME_SEARCH_CATEGORY = 'search_cat'; - const PARAMETER_NAME_SEARCH_KEYWORD = 'search'; - - /* Custom Variables names & slots plus Tracking API Parameters for performance analytics */ - const DB_COLUMN_TIME_GENERATION = 'custom_float_1'; - const PARAMETER_NAME_TIME_GENERATION = 'generation_time_ms'; - - /** - * Map URL prefixes to integers. - * @see self::normalizeUrl(), self::reconstructNormalizedUrl() - */ - static public $urlPrefixMap = array( - 'http://www.' => 1, - 'http://' => 0, - 'https://www.' => 3, - 'https://' => 2 - ); - - /** - * Extract the prefix from a URL. - * Return the prefix ID and the rest. - * - * @param string $url - * @return array - */ - static public function normalizeUrl($url) - { - foreach (self::$urlPrefixMap as $prefix => $id) - { - if (strtolower(substr($url, 0, strlen($prefix))) == $prefix) - { - return array( - 'url' => substr($url, strlen($prefix)), - 'prefixId' => $id - ); - } - } - return array('url' => $url, 'prefixId' => null); - } - - /** - * Build the full URL from the prefix ID and the rest. - * - * @param string $url - * @param integer $prefixId - * @return string - */ - static public function reconstructNormalizedUrl($url, $prefixId) - { - $map = array_flip(self::$urlPrefixMap); - if ($prefixId !== null && isset($map[$prefixId])) - { - $fullUrl = $map[$prefixId].$url; - } - else - { - $fullUrl = $url; - } - - // Clean up host & hash tags, for URLs - $parsedUrl = @parse_url($fullUrl); - $parsedUrl = self::cleanupHostAndHashTag($parsedUrl); - $url = Piwik_Common::getParseUrlReverse($parsedUrl); - if(!empty($url)) - { - return $url; - } - - return $fullUrl; - } - - - /** - * Set request parameters - * - * @param array $requestArray - */ - public function setRequest($requestArray) - { - $this->request = $requestArray; - } - - /** - * Returns the current set request parameters - * - * @return array - */ - public function getRequest() - { - return $this->request; - } - - /** - * Returns URL of the page currently being tracked, or the file being downloaded, or the outlink being clicked - * - * @return string - */ - public function getActionUrl() - { - return $this->actionUrl; - } - public function getActionName() - { - return $this->actionName; - } - public function getActionType() - { - return $this->actionType; - } - public function getActionNameType() - { - $actionNameType = null; - - // we can add here action types for names of other actions than page views (like downloads, outlinks) - switch( $this->getActionType() ) - { - case Piwik_Tracker_Action_Interface::TYPE_ACTION_URL: - $actionNameType = Piwik_Tracker_Action_Interface::TYPE_ACTION_NAME; - break; - - case Piwik_Tracker_Action_Interface::TYPE_SITE_SEARCH: - $actionNameType = Piwik_Tracker_Action_Interface::TYPE_SITE_SEARCH; - break; - } - - return $actionNameType; - } - - public function getIdActionUrl() - { - $idUrl = $this->idActionUrl; - if(!empty($idUrl)) { - return $idUrl; - } - // Site Search, by default, will not track URL. We do not want URL to appear as "Page URL not defined" - // so we specifically set it to NULL in the table (the archiving query does IS NOT NULL) - if($this->getActionType() == self::TYPE_SITE_SEARCH) { - return null; - } - - // However, for other cases, we record idaction_url = 0 which will be displayed as "Page URL Not Defined" - return 0; - } - public function getIdActionName() - { - return $this->idActionName; - } - - protected function setActionName($name) - { - $name = self::cleanupString($name); - $this->actionName = $name; - } - - protected function setActionType($type) - { - $this->actionType = $type; - } - - protected function setActionUrl($url) - { - $this->actionUrl = $url; - } - - /** - * Converts Matrix URL format - * from http://example.org/thing;paramA=1;paramB=6542 - * to http://example.org/thing?paramA=1¶mB=6542 - * - * @param string $url - */ - static public function convertMatrixUrl($originalUrl) - { - $posFirstSemiColon = strpos($originalUrl,";"); - if($posFirstSemiColon === false) - { - return $originalUrl; - } - $posQuestionMark = strpos($originalUrl,"?"); - $replace = ($posQuestionMark === false); - if ($posQuestionMark > $posFirstSemiColon) - { - $originalUrl = substr_replace($originalUrl,";",$posQuestionMark,1); - $replace = true; - } - if($replace) - { - $originalUrl = substr_replace($originalUrl,"?",strpos($originalUrl,";"),1); - $originalUrl = str_replace(";","&",$originalUrl); - } - return $originalUrl; - } - - static public function cleanupUrl($url) - { - $url = Piwik_Common::unsanitizeInputValue($url); - $url = self::cleanupString($url); - $url = self::convertMatrixUrl($url); - return $url; - } - - /** - * Will cleanup the hostname (some browser do not strolower the hostname), - * and deal ith the hash tag on incoming URLs based on website setting. - * - * @param $parsedUrl - * @param $idSite int|false The site ID of the current visit. This parameter is - * only used by the tracker to see if we should remove - * the URL fragment for this site. - * @return array - */ - static public function cleanupHostAndHashTag($parsedUrl, $idSite = false) - { - if(empty($parsedUrl)) - { - return $parsedUrl; - } - if(!empty($parsedUrl['host'])) - { - $parsedUrl['host'] = mb_strtolower($parsedUrl['host'], 'UTF-8'); - } - - if(!empty($parsedUrl['fragment'])) - { - $parsedUrl['fragment'] = self::processUrlFragment($parsedUrl['fragment'], $idSite); - } - - return $parsedUrl; - } - - /** - * Cleans and/or removes the URL fragment of a URL. - * - * @param $urlFragment string The URL fragment to process. - * @param $idSite int|false If not false, this function will check if URL fragments - * should be removed for the site w/ this ID and if so, - * the returned processed fragment will be empty. - * @return string The processed URL fragment. - */ - public static function processUrlFragment($urlFragment, $idSite = false) - { - // if we should discard the url fragment for this site, return an empty string as - // the processed url fragment - if ($idSite !== false - && self::shouldRemoveURLFragmentFor($idSite)) - { - return ''; - } - else - { - // Remove trailing Hash tag in ?query#hash# - if (substr($urlFragment, -1) == '#') - { - $urlFragment = substr($urlFragment, 0, strlen($urlFragment) - 1); - } - return $urlFragment; - } - } - - /** - * Returns true if URL fragments should be removed for a specific site, - * false if otherwise. - * - * This function uses the Tracker cache and not the MySQL database. - * - * @param $idSite int The ID of the site to check for. - * @return bool - */ - public static function shouldRemoveURLFragmentFor( $idSite ) - { - $websiteAttributes = Piwik_Tracker_Cache::getCacheWebsiteAttributes($idSite); - return !$websiteAttributes['keep_url_fragment']; - } - - /** - * Given the Input URL, will exclude all query parameters set for this site - * Note: Site Search parameters are excluded in detectSiteSearch() - * @static - * @param $originalUrl - * @param $idSite - * @return bool|string - */ - static public function excludeQueryParametersFromUrl($originalUrl, $idSite) - { - $originalUrl = self::cleanupUrl($originalUrl); - - $parsedUrl = @parse_url($originalUrl); - $parsedUrl = self::cleanupHostAndHashTag($parsedUrl, $idSite); - $parametersToExclude = self::getQueryParametersToExclude($idSite); - - if(empty($parsedUrl['query'])) - { - if(empty($parsedUrl['fragment'])) - { - return Piwik_Common::getParseUrlReverse($parsedUrl); - } - // Exclude from the hash tag as well - $queryParameters = Piwik_Common::getArrayFromQueryString($parsedUrl['fragment']); - $parsedUrl['fragment'] = self::getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude); - $url = Piwik_Common::getParseUrlReverse($parsedUrl); - return $url; - } - $queryParameters = Piwik_Common::getArrayFromQueryString($parsedUrl['query']); - $parsedUrl['query'] = self::getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude); - $url = Piwik_Common::getParseUrlReverse($parsedUrl); - return $url; - } - - /** - * Returns the array of parameters names that must be excluded from the Query String in all tracked URLs - * @static - * @param $idSite - * @return array - */ - public static function getQueryParametersToExclude($idSite) - { - $campaignTrackingParameters = Piwik_Common::getCampaignParameters(); - - $campaignTrackingParameters = array_merge( - $campaignTrackingParameters[0], // campaign name parameters - $campaignTrackingParameters[1] // campaign keyword parameters - ); - - $website = Piwik_Tracker_Cache::getCacheWebsiteAttributes($idSite); - $excludedParameters = isset($website['excluded_parameters']) - ? $website['excluded_parameters'] - : array(); - - if(!empty($excludedParameters)) { - printDebug('Excluding parameters "' . implode(',', $excludedParameters) . '" from URL'); - } - - $parametersToExclude = array_merge($excludedParameters, - self::$queryParametersToExclude, - $campaignTrackingParameters, - array(self::PARAMETER_NAME_TIME_GENERATION)); - - $parametersToExclude = array_map('strtolower', $parametersToExclude); - return $parametersToExclude; - } - - /** - * Returns a Query string, - * Given an array of input parameters, and an array of parameter names to exclude - * - * @static - * @param $queryParameters - * @param $parametersToExclude - * @return string - */ - public static function getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude) - { - $validQuery = ''; - $separator = '&'; - foreach ($queryParameters as $name => $value) { - // decode encoded square brackets - $name = str_replace(array('%5B', '%5D'), array('[', ']'), $name); - - if (!in_array(strtolower($name), $parametersToExclude)) { - if (is_array($value)) { - foreach ($value as $param) { - if ($param === false) { - $validQuery .= $name . '[]' . $separator; - } else { - $validQuery .= $name . '[]=' . $param . $separator; - } - } - } else if ($value === false) { - $validQuery .= $name . $separator; - } else { - $validQuery .= $name . '=' . $value . $separator; - } - } - } - $validQuery = substr($validQuery,0,-strlen($separator)); - return $validQuery; - } - - public function init() - { - $this->pageEncoding = Piwik_Common::getRequestVar('cs', false, null, $this->request); - - $info = $this->extractUrlAndActionNameFromRequest(); - - $originalUrl = $info['url']; - $info['url'] = self::excludeQueryParametersFromUrl($originalUrl, $this->idSite); - - if($originalUrl != $info['url']) - { - printDebug(' Before was "'.$originalUrl.'"'); - printDebug(' After is "'.$info['url'].'"'); - } - - // Set Final attributes for this Action (Pageview, Search, etc.) - $this->setActionName($info['name']); - $this->setActionType($info['type']); - $this->setActionUrl($info['url']); - } - - static public function getSqlSelectActionId() - { - $sql = "SELECT idaction, type, name - FROM ".Piwik_Common::prefixTable('log_action') - ." WHERE " - ." ( hash = CRC32(?) AND name = ? AND type = ? ) "; - return $sql; - } - - /** - * This function will find the idaction from the lookup table piwik_log_action, - * given an Action name and type. - * - * This is used to record Page URLs, Page Titles, Ecommerce items SKUs, item names, item categories - * - * If the action name does not exist in the lookup table, it will INSERT it - * @param array $actionNamesAndTypes Array of one or many (name,type) - * @return array Returns the input array, with the idaction appended ie. Array of one or many (name,type,idaction) - */ - static public function loadActionId( $actionNamesAndTypes ) - { - // First, we try and select the actions that are already recorded - $sql = self::getSqlSelectActionId(); - $bind = array(); - $normalizedUrls = array(); - $i = 0; - foreach($actionNamesAndTypes as $index => &$actionNameType) - { - list($name,$type) = $actionNameType; - if(empty($name)) - { - $actionNameType[] = false; - continue; - } - if($i > 0) - { - $sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) "; - } - if ($type == Piwik_Tracker_Action::TYPE_ACTION_URL) - { - // normalize urls by stripping protocol and www - $normalizedUrls[$index] = self::normalizeUrl($name); - $name = $normalizedUrls[$index]['url']; - } - $bind[] = $name; - $bind[] = $name; - $bind[] = $type; - $i++; - } - // Case URL & Title are empty - if(empty($bind)) - { - return $actionNamesAndTypes; - } - $actionIds = Piwik_Tracker::getDatabase()->fetchAll($sql, $bind); - - // For the Actions found in the lookup table, add the idaction in the array, - // If not found in lookup table, queue for INSERT - $actionsToInsert = array(); - foreach($actionNamesAndTypes as $index => &$actionNameType) - { - list($name,$type) = $actionNameType; - if(empty($name)) { continue; } - if(isset($normalizedUrls[$index])) - { - $name = $normalizedUrls[$index]['url']; - } - $found = false; - foreach($actionIds as $row) - { - if($name == $row['name'] - && $type == $row['type']) - { - $found = true; - $actionNameType[] = $row['idaction']; - continue; - } - } - if(!$found) - { - $actionsToInsert[] = $index; - } - } - - $sql = "INSERT INTO ". Piwik_Common::prefixTable('log_action'). - "( name, hash, type, url_prefix ) VALUES (?,CRC32(?),?,?)"; - // Then, we insert all new actions in the lookup table - foreach($actionsToInsert as $actionToInsert) - { - list($name,$type) = $actionNamesAndTypes[$actionToInsert]; - - $urlPrefix = null; - if(isset($normalizedUrls[$actionToInsert])) - { - $name = $normalizedUrls[$actionToInsert]['url']; - $urlPrefix = $normalizedUrls[$actionToInsert]['prefixId']; - } - - Piwik_Tracker::getDatabase()->query($sql, array($name, $name, $type, $urlPrefix)); - $actionId = Piwik_Tracker::getDatabase()->lastInsertId(); - printDebug("Recorded a new action (".self::getActionTypeName($type).") in the lookup table: ". $name . " (idaction = ".$actionId.")"); - - $actionNamesAndTypes[$actionToInsert][] = $actionId; - } - return $actionNamesAndTypes; - } - - static public function getActionTypeName($type) - { - switch($type) - { - case self::TYPE_ACTION_URL: return 'Page URL'; break; - case self::TYPE_OUTLINK: return 'Outlink URL'; break; - case self::TYPE_DOWNLOAD: return 'Download URL'; break; - case self::TYPE_ACTION_NAME: return 'Page Title'; break; - case self::TYPE_SITE_SEARCH: return 'Site Search'; break; - case self::TYPE_ECOMMERCE_ITEM_SKU: return 'Ecommerce Item SKU'; break; - case self::TYPE_ECOMMERCE_ITEM_NAME: return 'Ecommerce Item Name'; break; - case self::TYPE_ECOMMERCE_ITEM_CATEGORY: return 'Ecommerce Item Category'; break; - default: throw new Exception("Unexpected action type ".$type); break; - } - } - - /** - * 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 - * (entry action, exit action) to the actions. - * These idactions are also used in the table that links the visits and their actions. - * - * The methods takes care of creating a new record(s) in the action table if the existing - * action name and action url doesn't exist yet. - */ - function loadIdActionNameAndUrl() - { - if( $this->idActionUrl !== false - && $this->idActionName !== false ) - { - return; - } - $actions = array(); - $nameType = $this->getActionNameType(); - $action = array($this->getActionName(), $nameType); - if(!is_null($action[1])) - { - $actions[] = $action; - } - - $urlType = $this->getActionType(); - $url = $this->getActionUrl(); - // this code is a mess, but basically, getActionType() returns SITE_SEARCH, - // but we do want to record the site search URL as an ACTION_URL - if($nameType == Piwik_Tracker_Action::TYPE_SITE_SEARCH) - { - $urlType = Piwik_Tracker_Action::TYPE_ACTION_URL; - - // By default, Site Search does not record the URL for the Search Result page, to slightly improve performance - if(empty(Piwik_Config::getInstance()->Tracker['action_sitesearch_record_url'])) - { - $url = false; - } - } - if(!is_null($urlType) && !empty($url)) - { - $actions[] = array($url, $urlType); - } - - $loadedActionIds = self::loadActionId($actions); - - foreach($loadedActionIds as $loadedActionId) - { - list($name, $type, $actionId) = $loadedActionId; - if($type == Piwik_Tracker_Action::TYPE_ACTION_NAME - || $type == Piwik_Tracker_Action::TYPE_SITE_SEARCH) - { - $this->idActionName = $actionId; - } - else - { - $this->idActionUrl = $actionId; - } - } - } - - /** - * @param int $idSite - */ - function setIdSite($idSite) - { - $this->idSite = $idSite; - } - - function setTimestamp($timestamp) - { - $this->timestamp = $timestamp; - } - - - /** - * Records in the DB the association between the visit and this action. - * - * @param int $idVisit is the ID of the current visit in the DB table log_visit - * @param $visitorIdCookie - * @param int $idRefererActionUrl is the ID of the last action done by the current visit. - * @param $idRefererActionName - * @param int $timeSpentRefererAction is the number of seconds since the last action was done. - * It is directly related to idRefererActionUrl. - */ - public function record( $idVisit, $visitorIdCookie, $idRefererActionUrl, $idRefererActionName, $timeSpentRefererAction) - { - $this->loadIdActionNameAndUrl(); - - $idActionName = in_array($this->getActionType(), array( Piwik_Tracker_Action::TYPE_ACTION_NAME, - Piwik_Tracker_Action::TYPE_ACTION_URL, - Piwik_Tracker_Action::TYPE_SITE_SEARCH)) - ? (int)$this->getIdActionName() - : null; - - - $insert = array( - 'idvisit' => $idVisit, - 'idsite' => $this->idSite, - 'idvisitor' => $visitorIdCookie, - 'server_time' => Piwik_Tracker::getDatetimeFromTimestamp($this->timestamp), - 'idaction_url' => $this->getIdActionUrl(), - 'idaction_name' => $idActionName, - 'idaction_url_ref' => $idRefererActionUrl, - 'idaction_name_ref' => $idRefererActionName, - 'time_spent_ref_action' => $timeSpentRefererAction - ); - - if (!empty($this->timeGeneration)) { - $insert[self::DB_COLUMN_TIME_GENERATION] = (int)$this->timeGeneration; - } - - $customVariables = $this->getCustomVariables(); - - $insert = array_merge($insert, $customVariables); - - // Mysqli apparently does not like NULL inserts? - $insertWithoutNulls = array(); - foreach($insert as $column => $value) - { - if(!is_null($value) || $column == 'idaction_url_ref') - { - $insertWithoutNulls[$column] = $value; - } - } - - $fields = implode(", ", array_keys($insertWithoutNulls)); - $bind = array_values($insertWithoutNulls); - $values = Piwik_Common::getSqlStringFieldsArray($insertWithoutNulls); - - $sql = "INSERT INTO ".Piwik_Common::prefixTable('log_link_visit_action'). " ($fields) VALUES ($values)"; - Piwik_Tracker::getDatabase()->query( $sql, $bind ); - - $this->idLinkVisitAction = Piwik_Tracker::getDatabase()->lastInsertId(); - - $info = array( - 'idSite' => $this->idSite, - 'idLinkVisitAction' => $this->idLinkVisitAction, - 'idVisit' => $idVisit, - 'idRefererActionUrl' => $idRefererActionUrl, - 'idRefererActionName' => $idRefererActionName, - 'timeSpentRefererAction' => $timeSpentRefererAction, - ); - printDebug($insertWithoutNulls); - - /* - * send the Action object ($this) and the list of ids ($info) as arguments to the event - */ - Piwik_PostEvent('Tracker.Action.record', $this, $info); - } - - public function getCustomVariables() - { - $customVariables = Piwik_Tracker_Visit::getCustomVariables($scope = 'page', $this->request); - - // Enrich Site Search actions with Custom Variables, overwriting existing values - if (!empty($this->searchCategory)) { - if (!empty($customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_CATEGORY])) { - printDebug("WARNING: Overwriting existing Custom Variable in slot " . self::CVAR_INDEX_SEARCH_CATEGORY . " for this page view"); - } - $customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_CATEGORY] = self::CVAR_KEY_SEARCH_CATEGORY; - $customVariables['custom_var_v' . self::CVAR_INDEX_SEARCH_CATEGORY] = Piwik_Tracker_Visit::truncateCustomVariable($this->searchCategory); - } - if ($this->searchCount !== false) { - if (!empty($customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_COUNT])) { - printDebug("WARNING: Overwriting existing Custom Variable in slot " . self::CVAR_INDEX_SEARCH_COUNT . " for this page view"); - } - $customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_COUNT] = self::CVAR_KEY_SEARCH_COUNT; - $customVariables['custom_var_v' . self::CVAR_INDEX_SEARCH_COUNT] = (int)$this->searchCount; - } - - if (!empty($customVariables)) - { - printDebug("Page level Custom Variables: "); - printDebug($customVariables); - } - return $customVariables; - } - - /** - * Returns the ID of the newly created record in the log_link_visit_action table - * - * @return int | false - */ - public function getIdLinkVisitAction() - { - return $this->idLinkVisitAction; - } - - /** - * Generates the name of the action from the URL or the specified name. - * Sets the name as $this->actionName - * - * @return array - */ - protected function extractUrlAndActionNameFromRequest() - { - $actionName = null; - - // download? - $downloadUrl = Piwik_Common::getRequestVar( 'download', '', 'string', $this->request); - if(!empty($downloadUrl)) - { - $actionType = self::TYPE_DOWNLOAD; - $url = $downloadUrl; - } - - // outlink? - if(empty($actionType)) - { - $outlinkUrl = Piwik_Common::getRequestVar( 'link', '', 'string', $this->request); - if(!empty($outlinkUrl)) - { - $actionType = self::TYPE_OUTLINK; - $url = $outlinkUrl; - } - } - - // handle encoding - $actionName = Piwik_Common::getRequestVar( 'action_name', '', 'string', $this->request); - - // defaults to page view - if(empty($actionType)) - { - $actionType = self::TYPE_ACTION_URL; - $url = Piwik_Common::getRequestVar( 'url', '', 'string', $this->request); - - // get the delimiter, by default '/'; BC, we read the old action_category_delimiter first (see #1067) - $actionCategoryDelimiter = isset(Piwik_Config::getInstance()->General['action_category_delimiter']) - ? Piwik_Config::getInstance()->General['action_category_delimiter'] - : Piwik_Config::getInstance()->General['action_url_category_delimiter']; - - // create an array of the categories delimited by the delimiter - $split = explode($actionCategoryDelimiter, $actionName); - - // trim every category - $split = array_map('trim', $split); - - // remove empty categories - $split = array_filter($split, 'strlen'); - - // rebuild the name from the array of cleaned categories - $actionName = implode($actionCategoryDelimiter, $split); - } - $url = self::cleanupString($url); - - if(!Piwik_Common::isLookLikeUrl($url)) - { - printDebug("WARNING: URL looks invalid and is discarded"); - $url = ''; - } - - // Site search? - if($actionType == self::TYPE_ACTION_URL) - { - // Look in tracked URL for the Site Search parameters - $siteSearch = $this->detectSiteSearch($url); - if(!empty($siteSearch)) - { - $actionType = self::TYPE_SITE_SEARCH; - list($actionName, $url) = $siteSearch; - } - // Look for performance analytics parameters - $this->detectPerformanceAnalyticsParameters(); - } - $actionName = self::cleanupString($actionName); - - return array( - 'name' => empty($actionName) ? '' : $actionName, - 'type' => $actionType, - 'url' => $url, - ); - } - - protected function detectSiteSearch($originalUrl) - { - $website = Piwik_Tracker_Cache::getCacheWebsiteAttributes($this->idSite); - if(empty($website['sitesearch'])) - { - printDebug("Internal 'Site Search' tracking is not enabled for this site. "); - return false; - } - $actionName = $url = $categoryName = $count = false; - $doTrackUrlForSiteSearch = !empty(Piwik_Config::getInstance()->Tracker['action_sitesearch_record_url']); - - $originalUrl = self::cleanupUrl($originalUrl); - - - // Detect Site search from Tracking API parameters rather than URL - $searchKwd = Piwik_Common::getRequestVar( self::PARAMETER_NAME_SEARCH_KEYWORD, '', 'string', $this->request); - if(!empty($searchKwd)) - { - $actionName = $searchKwd; - if($doTrackUrlForSiteSearch) { - $url = $originalUrl; - } - $isCategoryName = Piwik_Common::getRequestVar( self::PARAMETER_NAME_SEARCH_CATEGORY, false, 'string', $this->request); - if(!empty($isCategoryName)) { - $categoryName = $isCategoryName; - } - $isCount = Piwik_Common::getRequestVar( self::PARAMETER_NAME_SEARCH_COUNT, -1, 'int', $this->request); - if($this->isValidSearchCount($isCount)) { - $count = $isCount; - } - } - - if(empty($actionName)) - { - $parsedUrl = @parse_url($originalUrl); - - // Detect Site Search from URL query parameters - if(!empty($parsedUrl['query']) || !empty($parsedUrl['fragment'])) - { - // array($url, $actionName, $categoryName, $count); - $searchInfo = $this->detectSiteSearchFromUrl($website, $parsedUrl); - if(!empty($searchInfo)) { - list ($url, $actionName, $categoryName, $count) = $searchInfo; - } - } - } - - if(empty($actionName)) - { - printDebug("(this is not a Site Search request)"); - return false; - } - - printDebug("Detected Site Search keyword '$actionName'. "); - if (!empty($categoryName)) - { - printDebug("- Detected Site Search Category '$categoryName'. "); - } - if ($count !== false) - { - printDebug("- Search Results Count was '$count'. "); - } - if($url != $originalUrl) { - printDebug("NOTE: The Page URL was changed / removed, during the Site Search detection, was '$originalUrl', now is '$url'"); - } - - if(!empty($categoryName) || $count !== false) { - $this->setActionSearchMetadata($categoryName, $count); - } - return array( - $actionName, - $url - ); - } - - protected function isValidSearchCount($count) - { - return is_numeric($count) && $count >= 0; - } - - - protected function setActionSearchMetadata($category, $count) - { - if(!empty($category)) { - $this->searchCategory = trim($category); - } - if($count !== false) { - $this->searchCount = $count; - } - } - - protected function detectSiteSearchFromUrl($website, $parsedUrl) - { - $doRemoveSearchParametersFromUrl = false; - $separator = '&'; - $count = $actionName = $categoryName = false; - - $keywordParameters = isset($website['sitesearch_keyword_parameters']) - ? $website['sitesearch_keyword_parameters'] - : array(); - $queryString = (!empty($parsedUrl['query']) ? $parsedUrl['query'] : '') . (!empty($parsedUrl['fragment']) ? $separator . $parsedUrl['fragment'] : ''); - $parametersRaw = Piwik_Common::getArrayFromQueryString($queryString); - - // strtolower the parameter names for smooth site search detection - $parameters = array(); - foreach($parametersRaw as $k => $v) { - $parameters[Piwik_Common::mb_strtolower($k)] = $v; - } - // decode values if they were sent from a client using another charset - self::reencodeParameters($parameters, $this->pageEncoding); - - // Detect Site Search keyword - foreach ($keywordParameters as $keywordParameterRaw) { - $keywordParameter = Piwik_Common::mb_strtolower($keywordParameterRaw); - if (!empty($parameters[$keywordParameter])) { - $actionName = $parameters[$keywordParameter]; - break; - } - } - - if (empty($actionName)) - { - return false; - } - - $categoryParameters = isset($website['sitesearch_category_parameters']) - ? $website['sitesearch_category_parameters'] - : array(); - - foreach ($categoryParameters as $categoryParameterRaw) { - $categoryParameter = Piwik_Common::mb_strtolower($categoryParameterRaw); - if (!empty($parameters[$categoryParameter])) { - $categoryName = $parameters[$categoryParameter]; - break; - } - } - - if(isset($parameters[self::PARAMETER_NAME_SEARCH_COUNT]) - && $this->isValidSearchCount($parameters[self::PARAMETER_NAME_SEARCH_COUNT])) - { - $count = $parameters[self::PARAMETER_NAME_SEARCH_COUNT]; - } - // Remove search kwd from URL - if ($doRemoveSearchParametersFromUrl) - { - // @see excludeQueryParametersFromUrl() - // Excluded the detected parameters from the URL - $parametersToExclude = array($categoryParameterRaw, $keywordParameterRaw); - $parsedUrl['query'] = self::getQueryStringWithExcludedParameters(Piwik_Common::getArrayFromQueryString($parsedUrl['query']), $parametersToExclude); - $parsedUrl['fragment'] = self::getQueryStringWithExcludedParameters(Piwik_Common::getArrayFromQueryString($parsedUrl['fragment']), $parametersToExclude); - } - $url = Piwik_Common::getParseUrlReverse($parsedUrl); - if(is_array($actionName)) { - $actionName = reset($actionName); - } - $actionName = trim(urldecode($actionName)); - if(empty($actionName)) { - return false; - } - if(is_array($categoryName)) { - $categoryName = reset($categoryName); - } - $categoryName = trim(urldecode($categoryName)); - return array($url, $actionName, $categoryName, $count); - } - - protected function detectPerformanceAnalyticsParameters() - { - $generationTime = Piwik_Common::getRequestVar(self::PARAMETER_NAME_TIME_GENERATION, -1, 'int', $this->request); - if ($generationTime > 0) { - $this->timeGeneration = $generationTime; - } - } - - /** - * Clean up string contents (filter, truncate, ...) - * - * @param string $string Dirty string - * @return string - */ - protected static function cleanupString($string) - { - $string = trim($string); - $string = str_replace(array("\n", "\r", "\0"), '', $string); - - $limit = Piwik_Config::getInstance()->Tracker['page_maximum_length']; - return substr($string, 0, $limit); - } - - /** - * Checks if query parameters are of a non-UTF-8 encoding and converts the values - * from the specified encoding to UTF-8. - * - * This method is used to workaround browser/webapp bugs (see #3450). When - * browsers fail to encode query parameters in UTF-8, the tracker will send the - * charset of the page viewed and we can sometimes work around invalid data - * being stored. - * - * @param array $queryParameters Name/value mapping of query parameters. - * @param string|false $encoding of the HTML page the URL is for. Used to workaround - * browser bugs & mis-coded webapps. See #3450. - */ - private static function reencodeParameters( &$queryParameters, $encoding = false ) - { - // if query params are encoded w/ non-utf8 characters (due to browser bug or whatever), - // encode to UTF-8. - if ($encoding !== false - && strtolower($encoding) != 'utf-8' - && function_exists('mb_check_encoding')) - { - $queryParameters = self::reencodeParametersArray($queryParameters, $encoding); - } - return $queryParameters; - } - - private static function reencodeParametersArray($queryParameters, $encoding) - { - foreach($queryParameters as &$value) - { - if(is_array($value)) { - $value = self::reencodeParametersArray($value, $encoding); - } else { - $value = self::reencodeParameterValue($value, $encoding); - } - } - return $queryParameters; - } - - private static function reencodeParameterValue($value, $encoding) - { - if(is_string($value)) - { - $decoded = urldecode($value); - if (@mb_check_encoding($decoded, $encoding)) { - $value = urlencode(mb_convert_encoding($decoded, 'UTF-8', $encoding)); - } - } - return $value; - } + private $request; + private $idSite; + private $timestamp; + private $idLinkVisitAction; + private $idActionName = false; + private $idActionUrl = false; + + private $actionName; + private $actionType; + private $actionUrl; + + private $searchCategory = false; + private $searchCount = false; + + private $timeGeneration = false; + + /** + * Encoding of HTML page being viewed. See reencodeParameters for more info. + * + * @var string + */ + private $pageEncoding = false; + + static private $queryParametersToExclude = array('phpsessid', 'jsessionid', 'sessionid', 'aspsessionid', 'fb_xd_fragment', 'fb_comment_id', 'doing_wp_cron'); + + /* Custom Variable names & slots used for Site Search metadata (category, results count) */ + const CVAR_KEY_SEARCH_CATEGORY = '_pk_scat'; + const CVAR_KEY_SEARCH_COUNT = '_pk_scount'; + const CVAR_INDEX_SEARCH_CATEGORY = '4'; + const CVAR_INDEX_SEARCH_COUNT = '5'; + + /* Tracking API Parameters used to force a site search request */ + const PARAMETER_NAME_SEARCH_COUNT = 'search_count'; + const PARAMETER_NAME_SEARCH_CATEGORY = 'search_cat'; + const PARAMETER_NAME_SEARCH_KEYWORD = 'search'; + + /* Custom Variables names & slots plus Tracking API Parameters for performance analytics */ + const DB_COLUMN_TIME_GENERATION = 'custom_float_1'; + const PARAMETER_NAME_TIME_GENERATION = 'generation_time_ms'; + + /** + * Map URL prefixes to integers. + * @see self::normalizeUrl(), self::reconstructNormalizedUrl() + */ + static public $urlPrefixMap = array( + 'http://www.' => 1, + 'http://' => 0, + 'https://www.' => 3, + 'https://' => 2 + ); + + /** + * Extract the prefix from a URL. + * Return the prefix ID and the rest. + * + * @param string $url + * @return array + */ + static public function normalizeUrl($url) + { + foreach (self::$urlPrefixMap as $prefix => $id) { + if (strtolower(substr($url, 0, strlen($prefix))) == $prefix) { + return array( + 'url' => substr($url, strlen($prefix)), + 'prefixId' => $id + ); + } + } + return array('url' => $url, 'prefixId' => null); + } + + /** + * Build the full URL from the prefix ID and the rest. + * + * @param string $url + * @param integer $prefixId + * @return string + */ + static public function reconstructNormalizedUrl($url, $prefixId) + { + $map = array_flip(self::$urlPrefixMap); + if ($prefixId !== null && isset($map[$prefixId])) { + $fullUrl = $map[$prefixId] . $url; + } else { + $fullUrl = $url; + } + + // Clean up host & hash tags, for URLs + $parsedUrl = @parse_url($fullUrl); + $parsedUrl = self::cleanupHostAndHashTag($parsedUrl); + $url = Piwik_Common::getParseUrlReverse($parsedUrl); + if (!empty($url)) { + return $url; + } + + return $fullUrl; + } + + + /** + * Set request parameters + * + * @param array $requestArray + */ + public function setRequest($requestArray) + { + $this->request = $requestArray; + } + + /** + * Returns the current set request parameters + * + * @return array + */ + public function getRequest() + { + return $this->request; + } + + /** + * Returns URL of the page currently being tracked, or the file being downloaded, or the outlink being clicked + * + * @return string + */ + public function getActionUrl() + { + return $this->actionUrl; + } + + public function getActionName() + { + return $this->actionName; + } + + public function getActionType() + { + return $this->actionType; + } + + public function getActionNameType() + { + $actionNameType = null; + + // we can add here action types for names of other actions than page views (like downloads, outlinks) + switch ($this->getActionType()) { + case Piwik_Tracker_Action_Interface::TYPE_ACTION_URL: + $actionNameType = Piwik_Tracker_Action_Interface::TYPE_ACTION_NAME; + break; + + case Piwik_Tracker_Action_Interface::TYPE_SITE_SEARCH: + $actionNameType = Piwik_Tracker_Action_Interface::TYPE_SITE_SEARCH; + break; + } + + return $actionNameType; + } + + public function getIdActionUrl() + { + $idUrl = $this->idActionUrl; + if (!empty($idUrl)) { + return $idUrl; + } + // Site Search, by default, will not track URL. We do not want URL to appear as "Page URL not defined" + // so we specifically set it to NULL in the table (the archiving query does IS NOT NULL) + if ($this->getActionType() == self::TYPE_SITE_SEARCH) { + return null; + } + + // However, for other cases, we record idaction_url = 0 which will be displayed as "Page URL Not Defined" + return 0; + } + + public function getIdActionName() + { + return $this->idActionName; + } + + protected function setActionName($name) + { + $name = self::cleanupString($name); + $this->actionName = $name; + } + + protected function setActionType($type) + { + $this->actionType = $type; + } + + protected function setActionUrl($url) + { + $this->actionUrl = $url; + } + + /** + * Converts Matrix URL format + * from http://example.org/thing;paramA=1;paramB=6542 + * to http://example.org/thing?paramA=1¶mB=6542 + * + * @param string $url + */ + static public function convertMatrixUrl($originalUrl) + { + $posFirstSemiColon = strpos($originalUrl, ";"); + if ($posFirstSemiColon === false) { + return $originalUrl; + } + $posQuestionMark = strpos($originalUrl, "?"); + $replace = ($posQuestionMark === false); + if ($posQuestionMark > $posFirstSemiColon) { + $originalUrl = substr_replace($originalUrl, ";", $posQuestionMark, 1); + $replace = true; + } + if ($replace) { + $originalUrl = substr_replace($originalUrl, "?", strpos($originalUrl, ";"), 1); + $originalUrl = str_replace(";", "&", $originalUrl); + } + return $originalUrl; + } + + static public function cleanupUrl($url) + { + $url = Piwik_Common::unsanitizeInputValue($url); + $url = self::cleanupString($url); + $url = self::convertMatrixUrl($url); + return $url; + } + + /** + * Will cleanup the hostname (some browser do not strolower the hostname), + * and deal ith the hash tag on incoming URLs based on website setting. + * + * @param $parsedUrl + * @param $idSite int|false The site ID of the current visit. This parameter is + * only used by the tracker to see if we should remove + * the URL fragment for this site. + * @return array + */ + static public function cleanupHostAndHashTag($parsedUrl, $idSite = false) + { + if (empty($parsedUrl)) { + return $parsedUrl; + } + if (!empty($parsedUrl['host'])) { + $parsedUrl['host'] = mb_strtolower($parsedUrl['host'], 'UTF-8'); + } + + if (!empty($parsedUrl['fragment'])) { + $parsedUrl['fragment'] = self::processUrlFragment($parsedUrl['fragment'], $idSite); + } + + return $parsedUrl; + } + + /** + * Cleans and/or removes the URL fragment of a URL. + * + * @param $urlFragment string The URL fragment to process. + * @param $idSite int|false If not false, this function will check if URL fragments + * should be removed for the site w/ this ID and if so, + * the returned processed fragment will be empty. + * @return string The processed URL fragment. + */ + public static function processUrlFragment($urlFragment, $idSite = false) + { + // if we should discard the url fragment for this site, return an empty string as + // the processed url fragment + if ($idSite !== false + && self::shouldRemoveURLFragmentFor($idSite) + ) { + return ''; + } else { + // Remove trailing Hash tag in ?query#hash# + if (substr($urlFragment, -1) == '#') { + $urlFragment = substr($urlFragment, 0, strlen($urlFragment) - 1); + } + return $urlFragment; + } + } + + /** + * Returns true if URL fragments should be removed for a specific site, + * false if otherwise. + * + * This function uses the Tracker cache and not the MySQL database. + * + * @param $idSite int The ID of the site to check for. + * @return bool + */ + public static function shouldRemoveURLFragmentFor($idSite) + { + $websiteAttributes = Piwik_Tracker_Cache::getCacheWebsiteAttributes($idSite); + return !$websiteAttributes['keep_url_fragment']; + } + + /** + * Given the Input URL, will exclude all query parameters set for this site + * Note: Site Search parameters are excluded in detectSiteSearch() + * @static + * @param $originalUrl + * @param $idSite + * @return bool|string + */ + static public function excludeQueryParametersFromUrl($originalUrl, $idSite) + { + $originalUrl = self::cleanupUrl($originalUrl); + + $parsedUrl = @parse_url($originalUrl); + $parsedUrl = self::cleanupHostAndHashTag($parsedUrl, $idSite); + $parametersToExclude = self::getQueryParametersToExclude($idSite); + + if (empty($parsedUrl['query'])) { + if (empty($parsedUrl['fragment'])) { + return Piwik_Common::getParseUrlReverse($parsedUrl); + } + // Exclude from the hash tag as well + $queryParameters = Piwik_Common::getArrayFromQueryString($parsedUrl['fragment']); + $parsedUrl['fragment'] = self::getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude); + $url = Piwik_Common::getParseUrlReverse($parsedUrl); + return $url; + } + $queryParameters = Piwik_Common::getArrayFromQueryString($parsedUrl['query']); + $parsedUrl['query'] = self::getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude); + $url = Piwik_Common::getParseUrlReverse($parsedUrl); + return $url; + } + + /** + * Returns the array of parameters names that must be excluded from the Query String in all tracked URLs + * @static + * @param $idSite + * @return array + */ + public static function getQueryParametersToExclude($idSite) + { + $campaignTrackingParameters = Piwik_Common::getCampaignParameters(); + + $campaignTrackingParameters = array_merge( + $campaignTrackingParameters[0], // campaign name parameters + $campaignTrackingParameters[1] // campaign keyword parameters + ); + + $website = Piwik_Tracker_Cache::getCacheWebsiteAttributes($idSite); + $excludedParameters = isset($website['excluded_parameters']) + ? $website['excluded_parameters'] + : array(); + + if (!empty($excludedParameters)) { + printDebug('Excluding parameters "' . implode(',', $excludedParameters) . '" from URL'); + } + + $parametersToExclude = array_merge($excludedParameters, + self::$queryParametersToExclude, + $campaignTrackingParameters, + array(self::PARAMETER_NAME_TIME_GENERATION)); + + $parametersToExclude = array_map('strtolower', $parametersToExclude); + return $parametersToExclude; + } + + /** + * Returns a Query string, + * Given an array of input parameters, and an array of parameter names to exclude + * + * @static + * @param $queryParameters + * @param $parametersToExclude + * @return string + */ + public static function getQueryStringWithExcludedParameters($queryParameters, $parametersToExclude) + { + $validQuery = ''; + $separator = '&'; + foreach ($queryParameters as $name => $value) { + // decode encoded square brackets + $name = str_replace(array('%5B', '%5D'), array('[', ']'), $name); + + if (!in_array(strtolower($name), $parametersToExclude)) { + if (is_array($value)) { + foreach ($value as $param) { + if ($param === false) { + $validQuery .= $name . '[]' . $separator; + } else { + $validQuery .= $name . '[]=' . $param . $separator; + } + } + } else if ($value === false) { + $validQuery .= $name . $separator; + } else { + $validQuery .= $name . '=' . $value . $separator; + } + } + } + $validQuery = substr($validQuery, 0, -strlen($separator)); + return $validQuery; + } + + public function init() + { + $this->pageEncoding = Piwik_Common::getRequestVar('cs', false, null, $this->request); + + $info = $this->extractUrlAndActionNameFromRequest(); + + $originalUrl = $info['url']; + $info['url'] = self::excludeQueryParametersFromUrl($originalUrl, $this->idSite); + + if ($originalUrl != $info['url']) { + printDebug(' Before was "' . $originalUrl . '"'); + printDebug(' After is "' . $info['url'] . '"'); + } + + // Set Final attributes for this Action (Pageview, Search, etc.) + $this->setActionName($info['name']); + $this->setActionType($info['type']); + $this->setActionUrl($info['url']); + } + + static public function getSqlSelectActionId() + { + $sql = "SELECT idaction, type, name + FROM " . Piwik_Common::prefixTable('log_action') + . " WHERE " + . " ( hash = CRC32(?) AND name = ? AND type = ? ) "; + return $sql; + } + + /** + * This function will find the idaction from the lookup table piwik_log_action, + * given an Action name and type. + * + * This is used to record Page URLs, Page Titles, Ecommerce items SKUs, item names, item categories + * + * If the action name does not exist in the lookup table, it will INSERT it + * @param array $actionNamesAndTypes Array of one or many (name,type) + * @return array Returns the input array, with the idaction appended ie. Array of one or many (name,type,idaction) + */ + static public function loadActionId($actionNamesAndTypes) + { + // First, we try and select the actions that are already recorded + $sql = self::getSqlSelectActionId(); + $bind = array(); + $normalizedUrls = array(); + $i = 0; + foreach ($actionNamesAndTypes as $index => &$actionNameType) { + list($name, $type) = $actionNameType; + if (empty($name)) { + $actionNameType[] = false; + continue; + } + if ($i > 0) { + $sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) "; + } + if ($type == Piwik_Tracker_Action::TYPE_ACTION_URL) { + // normalize urls by stripping protocol and www + $normalizedUrls[$index] = self::normalizeUrl($name); + $name = $normalizedUrls[$index]['url']; + } + $bind[] = $name; + $bind[] = $name; + $bind[] = $type; + $i++; + } + // Case URL & Title are empty + if (empty($bind)) { + return $actionNamesAndTypes; + } + $actionIds = Piwik_Tracker::getDatabase()->fetchAll($sql, $bind); + + // For the Actions found in the lookup table, add the idaction in the array, + // If not found in lookup table, queue for INSERT + $actionsToInsert = array(); + foreach ($actionNamesAndTypes as $index => &$actionNameType) { + list($name, $type) = $actionNameType; + if (empty($name)) { + continue; + } + if (isset($normalizedUrls[$index])) { + $name = $normalizedUrls[$index]['url']; + } + $found = false; + foreach ($actionIds as $row) { + if ($name == $row['name'] + && $type == $row['type'] + ) { + $found = true; + $actionNameType[] = $row['idaction']; + continue; + } + } + if (!$found) { + $actionsToInsert[] = $index; + } + } + + $sql = "INSERT INTO " . Piwik_Common::prefixTable('log_action') . + "( name, hash, type, url_prefix ) VALUES (?,CRC32(?),?,?)"; + // Then, we insert all new actions in the lookup table + foreach ($actionsToInsert as $actionToInsert) { + list($name, $type) = $actionNamesAndTypes[$actionToInsert]; + + $urlPrefix = null; + if (isset($normalizedUrls[$actionToInsert])) { + $name = $normalizedUrls[$actionToInsert]['url']; + $urlPrefix = $normalizedUrls[$actionToInsert]['prefixId']; + } + + Piwik_Tracker::getDatabase()->query($sql, array($name, $name, $type, $urlPrefix)); + $actionId = Piwik_Tracker::getDatabase()->lastInsertId(); + printDebug("Recorded a new action (" . self::getActionTypeName($type) . ") in the lookup table: " . $name . " (idaction = " . $actionId . ")"); + + $actionNamesAndTypes[$actionToInsert][] = $actionId; + } + return $actionNamesAndTypes; + } + + static public function getActionTypeName($type) + { + switch ($type) { + case self::TYPE_ACTION_URL: + return 'Page URL'; + break; + case self::TYPE_OUTLINK: + return 'Outlink URL'; + break; + case self::TYPE_DOWNLOAD: + return 'Download URL'; + break; + case self::TYPE_ACTION_NAME: + return 'Page Title'; + break; + case self::TYPE_SITE_SEARCH: + return 'Site Search'; + break; + case self::TYPE_ECOMMERCE_ITEM_SKU: + return 'Ecommerce Item SKU'; + break; + case self::TYPE_ECOMMERCE_ITEM_NAME: + return 'Ecommerce Item Name'; + break; + case self::TYPE_ECOMMERCE_ITEM_CATEGORY: + return 'Ecommerce Item Category'; + break; + default: + throw new Exception("Unexpected action type " . $type); + break; + } + } + + /** + * 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 + * (entry action, exit action) to the actions. + * These idactions are also used in the table that links the visits and their actions. + * + * The methods takes care of creating a new record(s) in the action table if the existing + * action name and action url doesn't exist yet. + */ + function loadIdActionNameAndUrl() + { + if ($this->idActionUrl !== false + && $this->idActionName !== false + ) { + return; + } + $actions = array(); + $nameType = $this->getActionNameType(); + $action = array($this->getActionName(), $nameType); + if (!is_null($action[1])) { + $actions[] = $action; + } + + $urlType = $this->getActionType(); + $url = $this->getActionUrl(); + // this code is a mess, but basically, getActionType() returns SITE_SEARCH, + // but we do want to record the site search URL as an ACTION_URL + if ($nameType == Piwik_Tracker_Action::TYPE_SITE_SEARCH) { + $urlType = Piwik_Tracker_Action::TYPE_ACTION_URL; + + // By default, Site Search does not record the URL for the Search Result page, to slightly improve performance + if (empty(Piwik_Config::getInstance()->Tracker['action_sitesearch_record_url'])) { + $url = false; + } + } + if (!is_null($urlType) && !empty($url)) { + $actions[] = array($url, $urlType); + } + + $loadedActionIds = self::loadActionId($actions); + + foreach ($loadedActionIds as $loadedActionId) { + list($name, $type, $actionId) = $loadedActionId; + if ($type == Piwik_Tracker_Action::TYPE_ACTION_NAME + || $type == Piwik_Tracker_Action::TYPE_SITE_SEARCH + ) { + $this->idActionName = $actionId; + } else { + $this->idActionUrl = $actionId; + } + } + } + + /** + * @param int $idSite + */ + function setIdSite($idSite) + { + $this->idSite = $idSite; + } + + function setTimestamp($timestamp) + { + $this->timestamp = $timestamp; + } + + + /** + * Records in the DB the association between the visit and this action. + * + * @param int $idVisit is the ID of the current visit in the DB table log_visit + * @param $visitorIdCookie + * @param int $idRefererActionUrl is the ID of the last action done by the current visit. + * @param $idRefererActionName + * @param int $timeSpentRefererAction is the number of seconds since the last action was done. + * It is directly related to idRefererActionUrl. + */ + public function record($idVisit, $visitorIdCookie, $idRefererActionUrl, $idRefererActionName, $timeSpentRefererAction) + { + $this->loadIdActionNameAndUrl(); + + $idActionName = in_array($this->getActionType(), array(Piwik_Tracker_Action::TYPE_ACTION_NAME, + Piwik_Tracker_Action::TYPE_ACTION_URL, + Piwik_Tracker_Action::TYPE_SITE_SEARCH)) + ? (int)$this->getIdActionName() + : null; + + + $insert = array( + 'idvisit' => $idVisit, + 'idsite' => $this->idSite, + 'idvisitor' => $visitorIdCookie, + 'server_time' => Piwik_Tracker::getDatetimeFromTimestamp($this->timestamp), + 'idaction_url' => $this->getIdActionUrl(), + 'idaction_name' => $idActionName, + 'idaction_url_ref' => $idRefererActionUrl, + 'idaction_name_ref' => $idRefererActionName, + 'time_spent_ref_action' => $timeSpentRefererAction + ); + + if (!empty($this->timeGeneration)) { + $insert[self::DB_COLUMN_TIME_GENERATION] = (int)$this->timeGeneration; + } + + $customVariables = $this->getCustomVariables(); + + $insert = array_merge($insert, $customVariables); + + // Mysqli apparently does not like NULL inserts? + $insertWithoutNulls = array(); + foreach ($insert as $column => $value) { + if (!is_null($value) || $column == 'idaction_url_ref') { + $insertWithoutNulls[$column] = $value; + } + } + + $fields = implode(", ", array_keys($insertWithoutNulls)); + $bind = array_values($insertWithoutNulls); + $values = Piwik_Common::getSqlStringFieldsArray($insertWithoutNulls); + + $sql = "INSERT INTO " . Piwik_Common::prefixTable('log_link_visit_action') . " ($fields) VALUES ($values)"; + Piwik_Tracker::getDatabase()->query($sql, $bind); + + $this->idLinkVisitAction = Piwik_Tracker::getDatabase()->lastInsertId(); + + $info = array( + 'idSite' => $this->idSite, + 'idLinkVisitAction' => $this->idLinkVisitAction, + 'idVisit' => $idVisit, + 'idRefererActionUrl' => $idRefererActionUrl, + 'idRefererActionName' => $idRefererActionName, + 'timeSpentRefererAction' => $timeSpentRefererAction, + ); + printDebug($insertWithoutNulls); + + /* + * send the Action object ($this) and the list of ids ($info) as arguments to the event + */ + Piwik_PostEvent('Tracker.Action.record', $this, $info); + } + + public function getCustomVariables() + { + $customVariables = Piwik_Tracker_Visit::getCustomVariables($scope = 'page', $this->request); + + // Enrich Site Search actions with Custom Variables, overwriting existing values + if (!empty($this->searchCategory)) { + if (!empty($customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_CATEGORY])) { + printDebug("WARNING: Overwriting existing Custom Variable in slot " . self::CVAR_INDEX_SEARCH_CATEGORY . " for this page view"); + } + $customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_CATEGORY] = self::CVAR_KEY_SEARCH_CATEGORY; + $customVariables['custom_var_v' . self::CVAR_INDEX_SEARCH_CATEGORY] = Piwik_Tracker_Visit::truncateCustomVariable($this->searchCategory); + } + if ($this->searchCount !== false) { + if (!empty($customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_COUNT])) { + printDebug("WARNING: Overwriting existing Custom Variable in slot " . self::CVAR_INDEX_SEARCH_COUNT . " for this page view"); + } + $customVariables['custom_var_k' . self::CVAR_INDEX_SEARCH_COUNT] = self::CVAR_KEY_SEARCH_COUNT; + $customVariables['custom_var_v' . self::CVAR_INDEX_SEARCH_COUNT] = (int)$this->searchCount; + } + + if (!empty($customVariables)) { + printDebug("Page level Custom Variables: "); + printDebug($customVariables); + } + return $customVariables; + } + + /** + * Returns the ID of the newly created record in the log_link_visit_action table + * + * @return int | false + */ + public function getIdLinkVisitAction() + { + return $this->idLinkVisitAction; + } + + /** + * Generates the name of the action from the URL or the specified name. + * Sets the name as $this->actionName + * + * @return array + */ + protected function extractUrlAndActionNameFromRequest() + { + $actionName = null; + + // download? + $downloadUrl = Piwik_Common::getRequestVar('download', '', 'string', $this->request); + if (!empty($downloadUrl)) { + $actionType = self::TYPE_DOWNLOAD; + $url = $downloadUrl; + } + + // outlink? + if (empty($actionType)) { + $outlinkUrl = Piwik_Common::getRequestVar('link', '', 'string', $this->request); + if (!empty($outlinkUrl)) { + $actionType = self::TYPE_OUTLINK; + $url = $outlinkUrl; + } + } + + // handle encoding + $actionName = Piwik_Common::getRequestVar('action_name', '', 'string', $this->request); + + // defaults to page view + if (empty($actionType)) { + $actionType = self::TYPE_ACTION_URL; + $url = Piwik_Common::getRequestVar('url', '', 'string', $this->request); + + // get the delimiter, by default '/'; BC, we read the old action_category_delimiter first (see #1067) + $actionCategoryDelimiter = isset(Piwik_Config::getInstance()->General['action_category_delimiter']) + ? Piwik_Config::getInstance()->General['action_category_delimiter'] + : Piwik_Config::getInstance()->General['action_url_category_delimiter']; + + // create an array of the categories delimited by the delimiter + $split = explode($actionCategoryDelimiter, $actionName); + + // trim every category + $split = array_map('trim', $split); + + // remove empty categories + $split = array_filter($split, 'strlen'); + + // rebuild the name from the array of cleaned categories + $actionName = implode($actionCategoryDelimiter, $split); + } + $url = self::cleanupString($url); + + if (!Piwik_Common::isLookLikeUrl($url)) { + printDebug("WARNING: URL looks invalid and is discarded"); + $url = ''; + } + + // Site search? + if ($actionType == self::TYPE_ACTION_URL) { + // Look in tracked URL for the Site Search parameters + $siteSearch = $this->detectSiteSearch($url); + if (!empty($siteSearch)) { + $actionType = self::TYPE_SITE_SEARCH; + list($actionName, $url) = $siteSearch; + } + // Look for performance analytics parameters + $this->detectPerformanceAnalyticsParameters(); + } + $actionName = self::cleanupString($actionName); + + return array( + 'name' => empty($actionName) ? '' : $actionName, + 'type' => $actionType, + 'url' => $url, + ); + } + + protected function detectSiteSearch($originalUrl) + { + $website = Piwik_Tracker_Cache::getCacheWebsiteAttributes($this->idSite); + if (empty($website['sitesearch'])) { + printDebug("Internal 'Site Search' tracking is not enabled for this site. "); + return false; + } + $actionName = $url = $categoryName = $count = false; + $doTrackUrlForSiteSearch = !empty(Piwik_Config::getInstance()->Tracker['action_sitesearch_record_url']); + + $originalUrl = self::cleanupUrl($originalUrl); + + + // Detect Site search from Tracking API parameters rather than URL + $searchKwd = Piwik_Common::getRequestVar(self::PARAMETER_NAME_SEARCH_KEYWORD, '', 'string', $this->request); + if (!empty($searchKwd)) { + $actionName = $searchKwd; + if ($doTrackUrlForSiteSearch) { + $url = $originalUrl; + } + $isCategoryName = Piwik_Common::getRequestVar(self::PARAMETER_NAME_SEARCH_CATEGORY, false, 'string', $this->request); + if (!empty($isCategoryName)) { + $categoryName = $isCategoryName; + } + $isCount = Piwik_Common::getRequestVar(self::PARAMETER_NAME_SEARCH_COUNT, -1, 'int', $this->request); + if ($this->isValidSearchCount($isCount)) { + $count = $isCount; + } + } + + if (empty($actionName)) { + $parsedUrl = @parse_url($originalUrl); + + // Detect Site Search from URL query parameters + if (!empty($parsedUrl['query']) || !empty($parsedUrl['fragment'])) { + // array($url, $actionName, $categoryName, $count); + $searchInfo = $this->detectSiteSearchFromUrl($website, $parsedUrl); + if (!empty($searchInfo)) { + list ($url, $actionName, $categoryName, $count) = $searchInfo; + } + } + } + + if (empty($actionName)) { + printDebug("(this is not a Site Search request)"); + return false; + } + + printDebug("Detected Site Search keyword '$actionName'. "); + if (!empty($categoryName)) { + printDebug("- Detected Site Search Category '$categoryName'. "); + } + if ($count !== false) { + printDebug("- Search Results Count was '$count'. "); + } + if ($url != $originalUrl) { + printDebug("NOTE: The Page URL was changed / removed, during the Site Search detection, was '$originalUrl', now is '$url'"); + } + + if (!empty($categoryName) || $count !== false) { + $this->setActionSearchMetadata($categoryName, $count); + } + return array( + $actionName, + $url + ); + } + + protected function isValidSearchCount($count) + { + return is_numeric($count) && $count >= 0; + } + + + protected function setActionSearchMetadata($category, $count) + { + if (!empty($category)) { + $this->searchCategory = trim($category); + } + if ($count !== false) { + $this->searchCount = $count; + } + } + + protected function detectSiteSearchFromUrl($website, $parsedUrl) + { + $doRemoveSearchParametersFromUrl = false; + $separator = '&'; + $count = $actionName = $categoryName = false; + + $keywordParameters = isset($website['sitesearch_keyword_parameters']) + ? $website['sitesearch_keyword_parameters'] + : array(); + $queryString = (!empty($parsedUrl['query']) ? $parsedUrl['query'] : '') . (!empty($parsedUrl['fragment']) ? $separator . $parsedUrl['fragment'] : ''); + $parametersRaw = Piwik_Common::getArrayFromQueryString($queryString); + + // strtolower the parameter names for smooth site search detection + $parameters = array(); + foreach ($parametersRaw as $k => $v) { + $parameters[Piwik_Common::mb_strtolower($k)] = $v; + } + // decode values if they were sent from a client using another charset + self::reencodeParameters($parameters, $this->pageEncoding); + + // Detect Site Search keyword + foreach ($keywordParameters as $keywordParameterRaw) { + $keywordParameter = Piwik_Common::mb_strtolower($keywordParameterRaw); + if (!empty($parameters[$keywordParameter])) { + $actionName = $parameters[$keywordParameter]; + break; + } + } + + if (empty($actionName)) { + return false; + } + + $categoryParameters = isset($website['sitesearch_category_parameters']) + ? $website['sitesearch_category_parameters'] + : array(); + + foreach ($categoryParameters as $categoryParameterRaw) { + $categoryParameter = Piwik_Common::mb_strtolower($categoryParameterRaw); + if (!empty($parameters[$categoryParameter])) { + $categoryName = $parameters[$categoryParameter]; + break; + } + } + + if (isset($parameters[self::PARAMETER_NAME_SEARCH_COUNT]) + && $this->isValidSearchCount($parameters[self::PARAMETER_NAME_SEARCH_COUNT]) + ) { + $count = $parameters[self::PARAMETER_NAME_SEARCH_COUNT]; + } + // Remove search kwd from URL + if ($doRemoveSearchParametersFromUrl) { + // @see excludeQueryParametersFromUrl() + // Excluded the detected parameters from the URL + $parametersToExclude = array($categoryParameterRaw, $keywordParameterRaw); + $parsedUrl['query'] = self::getQueryStringWithExcludedParameters(Piwik_Common::getArrayFromQueryString($parsedUrl['query']), $parametersToExclude); + $parsedUrl['fragment'] = self::getQueryStringWithExcludedParameters(Piwik_Common::getArrayFromQueryString($parsedUrl['fragment']), $parametersToExclude); + } + $url = Piwik_Common::getParseUrlReverse($parsedUrl); + if (is_array($actionName)) { + $actionName = reset($actionName); + } + $actionName = trim(urldecode($actionName)); + if (empty($actionName)) { + return false; + } + if (is_array($categoryName)) { + $categoryName = reset($categoryName); + } + $categoryName = trim(urldecode($categoryName)); + return array($url, $actionName, $categoryName, $count); + } + + protected function detectPerformanceAnalyticsParameters() + { + $generationTime = Piwik_Common::getRequestVar(self::PARAMETER_NAME_TIME_GENERATION, -1, 'int', $this->request); + if ($generationTime > 0) { + $this->timeGeneration = $generationTime; + } + } + + /** + * Clean up string contents (filter, truncate, ...) + * + * @param string $string Dirty string + * @return string + */ + protected static function cleanupString($string) + { + $string = trim($string); + $string = str_replace(array("\n", "\r", "\0"), '', $string); + + $limit = Piwik_Config::getInstance()->Tracker['page_maximum_length']; + return substr($string, 0, $limit); + } + + /** + * Checks if query parameters are of a non-UTF-8 encoding and converts the values + * from the specified encoding to UTF-8. + * + * This method is used to workaround browser/webapp bugs (see #3450). When + * browsers fail to encode query parameters in UTF-8, the tracker will send the + * charset of the page viewed and we can sometimes work around invalid data + * being stored. + * + * @param array $queryParameters Name/value mapping of query parameters. + * @param string|false $encoding of the HTML page the URL is for. Used to workaround + * browser bugs & mis-coded webapps. See #3450. + */ + private static function reencodeParameters(&$queryParameters, $encoding = false) + { + // if query params are encoded w/ non-utf8 characters (due to browser bug or whatever), + // encode to UTF-8. + if ($encoding !== false + && strtolower($encoding) != 'utf-8' + && function_exists('mb_check_encoding') + ) { + $queryParameters = self::reencodeParametersArray($queryParameters, $encoding); + } + return $queryParameters; + } + + private static function reencodeParametersArray($queryParameters, $encoding) + { + foreach ($queryParameters as &$value) { + if (is_array($value)) { + $value = self::reencodeParametersArray($value, $encoding); + } else { + $value = self::reencodeParameterValue($value, $encoding); + } + } + return $queryParameters; + } + + private static function reencodeParameterValue($value, $encoding) + { + if (is_string($value)) { + $decoded = urldecode($value); + if (@mb_check_encoding($decoded, $encoding)) { + $value = urlencode(mb_convert_encoding($decoded, 'UTF-8', $encoding)); + } + } + return $value; + } } diff --git a/core/Tracker/Cache.php b/core/Tracker/Cache.php index 49526a6bb8..b537cc31a8 100644 --- a/core/Tracker/Cache.php +++ b/core/Tracker/Cache.php @@ -17,141 +17,138 @@ */ class Piwik_Tracker_Cache { - /** - * Public for tests only - * @var Piwik_CacheFile - */ - static public $trackerCache = null; - - static protected function getInstance() - { - if(is_null(self::$trackerCache)) { - $ttl = Piwik_Config::getInstance()->Tracker['tracker_cache_file_ttl']; - self::$trackerCache = new Piwik_CacheFile('tracker', $ttl); - } - return self::$trackerCache; - } - - /** - * Returns array containing data about the website: goals, URLs, etc. - * - * @param int $idSite - * @return array - */ - static function getCacheWebsiteAttributes( $idSite ) - { - $idSite = (int)$idSite; - - $cache = self::getInstance(); - if(($cacheContent = $cache->get($idSite)) !== false) - { - return $cacheContent; - } - - Piwik_Tracker::initCorePiwikInTrackerMode(); - - // save current user privilege and temporarily assume super user privilege - $isSuperUser = Piwik::isUserIsSuperUser(); - Piwik::setUserIsSuperUser(); - - $content = array(); - Piwik_PostEvent('Common.fetchWebsiteAttributes', $content, $idSite); - - // restore original 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)) - { - $cache->set($idSite, $content); - } - return $content; - } - - /** - * Clear general (global) cache - */ - static public function clearCacheGeneral() - { - self::getInstance()->delete('general'); - } - - /** - * Returns contents of general (global) cache. - * If the cache file tmp/cache/tracker/general.php does not exist yet, create it - * - * @return array - */ - static public function getCacheGeneral() - { - $cache = self::getInstance(); - $cacheId = 'general'; - $expectedRows = 3; - if(($cacheContent = $cache->get($cacheId)) !== false - && count($cacheContent) == $expectedRows) - { - return $cacheContent; - } - - Piwik_Tracker::initCorePiwikInTrackerMode(); - $cacheContent = array ( - 'isBrowserTriggerArchivingEnabled' => Piwik_ArchiveProcessing::isBrowserTriggerArchivingEnabled(), - 'lastTrackerCronRun' => Piwik_GetOption('lastTrackerCronRun'), - 'currentLocationProviderId' => Piwik_UserCountry_LocationProvider::getCurrentProviderId(), - ); - self::setCacheGeneral( $cacheContent ); - return $cacheContent; - } - - /** - * Store data in general (global cache) - * - * @param mixed $value - * @return bool - */ - static public function setCacheGeneral($value) - { - $cache = self::getInstance(); - $cacheId = 'general'; - $cache->set($cacheId, $value); - return true; - } - - /** - * Regenerate Tracker cache files - * - * @param array $idSites Array of idSites to clear cache for - */ - static public function regenerateCacheWebsiteAttributes($idSites = array()) - { - if(!is_array($idSites)) - { - $idSites = array( $idSites ); - } - foreach($idSites as $idSite) { - self::deleteCacheWebsiteAttributes($idSite); - self::getCacheWebsiteAttributes($idSite); - } - } - - /** - * Delete existing Tracker cache - * - * @param string $idSite (website ID of the site to clear cache for - */ - static public function deleteCacheWebsiteAttributes( $idSite ) - { - $idSite = (int)$idSite; - self::getInstance()->delete($idSite); - } - - /** - * Deletes all Tracker cache files - */ - static public function deleteTrackerCache() - { - self::getInstance()->deleteAll(); - } + /** + * Public for tests only + * @var Piwik_CacheFile + */ + static public $trackerCache = null; + + static protected function getInstance() + { + if (is_null(self::$trackerCache)) { + $ttl = Piwik_Config::getInstance()->Tracker['tracker_cache_file_ttl']; + self::$trackerCache = new Piwik_CacheFile('tracker', $ttl); + } + return self::$trackerCache; + } + + /** + * Returns array containing data about the website: goals, URLs, etc. + * + * @param int $idSite + * @return array + */ + static function getCacheWebsiteAttributes($idSite) + { + $idSite = (int)$idSite; + + $cache = self::getInstance(); + if (($cacheContent = $cache->get($idSite)) !== false) { + return $cacheContent; + } + + Piwik_Tracker::initCorePiwikInTrackerMode(); + + // save current user privilege and temporarily assume super user privilege + $isSuperUser = Piwik::isUserIsSuperUser(); + Piwik::setUserIsSuperUser(); + + $content = array(); + Piwik_PostEvent('Common.fetchWebsiteAttributes', $content, $idSite); + + // restore original 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)) { + $cache->set($idSite, $content); + } + return $content; + } + + /** + * Clear general (global) cache + */ + static public function clearCacheGeneral() + { + self::getInstance()->delete('general'); + } + + /** + * Returns contents of general (global) cache. + * If the cache file tmp/cache/tracker/general.php does not exist yet, create it + * + * @return array + */ + static public function getCacheGeneral() + { + $cache = self::getInstance(); + $cacheId = 'general'; + $expectedRows = 3; + if (($cacheContent = $cache->get($cacheId)) !== false + && count($cacheContent) == $expectedRows + ) { + return $cacheContent; + } + + Piwik_Tracker::initCorePiwikInTrackerMode(); + $cacheContent = array( + 'isBrowserTriggerArchivingEnabled' => Piwik_ArchiveProcessing::isBrowserTriggerArchivingEnabled(), + 'lastTrackerCronRun' => Piwik_GetOption('lastTrackerCronRun'), + 'currentLocationProviderId' => Piwik_UserCountry_LocationProvider::getCurrentProviderId(), + ); + self::setCacheGeneral($cacheContent); + return $cacheContent; + } + + /** + * Store data in general (global cache) + * + * @param mixed $value + * @return bool + */ + static public function setCacheGeneral($value) + { + $cache = self::getInstance(); + $cacheId = 'general'; + $cache->set($cacheId, $value); + return true; + } + + /** + * Regenerate Tracker cache files + * + * @param array $idSites Array of idSites to clear cache for + */ + static public function regenerateCacheWebsiteAttributes($idSites = array()) + { + if (!is_array($idSites)) { + $idSites = array($idSites); + } + foreach ($idSites as $idSite) { + self::deleteCacheWebsiteAttributes($idSite); + self::getCacheWebsiteAttributes($idSite); + } + } + + /** + * Delete existing Tracker cache + * + * @param string $idSite (website ID of the site to clear cache for + */ + static public function deleteCacheWebsiteAttributes($idSite) + { + $idSite = (int)$idSite; + self::getInstance()->delete($idSite); + } + + /** + * Deletes all Tracker cache files + */ + static public function deleteTrackerCache() + { + self::getInstance()->deleteAll(); + } }
\ No newline at end of file diff --git a/core/Tracker/Config.php b/core/Tracker/Config.php index 90fe0e1f45..ed9bb9fb2c 100644 --- a/core/Tracker/Config.php +++ b/core/Tracker/Config.php @@ -14,7 +14,7 @@ * DO NOT USE * * Use this notation to fetch a config file value: - * Piwik_Config::getInstance()->General['enable_browser_archiving_triggering'] + * Piwik_Config::getInstance()->General['enable_browser_archiving_triggering'] * * @todo remove this in 2.0 * @since 1.7 @@ -25,13 +25,13 @@ */ class Piwik_Tracker_Config { - /** - * Returns the singleton Piwik_Config - * - * @return Piwik_Config - */ - static public function getInstance() - { - return Piwik_Config::getInstance(); - } + /** + * Returns the singleton Piwik_Config + * + * @return Piwik_Config + */ + static public function getInstance() + { + return Piwik_Config::getInstance(); + } } diff --git a/core/Tracker/Db.php b/core/Tracker/Db.php index b00ce82585..acccd4e858 100644 --- a/core/Tracker/Db.php +++ b/core/Tracker/Db.php @@ -12,213 +12,211 @@ /** * Simple database wrapper. * We can't afford to have a dependency with the Zend_Db module in Tracker. - * We wrote this simple class + * We wrote this simple class * * @package Piwik * @subpackage Piwik_Tracker */ abstract class Piwik_Tracker_Db { - protected static $profiling = false; - - protected $queriesProfiling = array(); - - protected $connection = null; - - /** - * Enables the SQL profiling. - * For each query, saves in the DB the time spent on this query. - * Very useful to see the slow query under heavy load. - * You can then use Piwik::printSqlProfilingReportTracker(); - * to display the SQLProfiling report and see which queries take time, etc. - */ - public static function enableProfiling() - { - self::$profiling = true; - } - - /** - * Disables the SQL profiling logging. - */ - public static function disableProfiling() - { - self::$profiling = false; - } - - /** - * Returns true if the SQL profiler is enabled - * Only used by the unit test that tests that the profiler is off on a production server - * - * @return bool - */ - public static function isProfilingEnabled() - { - return self::$profiling; - } - - /** - * Initialize profiler - * - * @return Piwik_Timer - */ - protected function initProfiler() - { - return new Piwik_Timer; - } - - /** - * Record query profile - * - * @param string $query - * @param Piwik_Timer $timer - */ - protected function recordQueryProfile( $query, $timer ) - { - if(!isset($this->queriesProfiling[$query])) $this->queriesProfiling[$query] = array('sum_time_ms' => 0, 'count' => 0); - $time = $timer->getTimeMs(2); - $time += $this->queriesProfiling[$query]['sum_time_ms']; - $count = $this->queriesProfiling[$query]['count'] + 1; - $this->queriesProfiling[$query] = array('sum_time_ms' => $time, 'count' => $count); - } - - /** - * When destroyed, if SQL profiled enabled, logs the SQL profiling information - */ - public function recordProfiling() - { - if(is_null($this->connection)) - { - return; - } - - // turn off the profiler so we don't profile the following queries - self::$profiling = false; - - foreach($this->queriesProfiling as $query => $info) - { - $time = $info['sum_time_ms']; - $count = $info['count']; - - $queryProfiling = "INSERT INTO ".Piwik_Common::prefixTable('log_profiling')." + protected static $profiling = false; + + protected $queriesProfiling = array(); + + protected $connection = null; + + /** + * Enables the SQL profiling. + * For each query, saves in the DB the time spent on this query. + * Very useful to see the slow query under heavy load. + * You can then use Piwik::printSqlProfilingReportTracker(); + * to display the SQLProfiling report and see which queries take time, etc. + */ + public static function enableProfiling() + { + self::$profiling = true; + } + + /** + * Disables the SQL profiling logging. + */ + public static function disableProfiling() + { + self::$profiling = false; + } + + /** + * Returns true if the SQL profiler is enabled + * Only used by the unit test that tests that the profiler is off on a production server + * + * @return bool + */ + public static function isProfilingEnabled() + { + return self::$profiling; + } + + /** + * Initialize profiler + * + * @return Piwik_Timer + */ + protected function initProfiler() + { + return new Piwik_Timer; + } + + /** + * Record query profile + * + * @param string $query + * @param Piwik_Timer $timer + */ + protected function recordQueryProfile($query, $timer) + { + if (!isset($this->queriesProfiling[$query])) $this->queriesProfiling[$query] = array('sum_time_ms' => 0, 'count' => 0); + $time = $timer->getTimeMs(2); + $time += $this->queriesProfiling[$query]['sum_time_ms']; + $count = $this->queriesProfiling[$query]['count'] + 1; + $this->queriesProfiling[$query] = array('sum_time_ms' => $time, 'count' => $count); + } + + /** + * When destroyed, if SQL profiled enabled, logs the SQL profiling information + */ + public function recordProfiling() + { + if (is_null($this->connection)) { + return; + } + + // turn off the profiler so we don't profile the following queries + self::$profiling = false; + + foreach ($this->queriesProfiling as $query => $info) { + $time = $info['sum_time_ms']; + $count = $info['count']; + + $queryProfiling = "INSERT INTO " . Piwik_Common::prefixTable('log_profiling') . " (query,count,sum_time_ms) VALUES (?,$count,$time) ON DUPLICATE KEY UPDATE count=count+$count,sum_time_ms=sum_time_ms+$time"; - $this->query($queryProfiling,array($query)); - } - - // turn back on profiling - self::$profiling = true; - } - - /** - * Connects to the DB - * - * @throws Piwik_Tracker_Db_Exception if there was an error connecting the DB - */ - abstract public function connect(); - - /** - * Disconnects from the server - */ - public function disconnect() - { - $this->connection = null; - } - - /** - * Returns an array containing all the rows of a query result, using optional bound parameters. - * - * @param string $query Query - * @param array $parameters Parameters to bind - * @see query() - * @throws Piwik_Tracker_Db_Exception if an exception occurred - */ - abstract public function fetchAll( $query, $parameters = array() ); - - /** - * Returns the first row of a query result, using optional bound parameters. - * - * @param string $query Query - * @param array $parameters Parameters to bind - * @see also query() - * - * @throws Piwik_Tracker_Db_Exception if an exception occurred - */ - abstract public function fetch( $query, $parameters = array() ); - - /** - * This function is a proxy to fetch(), used to maintain compatibility with Zend_Db interface - * - * @see fetch() - * @param string $query Query - * @param array $parameters Parameters to bind - * @return - */ - public function fetchRow( $query, $parameters = array() ) - { - return $this->fetch($query, $parameters); - } - - /** - * This function is a proxy to fetch(), used to maintain compatibility with Zend_Db interface - * - * @see fetch() - * @param string $query Query - * @param array $parameters Parameters to bind - * @return bool|mixed - */ - 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() - * @param string $query Query - * @param array $parameters Parameters to bind - * @return - */ - 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() - * @return int - */ - abstract public function rowCount($queryResult); - - /** - * Executes a query, using optional bound parameters. - * - * @param string $query Query - * @param array $parameters Parameters to bind array('idsite'=> 1) - * - * @return PDOStatement or false if failed - * @throws Piwik_Tracker_Db_Exception if an exception occurred - */ - abstract public function query($query, $parameters = array()); - - /** - * Returns the last inserted ID in the DB - * Wrapper of PDO::lastInsertId() - * - * @return int - */ - abstract public function lastInsertId(); - - /** - * Test error number - * - * @param Exception $e - * @param string $errno - * @return bool True if error number matches; false otherwise - */ - abstract public function isErrNo($e, $errno); + $this->query($queryProfiling, array($query)); + } + + // turn back on profiling + self::$profiling = true; + } + + /** + * Connects to the DB + * + * @throws Piwik_Tracker_Db_Exception if there was an error connecting the DB + */ + abstract public function connect(); + + /** + * Disconnects from the server + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * Returns an array containing all the rows of a query result, using optional bound parameters. + * + * @param string $query Query + * @param array $parameters Parameters to bind + * @see query() + * @throws Piwik_Tracker_Db_Exception if an exception occurred + */ + abstract public function fetchAll($query, $parameters = array()); + + /** + * Returns the first row of a query result, using optional bound parameters. + * + * @param string $query Query + * @param array $parameters Parameters to bind + * @see also query() + * + * @throws Piwik_Tracker_Db_Exception if an exception occurred + */ + abstract public function fetch($query, $parameters = array()); + + /** + * This function is a proxy to fetch(), used to maintain compatibility with Zend_Db interface + * + * @see fetch() + * @param string $query Query + * @param array $parameters Parameters to bind + * @return + */ + public function fetchRow($query, $parameters = array()) + { + return $this->fetch($query, $parameters); + } + + /** + * This function is a proxy to fetch(), used to maintain compatibility with Zend_Db interface + * + * @see fetch() + * @param string $query Query + * @param array $parameters Parameters to bind + * @return bool|mixed + */ + 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() + * @param string $query Query + * @param array $parameters Parameters to bind + * @return + */ + 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() + * @return int + */ + abstract public function rowCount($queryResult); + + /** + * Executes a query, using optional bound parameters. + * + * @param string $query Query + * @param array $parameters Parameters to bind array('idsite'=> 1) + * + * @return PDOStatement or false if failed + * @throws Piwik_Tracker_Db_Exception if an exception occurred + */ + abstract public function query($query, $parameters = array()); + + /** + * Returns the last inserted ID in the DB + * Wrapper of PDO::lastInsertId() + * + * @return int + */ + abstract public function lastInsertId(); + + /** + * Test error number + * + * @param Exception $e + * @param string $errno + * @return bool True if error number matches; false otherwise + */ + abstract public function isErrNo($e, $errno); }
\ No newline at end of file diff --git a/core/Tracker/Db/Exception.php b/core/Tracker/Db/Exception.php index 7b04400d6f..3958a1e008 100644 --- a/core/Tracker/Db/Exception.php +++ b/core/Tracker/Db/Exception.php @@ -15,5 +15,6 @@ * @package Piwik * @subpackage Piwik_Tracker */ -class Piwik_Tracker_Db_Exception extends Exception { +class Piwik_Tracker_Db_Exception extends Exception +{ } diff --git a/core/Tracker/Db/Mysqli.php b/core/Tracker/Db/Mysqli.php index e9d4414253..193bdff203 100644 --- a/core/Tracker/Db/Mysqli.php +++ b/core/Tracker/Db/Mysqli.php @@ -1,7 +1,7 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @@ -17,281 +17,260 @@ */ class Piwik_Tracker_Db_Mysqli extends Piwik_Tracker_Db { - protected $connection = null; - protected $host; - protected $port; - protected $socket; - protected $dbname; - protected $username; - protected $password; - protected $charset; + protected $connection = null; + protected $host; + protected $port; + protected $socket; + protected $dbname; + protected $username; + protected $password; + protected $charset; + + /** + * Builds the DB object + * + * @param array $dbInfo + * @param string $driverName + */ + public function __construct($dbInfo, $driverName = 'mysql') + { + if (isset($dbInfo['unix_socket']) && $dbInfo['unix_socket'][0] == '/') { + $this->host = null; + $this->port = null; + $this->socket = $dbInfo['unix_socket']; + } else if ($dbInfo['port'][0] == '/') { + $this->host = null; + $this->port = null; + $this->socket = $dbInfo['port']; + } else { + $this->host = $dbInfo['host']; + $this->port = $dbInfo['port']; + $this->socket = null; + } + $this->dbname = $dbInfo['dbname']; + $this->username = $dbInfo['username']; + $this->password = $dbInfo['password']; + $this->charset = isset($dbInfo['charset']) ? $dbInfo['charset'] : null; + } + + /** + * Destructor + */ + public function __destruct() + { + $this->connection = null; + } + + /** + * Connects to the DB + * + * @throws Exception|Piwik_Tracker_Db_Exception if there was an error connecting the DB + */ + public function connect() + { + if (self::$profiling) { + $timer = $this->initProfiler(); + } + + $this->connection = mysqli_connect($this->host, $this->username, $this->password, $this->dbname, $this->port, $this->socket); + if (!$this->connection || mysqli_connect_errno()) { + throw new Piwik_Tracker_Db_Exception("Connect failed: " . mysqli_connect_error()); + } + + if ($this->charset && !mysqli_set_charset($this->connection, $this->charset)) { + throw new Piwik_Tracker_Db_Exception("Set Charset failed: " . mysqli_error($this->connection)); + } - /** - * Builds the DB object - * - * @param array $dbInfo - * @param string $driverName - */ - public function __construct( $dbInfo, $driverName = 'mysql') - { - if(isset($dbInfo['unix_socket']) && $dbInfo['unix_socket'][0] == '/') - { - $this->host = null; - $this->port = null; - $this->socket = $dbInfo['unix_socket']; - } - else if ($dbInfo['port'][0] == '/') - { - $this->host = null; - $this->port = null; - $this->socket = $dbInfo['port']; - } - else - { - $this->host = $dbInfo['host']; - $this->port = $dbInfo['port']; - $this->socket = null; - } - $this->dbname = $dbInfo['dbname']; - $this->username = $dbInfo['username']; - $this->password = $dbInfo['password']; - $this->charset = isset($dbInfo['charset']) ? $dbInfo['charset'] : null; - } + $this->password = ''; - /** - * Destructor - */ - public function __destruct() - { - $this->connection = null; - } + if (self::$profiling) { + $this->recordQueryProfile('connect', $timer); + } + } - /** - * Connects to the DB - * - * @throws Exception|Piwik_Tracker_Db_Exception if there was an error connecting the DB - */ - public function connect() - { - if(self::$profiling) - { - $timer = $this->initProfiler(); - } - - $this->connection = mysqli_connect($this->host, $this->username, $this->password, $this->dbname, $this->port, $this->socket); - if(!$this->connection || mysqli_connect_errno()) - { - throw new Piwik_Tracker_Db_Exception("Connect failed: " . mysqli_connect_error()); - } + /** + * Disconnects from the server + */ + public function disconnect() + { + mysqli_close($this->connection); + $this->connection = null; + } - if($this->charset && !mysqli_set_charset($this->connection, $this->charset)) - { - throw new Piwik_Tracker_Db_Exception("Set Charset failed: " . mysqli_error($this->connection)); - } + /** + * Returns an array containing all the rows of a query result, using optional bound parameters. + * + * @see query() + * + * @param string $query Query + * @param array $parameters Parameters to bind + * @return array + * @throws Exception|Piwik_Tracker_Db_Exception if an exception occured + */ + public function fetchAll($query, $parameters = array()) + { + try { + if (self::$profiling) { + $timer = $this->initProfiler(); + } - $this->password = ''; - - if(self::$profiling) - { - $this->recordQueryProfile('connect', $timer); - } - } - - /** - * Disconnects from the server - */ - public function disconnect() - { - mysqli_close($this->connection); - $this->connection = null; - } - - /** - * Returns an array containing all the rows of a query result, using optional bound parameters. - * - * @see query() - * - * @param string $query Query - * @param array $parameters Parameters to bind - * @return array - * @throws Exception|Piwik_Tracker_Db_Exception if an exception occured - */ - public function fetchAll( $query, $parameters = array() ) - { - try { - if(self::$profiling) - { - $timer = $this->initProfiler(); - } + $rows = array(); + $query = $this->prepare($query, $parameters); + $rs = mysqli_query($this->connection, $query); + if (is_bool($rs)) { + throw new Piwik_Tracker_Db_Exception('fetchAll() failed: ' . mysqli_error($this->connection) . ' : ' . $query); + } - $rows = array(); - $query = $this->prepare( $query, $parameters ); - $rs = mysqli_query($this->connection, $query); - if(is_bool($rs)) - { - throw new Piwik_Tracker_Db_Exception('fetchAll() failed: ' . mysqli_error($this->connection) . ' : ' . $query); - } + while ($row = mysqli_fetch_array($rs, MYSQLI_ASSOC)) { + $rows[] = $row; + } + mysqli_free_result($rs); - while($row = mysqli_fetch_array($rs, MYSQLI_ASSOC)) - { - $rows[] = $row; - } - mysqli_free_result($rs); + if (self::$profiling) { + $this->recordQueryProfile($query, $timer); + } + return $rows; + } catch (Exception $e) { + throw new Piwik_Tracker_Db_Exception("Error query: " . $e->getMessage()); + } + } - if(self::$profiling) - { - $this->recordQueryProfile($query, $timer); - } - return $rows; - } catch (Exception $e) { - throw new Piwik_Tracker_Db_Exception("Error query: ".$e->getMessage()); - } - } - - /** - * Returns the first row of a query result, using optional bound parameters. - * - * @see query() - * - * @param string $query Query - * @param array $parameters Parameters to bind - * @throws Piwik_Tracker_Db_Exception if an exception occurred - */ - public function fetch( $query, $parameters = array() ) - { - try { - if(self::$profiling) - { - $timer = $this->initProfiler(); - } + /** + * Returns the first row of a query result, using optional bound parameters. + * + * @see query() + * + * @param string $query Query + * @param array $parameters Parameters to bind + * @throws Piwik_Tracker_Db_Exception if an exception occurred + */ + public function fetch($query, $parameters = array()) + { + try { + if (self::$profiling) { + $timer = $this->initProfiler(); + } - $query = $this->prepare( $query, $parameters ); - $rs = mysqli_query($this->connection, $query); - if(is_bool($rs)) - { - throw new Piwik_Tracker_Db_Exception('fetch() failed: ' . mysqli_error($this->connection) . ' : ' . $query); - } + $query = $this->prepare($query, $parameters); + $rs = mysqli_query($this->connection, $query); + if (is_bool($rs)) { + throw new Piwik_Tracker_Db_Exception('fetch() failed: ' . mysqli_error($this->connection) . ' : ' . $query); + } - $row = mysqli_fetch_array($rs, MYSQLI_ASSOC); - mysqli_free_result($rs); + $row = mysqli_fetch_array($rs, MYSQLI_ASSOC); + mysqli_free_result($rs); - if(self::$profiling) - { - $this->recordQueryProfile($query, $timer); - } - return $row; - } catch (Exception $e) { - throw new Piwik_Tracker_Db_Exception("Error query: ".$e->getMessage()); - } - } - - /** - * Executes a query, using optional bound parameters. - * - * @param string $query Query - * @param array|string $parameters Parameters to bind array('idsite'=> 1) - * - * @return bool|resource false if failed - * @throws Piwik_Tracker_Db_Exception if an exception occurred - */ - public function query($query, $parameters = array()) - { - if(is_null($this->connection)) - { - return false; - } + if (self::$profiling) { + $this->recordQueryProfile($query, $timer); + } + return $row; + } catch (Exception $e) { + throw new Piwik_Tracker_Db_Exception("Error query: " . $e->getMessage()); + } + } - try { - if(self::$profiling) - { - $timer = $this->initProfiler(); - } - - $query = $this->prepare( $query, $parameters ); - $result = mysqli_query($this->connection, $query); - if(!is_bool($result)) - { - mysqli_free_result($result); - } - - if(self::$profiling) - { - $this->recordQueryProfile($query, $timer); - } - return $result; - } catch (Exception $e) { - throw new Piwik_Tracker_Db_Exception("Error query: ".$e->getMessage() . " + /** + * Executes a query, using optional bound parameters. + * + * @param string $query Query + * @param array|string $parameters Parameters to bind array('idsite'=> 1) + * + * @return bool|resource false if failed + * @throws Piwik_Tracker_Db_Exception if an exception occurred + */ + public function query($query, $parameters = array()) + { + if (is_null($this->connection)) { + return false; + } + + try { + if (self::$profiling) { + $timer = $this->initProfiler(); + } + + $query = $this->prepare($query, $parameters); + $result = mysqli_query($this->connection, $query); + if (!is_bool($result)) { + mysqli_free_result($result); + } + + if (self::$profiling) { + $this->recordQueryProfile($query, $timer); + } + return $result; + } catch (Exception $e) { + throw new Piwik_Tracker_Db_Exception("Error query: " . $e->getMessage() . " In query: $query - Parameters: ".var_export($parameters, true)); - } - } + Parameters: " . var_export($parameters, true)); + } + } + + /** + * Returns the last inserted ID in the DB + * + * @return int + */ + public function lastInsertId() + { + return mysqli_insert_id($this->connection); + } + + /** + * Input is a prepared SQL statement and parameters + * Returns the SQL statement + * + * @param string $query + * @param array $parameters + * @return string + */ + private function prepare($query, $parameters) + { + if (!$parameters) { + $parameters = array(); + } else if (!is_array($parameters)) { + $parameters = array($parameters); + } + + $this->paramNb = 0; + $this->params = & $parameters; + $query = preg_replace_callback('/\?/', array($this, 'replaceParam'), $query); - /** - * Returns the last inserted ID in the DB - * - * @return int - */ - public function lastInsertId() - { - return mysqli_insert_id($this->connection); - } + return $query; + } - /** - * Input is a prepared SQL statement and parameters - * Returns the SQL statement - * - * @param string $query - * @param array $parameters - * @return string - */ - private function prepare($query, $parameters) { - if(!$parameters) - { - $parameters = array(); - } - else if(!is_array($parameters)) - { - $parameters = array( $parameters ); - } + public function replaceParam($match) + { + $param = & $this->params[$this->paramNb]; + $this->paramNb++; - $this->paramNb = 0; - $this->params = &$parameters; - $query = preg_replace_callback('/\?/', array($this, 'replaceParam'), $query); - - return $query; - } - - public function replaceParam($match) { - $param = &$this->params[$this->paramNb]; - $this->paramNb++; - - if ($param === null) { - return 'NULL'; - } else { - return "'".addslashes($param)."'"; - } - } + if ($param === null) { + return 'NULL'; + } else { + return "'" . addslashes($param) . "'"; + } + } - /** - * Test error number - * - * @param Exception $e - * @param string $errno - * @return bool - */ - public function isErrNo($e, $errno) - { - return mysqli_errno($this->_connection) == $errno; - } + /** + * Test error number + * + * @param Exception $e + * @param string $errno + * @return bool + */ + public function isErrNo($e, $errno) + { + return mysqli_errno($this->_connection) == $errno; + } - /** - * Return number of affected rows in last query - * - * @param mixed $queryResult Result from query() - * @return int - */ - public function rowCount($queryResult) - { - return mysqli_affected_rows($this->connection); - } + /** + * Return number of affected rows in last query + * + * @param mixed $queryResult Result from query() + * @return int + */ + public function rowCount($queryResult) + { + return mysqli_affected_rows($this->connection); + } } diff --git a/core/Tracker/Db/Pdo/Mysql.php b/core/Tracker/Db/Pdo/Mysql.php index 82a390ec28..23f87d4d85 100644 --- a/core/Tracker/Db/Pdo/Mysql.php +++ b/core/Tracker/Db/Pdo/Mysql.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -17,207 +17,192 @@ */ class Piwik_Tracker_Db_Pdo_Mysql extends Piwik_Tracker_Db { - protected $connection = null; - protected $dsn; - protected $username; - protected $password; - protected $charset; - - /** - * Builds the DB object - * - * @param array $dbInfo - * @param string $driverName - */ - public function __construct( $dbInfo, $driverName = 'mysql') - { - if(isset($dbInfo['unix_socket']) && $dbInfo['unix_socket'][0] == '/') - { - $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['unix_socket']; - } - else if ($dbInfo['port'][0] == '/') - { - $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['port']; - } - else - { - $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';host=' . $dbInfo['host'] . ';port=' . $dbInfo['port']; - } - $this->username = $dbInfo['username']; - $this->password = $dbInfo['password']; - $this->charset = isset($dbInfo['charset']) ? $dbInfo['charset'] : null; - } - - public function __destruct() - { - $this->connection = null; - } - - /** - * Connects to the DB - * - * @throws Exception if there was an error connecting the DB - */ - public function connect() - { - if(self::$profiling) - { - $timer = $this->initProfiler(); - } - - $this->connection = @new PDO($this->dsn, $this->username, $this->password, $config = array()); - $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - // we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked - // the piwik.php would stay waiting for the database... bad! - // we delete the password from this object "just in case" it could be printed - $this->password = ''; - - /* - * Lazy initialization via MYSQL_ATTR_INIT_COMMAND depends - * on mysqlnd support, PHP version, and OS. - * see ZF-7428 and http://bugs.php.net/bug.php?id=47224 - */ - if(!empty($this->charset)) - { - $sql = "SET NAMES '" . $this->charset . "'"; - $this->connection->exec($sql); - } - - if(self::$profiling) - { - $this->recordQueryProfile('connect', $timer); - } - } - - /** - * Disconnects from the server - */ - public function disconnect() - { - $this->connection = null; - } - - /** - * Returns an array containing all the rows of a query result, using optional bound parameters. - * - * @param string $query Query - * @param array $parameters Parameters to bind - * @return array|bool - * @see query() - * @throws Exception|Piwik_Tracker_Db_Exception if an exception occurred - */ - public function fetchAll( $query, $parameters = array() ) - { - try { - $sth = $this->query( $query, $parameters ); - if($sth === false) - { - return false; - } - return $sth->fetchAll(PDO::FETCH_ASSOC); - } catch (PDOException $e) { - throw new Piwik_Tracker_Db_Exception("Error query: ".$e->getMessage()); - } - } - - /** - * Returns the first row of a query result, using optional bound parameters. - * - * @param string $query Query - * @param array $parameters Parameters to bind - * @return bool|mixed - * @see query() - * @throws Exception|Piwik_Tracker_Db_Exception if an exception occurred - */ - public function fetch( $query, $parameters = array() ) - { - try { - $sth = $this->query( $query, $parameters ); - if($sth === false) - { - return false; - } - return $sth->fetch(PDO::FETCH_ASSOC); - } catch (PDOException $e) { - throw new Piwik_Tracker_Db_Exception("Error query: ".$e->getMessage()); - } - } - - /** - * Executes a query, using optional bound parameters. - * - * @param string $query Query - * @param array|string $parameters Parameters to bind array('idsite'=> 1) - * @return PDOStatement|bool PDOStatement or false if failed - * @throws Piwik_Tracker_Db_Exception if an exception occured - */ - public function query($query, $parameters = array()) - { - if(is_null($this->connection)) - { - return false; - } - - try { - if(self::$profiling) - { - $timer = $this->initProfiler(); - } - - if(!is_array($parameters)) - { - $parameters = array( $parameters ); - } - $sth = $this->connection->prepare($query); - $sth->execute( $parameters ); - - if(self::$profiling) - { - $this->recordQueryProfile($query, $timer); - } - return $sth; - } catch (PDOException $e) { - throw new Piwik_Tracker_Db_Exception("Error query: ".$e->getMessage() . " + protected $connection = null; + protected $dsn; + protected $username; + protected $password; + protected $charset; + + /** + * Builds the DB object + * + * @param array $dbInfo + * @param string $driverName + */ + public function __construct($dbInfo, $driverName = 'mysql') + { + if (isset($dbInfo['unix_socket']) && $dbInfo['unix_socket'][0] == '/') { + $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['unix_socket']; + } else if ($dbInfo['port'][0] == '/') { + $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';unix_socket=' . $dbInfo['port']; + } else { + $this->dsn = $driverName . ':dbname=' . $dbInfo['dbname'] . ';host=' . $dbInfo['host'] . ';port=' . $dbInfo['port']; + } + $this->username = $dbInfo['username']; + $this->password = $dbInfo['password']; + $this->charset = isset($dbInfo['charset']) ? $dbInfo['charset'] : null; + } + + public function __destruct() + { + $this->connection = null; + } + + /** + * Connects to the DB + * + * @throws Exception if there was an error connecting the DB + */ + public function connect() + { + if (self::$profiling) { + $timer = $this->initProfiler(); + } + + $this->connection = @new PDO($this->dsn, $this->username, $this->password, $config = array()); + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + // we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked + // the piwik.php would stay waiting for the database... bad! + // we delete the password from this object "just in case" it could be printed + $this->password = ''; + + /* + * Lazy initialization via MYSQL_ATTR_INIT_COMMAND depends + * on mysqlnd support, PHP version, and OS. + * see ZF-7428 and http://bugs.php.net/bug.php?id=47224 + */ + if (!empty($this->charset)) { + $sql = "SET NAMES '" . $this->charset . "'"; + $this->connection->exec($sql); + } + + if (self::$profiling) { + $this->recordQueryProfile('connect', $timer); + } + } + + /** + * Disconnects from the server + */ + public function disconnect() + { + $this->connection = null; + } + + /** + * Returns an array containing all the rows of a query result, using optional bound parameters. + * + * @param string $query Query + * @param array $parameters Parameters to bind + * @return array|bool + * @see query() + * @throws Exception|Piwik_Tracker_Db_Exception if an exception occurred + */ + public function fetchAll($query, $parameters = array()) + { + try { + $sth = $this->query($query, $parameters); + if ($sth === false) { + return false; + } + return $sth->fetchAll(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + throw new Piwik_Tracker_Db_Exception("Error query: " . $e->getMessage()); + } + } + + /** + * Returns the first row of a query result, using optional bound parameters. + * + * @param string $query Query + * @param array $parameters Parameters to bind + * @return bool|mixed + * @see query() + * @throws Exception|Piwik_Tracker_Db_Exception if an exception occurred + */ + public function fetch($query, $parameters = array()) + { + try { + $sth = $this->query($query, $parameters); + if ($sth === false) { + return false; + } + return $sth->fetch(PDO::FETCH_ASSOC); + } catch (PDOException $e) { + throw new Piwik_Tracker_Db_Exception("Error query: " . $e->getMessage()); + } + } + + /** + * Executes a query, using optional bound parameters. + * + * @param string $query Query + * @param array|string $parameters Parameters to bind array('idsite'=> 1) + * @return PDOStatement|bool PDOStatement or false if failed + * @throws Piwik_Tracker_Db_Exception if an exception occured + */ + public function query($query, $parameters = array()) + { + if (is_null($this->connection)) { + return false; + } + + try { + if (self::$profiling) { + $timer = $this->initProfiler(); + } + + if (!is_array($parameters)) { + $parameters = array($parameters); + } + $sth = $this->connection->prepare($query); + $sth->execute($parameters); + + if (self::$profiling) { + $this->recordQueryProfile($query, $timer); + } + return $sth; + } catch (PDOException $e) { + throw new Piwik_Tracker_Db_Exception("Error query: " . $e->getMessage() . " In query: $query - Parameters: ".var_export($parameters, true)); - } - } - - /** - * Returns the last inserted ID in the DB - * Wrapper of PDO::lastInsertId() - * - * @return int - */ - public function lastInsertId() - { - return $this->connection->lastInsertId(); - } - - /** - * Test error number - * - * @param Exception $e - * @param string $errno - * @return bool - */ - public function isErrNo($e, $errno) - { - if(preg_match('/([0-9]{4})/', $e->getMessage(), $match)) - { - return $match[1] == $errno; - } - return false; - } - - /** - * Return number of affected rows in last query - * - * @param mixed $queryResult Result from query() - * @return int - */ - public function rowCount($queryResult) - { - return $queryResult->rowCount(); - } + Parameters: " . var_export($parameters, true)); + } + } + + /** + * Returns the last inserted ID in the DB + * Wrapper of PDO::lastInsertId() + * + * @return int + */ + public function lastInsertId() + { + return $this->connection->lastInsertId(); + } + + /** + * Test error number + * + * @param Exception $e + * @param string $errno + * @return bool + */ + public function isErrNo($e, $errno) + { + if (preg_match('/([0-9]{4})/', $e->getMessage(), $match)) { + return $match[1] == $errno; + } + return false; + } + + /** + * Return number of affected rows in last query + * + * @param mixed $queryResult Result from query() + * @return int + */ + public function rowCount($queryResult) + { + return $queryResult->rowCount(); + } } diff --git a/core/Tracker/Db/Pdo/Pgsql.php b/core/Tracker/Db/Pdo/Pgsql.php index 22a020c359..c03246b348 100644 --- a/core/Tracker/Db/Pdo/Pgsql.php +++ b/core/Tracker/Db/Pdo/Pgsql.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -17,104 +17,100 @@ */ class Piwik_Tracker_Db_Pdo_Pgsql extends Piwik_Tracker_Db_Pdo_Mysql { - /** - * Builds the DB object - * - * @param array $dbInfo - * @param string $driverName - */ - public function __construct( $dbInfo, $driverName = 'pgsql') - { - parent::__construct( $dbInfo, $driverName ); - } + /** + * Builds the DB object + * + * @param array $dbInfo + * @param string $driverName + */ + public function __construct($dbInfo, $driverName = 'pgsql') + { + parent::__construct($dbInfo, $driverName); + } + + /** + * Connects to the DB + * + * @throws Exception if there was an error connecting the DB + */ + public function connect() + { + if (self::$profiling) { + $timer = $this->initProfiler(); + } + - /** - * Connects to the DB - * - * @throws Exception if there was an error connecting the DB - */ - public function connect() - { - if(self::$profiling) - { - $timer = $this->initProfiler(); - } + $this->connection = new PDO($this->dsn, $this->username, $this->password, $config = array()); + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + // we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked + // the piwik.php would stay waiting for the database... bad! + // we delete the password from this object "just in case" it could be printed + $this->password = ''; - - $this->connection = new PDO($this->dsn, $this->username, $this->password, $config = array()); - $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - // we may want to setAttribute(PDO::ATTR_TIMEOUT ) to a few seconds (default is 60) in case the DB is locked - // the piwik.php would stay waiting for the database... bad! - // we delete the password from this object "just in case" it could be printed - $this->password = ''; + if (!empty($this->charset)) { + $sql = "SET NAMES '" . $this->charset . "'"; + $this->connection->exec($sql); + } - if(!empty($this->charset)) - { - $sql = "SET NAMES '" . $this->charset . "'"; - $this->connection->exec($sql); - } + if (self::$profiling) { + $this->recordQueryProfile('connect', $timer); + } + } - if(self::$profiling) - { - $this->recordQueryProfile('connect', $timer); - } - } - - /** - * Test error number - * - * @param Exception $e - * @param string $errno - * @return bool - */ - public function isErrNo($e, $errno) - { - // map MySQL driver-specific error codes to PostgreSQL SQLSTATE - $map = array( - // MySQL: Unknown database '%s' - // PostgreSQL: database "%s" does not exist - '1049' => '08006', + /** + * Test error number + * + * @param Exception $e + * @param string $errno + * @return bool + */ + public function isErrNo($e, $errno) + { + // map MySQL driver-specific error codes to PostgreSQL SQLSTATE + $map = array( + // MySQL: Unknown database '%s' + // PostgreSQL: database "%s" does not exist + '1049' => '08006', - // MySQL: Table '%s' already exists - // PostgreSQL: relation "%s" already exists - '1050' => '42P07', + // MySQL: Table '%s' already exists + // PostgreSQL: relation "%s" already exists + '1050' => '42P07', - // MySQL: Unknown column '%s' in '%s' - // PostgreSQL: column "%s" does not exist - '1054' => '42703', + // MySQL: Unknown column '%s' in '%s' + // PostgreSQL: column "%s" does not exist + '1054' => '42703', - // MySQL: Duplicate column name '%s' - // PostgreSQL: column "%s" of relation "%s" already exists - '1060' => '42701', + // MySQL: Duplicate column name '%s' + // PostgreSQL: column "%s" of relation "%s" already exists + '1060' => '42701', - // MySQL: Duplicate entry '%s' for key '%s' - // PostgreSQL: duplicate key violates unique constraint - '1062' => '23505', + // MySQL: Duplicate entry '%s' for key '%s' + // PostgreSQL: duplicate key violates unique constraint + '1062' => '23505', - // MySQL: Can't DROP '%s'; check that column/key exists - // PostgreSQL: index "%s" does not exist - '1091' => '42704', + // MySQL: Can't DROP '%s'; check that column/key exists + // PostgreSQL: index "%s" does not exist + '1091' => '42704', - // MySQL: Table '%s.%s' doesn't exist - // PostgreSQL: relation "%s" does not exist - '1146' => '42P01', - ); + // MySQL: Table '%s.%s' doesn't exist + // PostgreSQL: relation "%s" does not exist + '1146' => '42P01', + ); - if(preg_match('/([0-9]{2}[0-9P][0-9]{2})/', $e->getMessage(), $match)) - { - return $match[1] == $map[$errno]; - } - return false; - } + if (preg_match('/([0-9]{2}[0-9P][0-9]{2})/', $e->getMessage(), $match)) { + return $match[1] == $map[$errno]; + } + return false; + } - /** - * Return number of affected rows in last query - * - * @param mixed $queryResult Result from query() - * @return int - */ - public function rowCount($queryResult) - { - return $queryResult->rowCount(); - } + /** + * Return number of affected rows in last query + * + * @param mixed $queryResult Result from query() + * @return int + */ + public function rowCount($queryResult) + { + return $queryResult->rowCount(); + } } diff --git a/core/Tracker/GoalManager.php b/core/Tracker/GoalManager.php index 9e77ab75e9..a00c49ad3f 100644 --- a/core/Tracker/GoalManager.php +++ b/core/Tracker/GoalManager.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -13,860 +13,799 @@ * @package Piwik * @subpackage Piwik_Tracker */ -class Piwik_Tracker_GoalManager +class Piwik_Tracker_GoalManager { - // log_visit.visit_goal_buyer - const TYPE_BUYER_NONE = 0; - const TYPE_BUYER_ORDERED = 1; - const TYPE_BUYER_OPEN_CART = 2; - const TYPE_BUYER_ORDERED_AND_OPEN_CART = 3; - - // log_conversion.idorder is NULLable, but not log_conversion_item which defaults to zero for carts - const ITEM_IDORDER_ABANDONED_CART = 0; - - // log_conversion.idgoal special values - const IDGOAL_CART = -1; - const IDGOAL_ORDER = 0; - - const REVENUE_PRECISION = 2; - - const MAXIMUM_PRODUCT_CATEGORIES = 5; - public $idGoal; - public $requestIsEcommerce; - public $isGoalAnOrder; - - /** - * @var Piwik_Tracker_Action - */ - protected $action = null; - protected $convertedGoals = array(); - protected $isThereExistingCartInVisit = false; - protected $request; - protected $orderId; - - function init($request) - { - $this->request = $request; - $this->orderId = Piwik_Common::getRequestVar('ec_id', false, 'string', $this->request); - $this->isGoalAnOrder = !empty($this->orderId); - $this->idGoal = Piwik_Common::getRequestVar('idgoal', -1, 'int', $this->request); - $this->requestIsEcommerce = ($this->idGoal == 0); - } - - function getBuyerType($existingType = Piwik_Tracker_GoalManager::TYPE_BUYER_NONE) - { - // Was there a Cart for this visit prior to the order? - $this->isThereExistingCartInVisit = in_array($existingType, - array( Piwik_Tracker_GoalManager::TYPE_BUYER_OPEN_CART, - Piwik_Tracker_GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART)); - - if(!$this->requestIsEcommerce) - { - return $existingType; - } - if($this->isGoalAnOrder) - { - return self::TYPE_BUYER_ORDERED; - } - // request is Add to Cart - if($existingType == self::TYPE_BUYER_ORDERED - || $existingType == self::TYPE_BUYER_ORDERED_AND_OPEN_CART) - { - return self::TYPE_BUYER_ORDERED_AND_OPEN_CART; - } - return self::TYPE_BUYER_OPEN_CART; - } - - static public function getGoalDefinitions( $idSite ) - { - $websiteAttributes = Piwik_Tracker_Cache::getCacheWebsiteAttributes( $idSite ); - if(isset($websiteAttributes['goals'])) - { - return $websiteAttributes['goals']; - } - return array(); - } - - static public function getGoalDefinition( $idSite, $idGoal ) - { - $goals = self::getGoalDefinitions( $idSite ); - foreach($goals as $goal) - { - if($goal['idgoal'] == $idGoal) - { - return $goal; - } - } - throw new Exception('Goal not found'); - } - - static public function getGoalIds( $idSite ) - { - $goals = self::getGoalDefinitions( $idSite ); - $goalIds = array(); - foreach($goals as $goal) - { - $goalIds[] = $goal['idgoal']; - } - return $goalIds; - } - - /** - * Look at the URL or Page Title and sees if it matches any existing Goal definition - * - * @param int $idSite - * @param Piwik_Tracker_Action $action - * @throws Exception - * @return int Number of goals matched - */ - function detectGoalsMatchingUrl($idSite, $action) - { - if(!Piwik_Common::isGoalPluginEnabled()) - { - return false; - } - - $decodedActionUrl = $action->getActionUrl(); - $actionType = $action->getActionType(); - $goals = $this->getGoalDefinitions($idSite); - foreach($goals as $goal) - { - $attribute = $goal['match_attribute']; - // if the attribute to match is not the type of the current action - if( ($actionType == Piwik_Tracker_Action::TYPE_ACTION_URL && $attribute != 'url' && $attribute != 'title') - || ($actionType == Piwik_Tracker_Action::TYPE_DOWNLOAD && $attribute != 'file') - || ($actionType == Piwik_Tracker_Action::TYPE_OUTLINK && $attribute != 'external_website') - || ($attribute == 'manually') - ) - { - continue; - } - - $url = $decodedActionUrl; - // Matching on Page Title - if($attribute == 'title') - { - $url = $action->getActionName(); - } - $pattern_type = $goal['pattern_type']; - - switch($pattern_type) - { - case 'regex': - $pattern = $goal['pattern']; - if(strpos($pattern, '/') !== false - && strpos($pattern, '\\/') === false) - { - $pattern = str_replace('/', '\\/', $pattern); - } - $pattern = '/' . $pattern . '/'; - if(!$goal['case_sensitive']) - { - $pattern .= 'i'; - } - $match = (@preg_match($pattern, $url) == 1); - break; - case 'contains': - if($goal['case_sensitive']) - { - $matched = strpos($url, $goal['pattern']); - } - else - { - $matched = stripos($url, $goal['pattern']); - } - $match = ($matched !== false); - break; - case 'exact': - if($goal['case_sensitive']) - { - $matched = strcmp($goal['pattern'], $url); - } - else - { - $matched = strcasecmp($goal['pattern'], $url); - } - $match = ($matched == 0); - break; - default: - throw new Exception(Piwik_TranslateException('General_ExceptionInvalidGoalPattern', array($pattern_type))); - break; - } - if($match) - { - $goal['url'] = $decodedActionUrl; - $this->convertedGoals[] = $goal; - } - } - return count($this->convertedGoals) > 0; - } - - function detectGoalId($idSite) - { - if(!Piwik_Common::isGoalPluginEnabled()) - { - return false; - } - $goals = $this->getGoalDefinitions($idSite); - if(!isset($goals[$this->idGoal])) - { - return false; - } - $goal = $goals[$this->idGoal]; - - $url = Piwik_Common::getRequestVar( 'url', '', 'string', $this->request); - $goal['url'] = Piwik_Tracker_Action::excludeQueryParametersFromUrl($url, $idSite); - $goal['revenue'] = $this->getRevenue(Piwik_Common::getRequestVar('revenue', $goal['revenue'], 'float', $this->request)); - $this->convertedGoals[] = $goal; - return true; - } - - /** - * Records one or several goals matched in this request. - * @param int $idSite - * @param array $visitorInformation - * @param array $visitCustomVariables - * @param string $action - * @param $referrerTimestamp - * @param string $referrerUrl - * @param string $referrerCampaignName - * @param string $referrerCampaignKeyword - */ - public function recordGoals($idSite, $visitorInformation, $visitCustomVariables, $action, $referrerTimestamp, $referrerUrl, $referrerCampaignName, $referrerCampaignKeyword, $browserLanguage) - { - $location_country = isset($visitorInformation['location_country']) - ? $visitorInformation['location_country'] - : Piwik_Common::getCountry( - $browserLanguage, - $enableLanguageToCountryGuess = Piwik_Config::getInstance()->Tracker['enable_language_to_country_guess'], - $visitorInformation['location_ip'] - ); - - $goal = array( - 'idvisit' => $visitorInformation['idvisit'], - 'idsite' => $idSite, - 'idvisitor' => $visitorInformation['idvisitor'], - 'server_time' => Piwik_Tracker::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time']), - 'location_country' => $location_country, - 'visitor_returning' => $visitorInformation['visitor_returning'], - 'visitor_days_since_first' => $visitorInformation['visitor_days_since_first'], - 'visitor_days_since_order' => $visitorInformation['visitor_days_since_order'], - 'visitor_count_visits' => $visitorInformation['visitor_count_visits'], - ); - - $extraLocationCols = array('location_region', 'location_city', 'location_latitude', 'location_longitude'); - foreach ($extraLocationCols as $col) - { - if (isset($visitorInformation[$col])) - { - $goal[$col] = $visitorInformation[$col]; - } - } - - // Copy Custom Variables from Visit row to the Goal conversion - for($i=1; $i<=Piwik_Tracker::MAX_CUSTOM_VARIABLES; $i++) - { - if(isset($visitorInformation['custom_var_k'.$i]) - && strlen($visitorInformation['custom_var_k'.$i])) - { - $goal['custom_var_k'.$i] = $visitorInformation['custom_var_k'.$i]; - } - if(isset($visitorInformation['custom_var_v'.$i]) - && strlen($visitorInformation['custom_var_v'.$i])) - { - $goal['custom_var_v'.$i] = $visitorInformation['custom_var_v'.$i]; - } - } - // Otherwise, set the Custom Variables found in the cookie sent with this request - $goal += $visitCustomVariables; - - // Attributing the correct Referrer to this conversion. - // Priority order is as follows: - // 0) In some cases, the campaign is not passed from the JS so we look it up from the current visit - // 1) Campaign name/kwd parsed in the JS - // 2) Referrer URL stored in the _ref cookie - // 3) If no info from the cookie, attribute to the current visit referrer - - // 3) Default values: current referrer + // log_visit.visit_goal_buyer + const TYPE_BUYER_NONE = 0; + const TYPE_BUYER_ORDERED = 1; + const TYPE_BUYER_OPEN_CART = 2; + const TYPE_BUYER_ORDERED_AND_OPEN_CART = 3; + + // log_conversion.idorder is NULLable, but not log_conversion_item which defaults to zero for carts + const ITEM_IDORDER_ABANDONED_CART = 0; + + // log_conversion.idgoal special values + const IDGOAL_CART = -1; + const IDGOAL_ORDER = 0; + + const REVENUE_PRECISION = 2; + + const MAXIMUM_PRODUCT_CATEGORIES = 5; + public $idGoal; + public $requestIsEcommerce; + public $isGoalAnOrder; + + /** + * @var Piwik_Tracker_Action + */ + protected $action = null; + protected $convertedGoals = array(); + protected $isThereExistingCartInVisit = false; + protected $request; + protected $orderId; + + function init($request) + { + $this->request = $request; + $this->orderId = Piwik_Common::getRequestVar('ec_id', false, 'string', $this->request); + $this->isGoalAnOrder = !empty($this->orderId); + $this->idGoal = Piwik_Common::getRequestVar('idgoal', -1, 'int', $this->request); + $this->requestIsEcommerce = ($this->idGoal == 0); + } + + function getBuyerType($existingType = Piwik_Tracker_GoalManager::TYPE_BUYER_NONE) + { + // Was there a Cart for this visit prior to the order? + $this->isThereExistingCartInVisit = in_array($existingType, + array(Piwik_Tracker_GoalManager::TYPE_BUYER_OPEN_CART, + Piwik_Tracker_GoalManager::TYPE_BUYER_ORDERED_AND_OPEN_CART)); + + if (!$this->requestIsEcommerce) { + return $existingType; + } + if ($this->isGoalAnOrder) { + return self::TYPE_BUYER_ORDERED; + } + // request is Add to Cart + if ($existingType == self::TYPE_BUYER_ORDERED + || $existingType == self::TYPE_BUYER_ORDERED_AND_OPEN_CART + ) { + return self::TYPE_BUYER_ORDERED_AND_OPEN_CART; + } + return self::TYPE_BUYER_OPEN_CART; + } + + static public function getGoalDefinitions($idSite) + { + $websiteAttributes = Piwik_Tracker_Cache::getCacheWebsiteAttributes($idSite); + if (isset($websiteAttributes['goals'])) { + return $websiteAttributes['goals']; + } + return array(); + } + + static public function getGoalDefinition($idSite, $idGoal) + { + $goals = self::getGoalDefinitions($idSite); + foreach ($goals as $goal) { + if ($goal['idgoal'] == $idGoal) { + return $goal; + } + } + throw new Exception('Goal not found'); + } + + static public function getGoalIds($idSite) + { + $goals = self::getGoalDefinitions($idSite); + $goalIds = array(); + foreach ($goals as $goal) { + $goalIds[] = $goal['idgoal']; + } + return $goalIds; + } + + /** + * Look at the URL or Page Title and sees if it matches any existing Goal definition + * + * @param int $idSite + * @param Piwik_Tracker_Action $action + * @throws Exception + * @return int Number of goals matched + */ + function detectGoalsMatchingUrl($idSite, $action) + { + if (!Piwik_Common::isGoalPluginEnabled()) { + return false; + } + + $decodedActionUrl = $action->getActionUrl(); + $actionType = $action->getActionType(); + $goals = $this->getGoalDefinitions($idSite); + foreach ($goals as $goal) { + $attribute = $goal['match_attribute']; + // if the attribute to match is not the type of the current action + if (($actionType == Piwik_Tracker_Action::TYPE_ACTION_URL && $attribute != 'url' && $attribute != 'title') + || ($actionType == Piwik_Tracker_Action::TYPE_DOWNLOAD && $attribute != 'file') + || ($actionType == Piwik_Tracker_Action::TYPE_OUTLINK && $attribute != 'external_website') + || ($attribute == 'manually') + ) { + continue; + } + + $url = $decodedActionUrl; + // Matching on Page Title + if ($attribute == 'title') { + $url = $action->getActionName(); + } + $pattern_type = $goal['pattern_type']; + + switch ($pattern_type) { + case 'regex': + $pattern = $goal['pattern']; + if (strpos($pattern, '/') !== false + && strpos($pattern, '\\/') === false + ) { + $pattern = str_replace('/', '\\/', $pattern); + } + $pattern = '/' . $pattern . '/'; + if (!$goal['case_sensitive']) { + $pattern .= 'i'; + } + $match = (@preg_match($pattern, $url) == 1); + break; + case 'contains': + if ($goal['case_sensitive']) { + $matched = strpos($url, $goal['pattern']); + } else { + $matched = stripos($url, $goal['pattern']); + } + $match = ($matched !== false); + break; + case 'exact': + if ($goal['case_sensitive']) { + $matched = strcmp($goal['pattern'], $url); + } else { + $matched = strcasecmp($goal['pattern'], $url); + } + $match = ($matched == 0); + break; + default: + throw new Exception(Piwik_TranslateException('General_ExceptionInvalidGoalPattern', array($pattern_type))); + break; + } + if ($match) { + $goal['url'] = $decodedActionUrl; + $this->convertedGoals[] = $goal; + } + } + return count($this->convertedGoals) > 0; + } + + function detectGoalId($idSite) + { + if (!Piwik_Common::isGoalPluginEnabled()) { + return false; + } + $goals = $this->getGoalDefinitions($idSite); + if (!isset($goals[$this->idGoal])) { + return false; + } + $goal = $goals[$this->idGoal]; + + $url = Piwik_Common::getRequestVar('url', '', 'string', $this->request); + $goal['url'] = Piwik_Tracker_Action::excludeQueryParametersFromUrl($url, $idSite); + $goal['revenue'] = $this->getRevenue(Piwik_Common::getRequestVar('revenue', $goal['revenue'], 'float', $this->request)); + $this->convertedGoals[] = $goal; + return true; + } + + /** + * Records one or several goals matched in this request. + * @param int $idSite + * @param array $visitorInformation + * @param array $visitCustomVariables + * @param string $action + * @param $referrerTimestamp + * @param string $referrerUrl + * @param string $referrerCampaignName + * @param string $referrerCampaignKeyword + */ + public function recordGoals($idSite, $visitorInformation, $visitCustomVariables, $action, $referrerTimestamp, $referrerUrl, $referrerCampaignName, $referrerCampaignKeyword, $browserLanguage) + { + $location_country = isset($visitorInformation['location_country']) + ? $visitorInformation['location_country'] + : Piwik_Common::getCountry( + $browserLanguage, + $enableLanguageToCountryGuess = Piwik_Config::getInstance()->Tracker['enable_language_to_country_guess'], + $visitorInformation['location_ip'] + ); + + $goal = array( + 'idvisit' => $visitorInformation['idvisit'], + 'idsite' => $idSite, + 'idvisitor' => $visitorInformation['idvisitor'], + 'server_time' => Piwik_Tracker::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time']), + 'location_country' => $location_country, + 'visitor_returning' => $visitorInformation['visitor_returning'], + 'visitor_days_since_first' => $visitorInformation['visitor_days_since_first'], + 'visitor_days_since_order' => $visitorInformation['visitor_days_since_order'], + 'visitor_count_visits' => $visitorInformation['visitor_count_visits'], + ); + + $extraLocationCols = array('location_region', 'location_city', 'location_latitude', 'location_longitude'); + foreach ($extraLocationCols as $col) { + if (isset($visitorInformation[$col])) { + $goal[$col] = $visitorInformation[$col]; + } + } + + // Copy Custom Variables from Visit row to the Goal conversion + for ($i = 1; $i <= Piwik_Tracker::MAX_CUSTOM_VARIABLES; $i++) { + if (isset($visitorInformation['custom_var_k' . $i]) + && strlen($visitorInformation['custom_var_k' . $i]) + ) { + $goal['custom_var_k' . $i] = $visitorInformation['custom_var_k' . $i]; + } + if (isset($visitorInformation['custom_var_v' . $i]) + && strlen($visitorInformation['custom_var_v' . $i]) + ) { + $goal['custom_var_v' . $i] = $visitorInformation['custom_var_v' . $i]; + } + } + // Otherwise, set the Custom Variables found in the cookie sent with this request + $goal += $visitCustomVariables; + + // Attributing the correct Referrer to this conversion. + // Priority order is as follows: + // 0) In some cases, the campaign is not passed from the JS so we look it up from the current visit + // 1) Campaign name/kwd parsed in the JS + // 2) Referrer URL stored in the _ref cookie + // 3) If no info from the cookie, attribute to the current visit referrer + + // 3) Default values: current referrer $type = $visitorInformation['referer_type']; $name = $visitorInformation['referer_name']; $keyword = $visitorInformation['referer_keyword']; $time = $visitorInformation['visit_first_action_time']; - + // 0) In some (unknown!?) cases the campaign is not found in the attribution cookie, but the URL ref was found. - // In this case we look up if the current visit is credited to a campaign and will credit this campaign rather than the URL ref (since campaigns have higher priority) - if(empty($referrerCampaignName) - && $type == Piwik_Common::REFERER_TYPE_CAMPAIGN - && !empty($name) - ) - { - // Use default values per above - } - // 1) Campaigns from 1st party cookie - elseif(!empty($referrerCampaignName)) - { - $type = Piwik_Common::REFERER_TYPE_CAMPAIGN; - $name = $referrerCampaignName; - $keyword = $referrerCampaignKeyword; - $time = $referrerTimestamp; - } - // 2) Referrer URL parsing - elseif(!empty($referrerUrl)) - { - $referrer = new Piwik_Tracker_Visit_Referer(); + // In this case we look up if the current visit is credited to a campaign and will credit this campaign rather than the URL ref (since campaigns have higher priority) + if (empty($referrerCampaignName) + && $type == Piwik_Common::REFERER_TYPE_CAMPAIGN + && !empty($name) + ) { + // Use default values per above + } // 1) Campaigns from 1st party cookie + elseif (!empty($referrerCampaignName)) { + $type = Piwik_Common::REFERER_TYPE_CAMPAIGN; + $name = $referrerCampaignName; + $keyword = $referrerCampaignKeyword; + $time = $referrerTimestamp; + } // 2) Referrer URL parsing + elseif (!empty($referrerUrl)) { + $referrer = new Piwik_Tracker_Visit_Referer(); $referrer = $referrer->getRefererInformation($referrerUrl, $currentUrl = '', $idSite); - + // if the parsed referer is interesting enough, ie. website or search engine - if(in_array($referrer['referer_type'], array(Piwik_Common::REFERER_TYPE_SEARCH_ENGINE, Piwik_Common::REFERER_TYPE_WEBSITE))) - { - $type = $referrer['referer_type']; - $name = $referrer['referer_name']; - $keyword = $referrer['referer_keyword']; - $time = $referrerTimestamp; + if (in_array($referrer['referer_type'], array(Piwik_Common::REFERER_TYPE_SEARCH_ENGINE, Piwik_Common::REFERER_TYPE_WEBSITE))) { + $type = $referrer['referer_type']; + $name = $referrer['referer_name']; + $keyword = $referrer['referer_keyword']; + $time = $referrerTimestamp; } - } - $goal += array( - 'referer_type' => $type, - 'referer_name' => $name, - 'referer_keyword' => $keyword, - // this field is currently unused - 'referer_visit_server_date' => date("Y-m-d", $time), - ); - - // some goals are converted, so must be ecommerce Order or Cart Update - if($this->requestIsEcommerce) - { - $this->recordEcommerceGoal($goal, $visitorInformation); - } - else - { - $this->recordStandardGoals($goal, $action, $visitorInformation); - } - } - - /** - * Returns rounded decimal revenue, or if revenue is integer, then returns as is. - * - * @param int|float $revenue - * @return int|float - */ - protected function getRevenue($revenue) - { - if(round($revenue) == $revenue) - { - return $revenue; - } - return round($revenue, self::REVENUE_PRECISION); - } - - /** - * Records an Ecommerce conversion in the DB. Deals with Items found in the request. - * Will deal with 2 types of conversions: Ecommerce Order and Ecommerce Cart update (Add to cart, Update Cart etc). - * - * @param array $goal - * @param array $visitorInformation - */ - protected function recordEcommerceGoal($goal, $visitorInformation) - { - // Is the transaction a Cart Update or an Ecommerce order? - $updateWhere = array( - 'idvisit' => $visitorInformation['idvisit'], - 'idgoal' => self::IDGOAL_CART, - 'buster' => 0, - ); - - if($this->isThereExistingCartInVisit) - { - printDebug("There is an existing cart for this visit"); - } - if($this->isGoalAnOrder) - { - $orderIdNumeric = Piwik_Common::hashStringToInt($this->orderId); - $goal['idgoal'] = self::IDGOAL_ORDER; - $goal['idorder'] = $this->orderId; - $goal['buster'] = $orderIdNumeric; - $goal['revenue_subtotal'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_st', false, 'float', $this->request)); - $goal['revenue_tax'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_tx', false, 'float', $this->request)); - $goal['revenue_shipping'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_sh', false, 'float', $this->request)); - $goal['revenue_discount'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_dt', false, 'float', $this->request)); - - $debugMessage = 'The conversion is an Ecommerce order'; - } - // If Cart update, select current items in the previous Cart - else - { - $goal['buster'] = 0; - $goal['idgoal'] = self::IDGOAL_CART; - $debugMessage = 'The conversion is an Ecommerce Cart Update'; - } - $goal['revenue'] = $this->getRevenue(Piwik_Common::getRequestVar('revenue', 0, 'float', $this->request)); - - printDebug($debugMessage . ':' . var_export($goal, true)); - - // INSERT or Sync items in the Cart / Order for this visit & order - $items = $this->getEcommerceItemsFromRequest(); - if($items === false) - { - return; - } - - $itemsCount = 0; - foreach($items as $item) - { - $itemsCount += $item[self::INTERNAL_ITEM_QUANTITY]; - } - $goal['items'] = $itemsCount; - - // If there is already a cart for this visit - // 1) If conversion is Order, we update the cart into an Order - // 2) If conversion is Cart Update, we update the cart - $recorded = $this->recordGoal($goal, $this->isThereExistingCartInVisit, $updateWhere); - if($recorded) - { - $this->recordEcommerceItems($goal, $items); - } - - Piwik_PostEvent('Tracker.recordEcommerceGoal', $goal); - } - - /** - * Returns Items read from the request string - * @return array|false - */ - protected function getEcommerceItemsFromRequest() - { - $items = Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('ec_items', '', 'string', $this->request)); - if(empty($items)) - { - printDebug("There are no Ecommerce items in the request"); - // we still record an Ecommerce order without any item in it - return array(); - } - $items = Piwik_Common::json_decode($items, $assoc = true); - if(!is_array($items)) - { - printDebug("Error while json_decode the Ecommerce items = ".var_export($items, true)); - return false; - } - - $cleanedItems = $this->getCleanedEcommerceItems($items); - return $cleanedItems; - } - - /** - * Loads the Ecommerce items from the request and records them in the DB - * - * @param array $goal - * @param array $items - * @throws Exception - * @return int Number of items in the cart - */ - protected function recordEcommerceItems($goal, $items) - { - $itemInCartBySku = array(); - foreach($items as $item) - { - $itemInCartBySku[$item[0]] = $item; - } - - // Select all items currently in the Cart if any - $sql = "SELECT idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder as idorder_original_value - FROM ". Piwik_Common::prefixTable('log_conversion_item') . " + } + $goal += array( + 'referer_type' => $type, + 'referer_name' => $name, + 'referer_keyword' => $keyword, + // this field is currently unused + 'referer_visit_server_date' => date("Y-m-d", $time), + ); + + // some goals are converted, so must be ecommerce Order or Cart Update + if ($this->requestIsEcommerce) { + $this->recordEcommerceGoal($goal, $visitorInformation); + } else { + $this->recordStandardGoals($goal, $action, $visitorInformation); + } + } + + /** + * Returns rounded decimal revenue, or if revenue is integer, then returns as is. + * + * @param int|float $revenue + * @return int|float + */ + protected function getRevenue($revenue) + { + if (round($revenue) == $revenue) { + return $revenue; + } + return round($revenue, self::REVENUE_PRECISION); + } + + /** + * Records an Ecommerce conversion in the DB. Deals with Items found in the request. + * Will deal with 2 types of conversions: Ecommerce Order and Ecommerce Cart update (Add to cart, Update Cart etc). + * + * @param array $goal + * @param array $visitorInformation + */ + protected function recordEcommerceGoal($goal, $visitorInformation) + { + // Is the transaction a Cart Update or an Ecommerce order? + $updateWhere = array( + 'idvisit' => $visitorInformation['idvisit'], + 'idgoal' => self::IDGOAL_CART, + 'buster' => 0, + ); + + if ($this->isThereExistingCartInVisit) { + printDebug("There is an existing cart for this visit"); + } + if ($this->isGoalAnOrder) { + $orderIdNumeric = Piwik_Common::hashStringToInt($this->orderId); + $goal['idgoal'] = self::IDGOAL_ORDER; + $goal['idorder'] = $this->orderId; + $goal['buster'] = $orderIdNumeric; + $goal['revenue_subtotal'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_st', false, 'float', $this->request)); + $goal['revenue_tax'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_tx', false, 'float', $this->request)); + $goal['revenue_shipping'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_sh', false, 'float', $this->request)); + $goal['revenue_discount'] = $this->getRevenue(Piwik_Common::getRequestVar('ec_dt', false, 'float', $this->request)); + + $debugMessage = 'The conversion is an Ecommerce order'; + } // If Cart update, select current items in the previous Cart + else { + $goal['buster'] = 0; + $goal['idgoal'] = self::IDGOAL_CART; + $debugMessage = 'The conversion is an Ecommerce Cart Update'; + } + $goal['revenue'] = $this->getRevenue(Piwik_Common::getRequestVar('revenue', 0, 'float', $this->request)); + + printDebug($debugMessage . ':' . var_export($goal, true)); + + // INSERT or Sync items in the Cart / Order for this visit & order + $items = $this->getEcommerceItemsFromRequest(); + if ($items === false) { + return; + } + + $itemsCount = 0; + foreach ($items as $item) { + $itemsCount += $item[self::INTERNAL_ITEM_QUANTITY]; + } + $goal['items'] = $itemsCount; + + // If there is already a cart for this visit + // 1) If conversion is Order, we update the cart into an Order + // 2) If conversion is Cart Update, we update the cart + $recorded = $this->recordGoal($goal, $this->isThereExistingCartInVisit, $updateWhere); + if ($recorded) { + $this->recordEcommerceItems($goal, $items); + } + + Piwik_PostEvent('Tracker.recordEcommerceGoal', $goal); + } + + /** + * Returns Items read from the request string + * @return array|false + */ + protected function getEcommerceItemsFromRequest() + { + $items = Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('ec_items', '', 'string', $this->request)); + if (empty($items)) { + printDebug("There are no Ecommerce items in the request"); + // we still record an Ecommerce order without any item in it + return array(); + } + $items = Piwik_Common::json_decode($items, $assoc = true); + if (!is_array($items)) { + printDebug("Error while json_decode the Ecommerce items = " . var_export($items, true)); + return false; + } + + $cleanedItems = $this->getCleanedEcommerceItems($items); + return $cleanedItems; + } + + /** + * Loads the Ecommerce items from the request and records them in the DB + * + * @param array $goal + * @param array $items + * @throws Exception + * @return int Number of items in the cart + */ + protected function recordEcommerceItems($goal, $items) + { + $itemInCartBySku = array(); + foreach ($items as $item) { + $itemInCartBySku[$item[0]] = $item; + } + + // Select all items currently in the Cart if any + $sql = "SELECT idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder as idorder_original_value + FROM " . Piwik_Common::prefixTable('log_conversion_item') . " WHERE idvisit = ? AND (idorder = ? OR idorder = ?)"; - - $bind = array( $goal['idvisit'], - isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, - self::ITEM_IDORDER_ABANDONED_CART - ); - - $itemsInDb = Piwik_Tracker::getDatabase()->fetchAll($sql, $bind); - - printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind,true)); - printDebug($itemsInDb); - // Look at which items need to be deleted, which need to be added or updated, based on the SKU - $skuFoundInDb = $itemsToUpdate = array(); - foreach($itemsInDb as $itemInDb) - { - $skuFoundInDb[] = $itemInDb['idaction_sku']; - - // Ensure price comparisons will have the same assumption - $itemInDb['price'] = $this->getRevenue($itemInDb['price']); - $itemInDbOriginal = $itemInDb; - $itemInDb = array_values($itemInDb); - - // Cast all as string, because what comes out of the fetchAll() are strings - $itemInDb = $this->getItemRowCast($itemInDb); - - //Item in the cart in the DB, but not anymore in the cart - if(!isset($itemInCartBySku[$itemInDb[0]])) - { - $itemToUpdate = array_merge($itemInDb, - array( 'deleted' => 1, - 'idorder_original_value' => $itemInDbOriginal['idorder_original_value'] - ) - ); - - $itemsToUpdate[] = $itemToUpdate; - printDebug("Item found in the previous Cart, but no in the current cart/order"); - printDebug($itemToUpdate); - continue; - } - - $newItem = $itemInCartBySku[$itemInDb[0]]; - $newItem = $this->getItemRowCast($newItem); - - if(count($itemInDb) != count($newItem)) - { - printDebug("ERROR: Different format in items from cart and DB"); - throw new Exception(" Item in DB and Item in cart have a different format, this is not expected... ".var_export($itemInDb, true) . var_export($newItem, true)); - } - printDebug("Item has changed since the last cart. Previous item stored in cart in database:"); - printDebug($itemInDb); - printDebug("New item to UPDATE the previous row:"); - $newItem['idorder_original_value'] = $itemInDbOriginal['idorder_original_value']; - printDebug($newItem); - $itemsToUpdate[] = $newItem; - } - - // Items to UPDATE - $this->updateEcommerceItems($goal, $itemsToUpdate); - - // Items to INSERT - $itemsToInsert = array(); - foreach($items as $item) - { - if(!in_array($item[0], $skuFoundInDb)) - { - $itemsToInsert[] = $item; - } - } - $this->insertEcommerceItems($goal, $itemsToInsert); - } - - // In the GET items parameter, each item has the following array of information - const INDEX_ITEM_SKU = 0; - const INDEX_ITEM_NAME = 1; - const INDEX_ITEM_CATEGORY = 2; - const INDEX_ITEM_PRICE = 3; - const INDEX_ITEM_QUANTITY = 4; - - // Used in the array of items, internally to this class - const INTERNAL_ITEM_SKU = 0; - const INTERNAL_ITEM_NAME = 1; - const INTERNAL_ITEM_CATEGORY = 2; - const INTERNAL_ITEM_CATEGORY2 = 3; - const INTERNAL_ITEM_CATEGORY3 = 4; - const INTERNAL_ITEM_CATEGORY4 = 5; - const INTERNAL_ITEM_CATEGORY5 = 6; - const INTERNAL_ITEM_PRICE = 7; - const INTERNAL_ITEM_QUANTITY = 8; - - /** - * Reads items from the request, then looks up the names from the lookup table - * and returns a clean array of items ready for the database. - * - * @param array $items - * @return array $cleanedItems - */ - protected function getCleanedEcommerceItems($items) - { - // Clean up the items array - $cleanedItems = array(); - foreach($items as $item) - { - $name = $category = $category2 = $category3 = $category4 = $category5 = false; - $price = 0; - $quantity = 1; - // items are passed in the request as an array: ( $sku, $name, $category, $price, $quantity ) - if(empty($item[self::INDEX_ITEM_SKU])) { - continue; - } - - $sku = $item[self::INDEX_ITEM_SKU]; - if(!empty($item[self::INDEX_ITEM_NAME])) { - $name = $item[self::INDEX_ITEM_NAME]; - } - - if(!empty($item[self::INDEX_ITEM_CATEGORY])) { - $category = $item[self::INDEX_ITEM_CATEGORY]; - } - - if(isset($item[self::INDEX_ITEM_PRICE]) - && is_numeric($item[self::INDEX_ITEM_PRICE])) { - $price = $this->getRevenue($item[self::INDEX_ITEM_PRICE]); - } - if(!empty($item[self::INDEX_ITEM_QUANTITY]) - && is_numeric($item[self::INDEX_ITEM_QUANTITY])) { - $quantity = (int)$item[self::INDEX_ITEM_QUANTITY]; - } - - // self::INDEX_ITEM_* are in order - $cleanedItems[] = array( - self::INTERNAL_ITEM_SKU => $sku, - self::INTERNAL_ITEM_NAME => $name, - self::INTERNAL_ITEM_CATEGORY => $category, - self::INTERNAL_ITEM_CATEGORY2 => $category2, - self::INTERNAL_ITEM_CATEGORY3 => $category3, - self::INTERNAL_ITEM_CATEGORY4 => $category4, - self::INTERNAL_ITEM_CATEGORY5 => $category5, - self::INTERNAL_ITEM_PRICE => $price, - self::INTERNAL_ITEM_QUANTITY => $quantity - ); - } - - // Lookup Item SKUs, Names & Categories Ids - $actionsToLookupAllItems = array(); - - // Each item has 7 potential "ids" to lookup in the lookup table - $columnsInEachRow = 1 + 1 + self::MAXIMUM_PRODUCT_CATEGORIES; - - foreach($cleanedItems as $item) - { - $actionsToLookup = array(); - list($sku, $name, $category, $price, $quantity) = $item; - $actionsToLookup[] = array(trim($sku), Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_SKU); - $actionsToLookup[] = array(trim($name), Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_NAME); - - // Only one category - if(!is_array($category)) - { - $actionsToLookup[] = array(trim($category), Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_CATEGORY); - } - // Multiple categories - else - { - $countCategories = 0; - foreach($category as $productCategory) { - $productCategory = trim($productCategory); - if(empty($productCategory)) { - continue; - } - $countCategories++; - if($countCategories > self::MAXIMUM_PRODUCT_CATEGORIES) { - break; - } - $actionsToLookup[] = array($productCategory, Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_CATEGORY); - } - } - // Ensure that each row has the same number of columns, fill in the blanks - for($i = count($actionsToLookup); $i < $columnsInEachRow; $i++) { - $actionsToLookup[] = array(false, Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_CATEGORY); - } - $actionsToLookupAllItems = array_merge($actionsToLookupAllItems, $actionsToLookup); - } - - $actionsLookedUp = Piwik_Tracker_Action::loadActionId($actionsToLookupAllItems); - - // Replace SKU, name & category by their ID action - foreach($cleanedItems as $index => &$item) - { - // SKU - $item[0] = $actionsLookedUp[ $index * $columnsInEachRow + 0][2]; - // Name - $item[1] = $actionsLookedUp[ $index * $columnsInEachRow + 1][2]; - // Categories - $item[2] = $actionsLookedUp[ $index * $columnsInEachRow + 2][2]; - $item[3] = $actionsLookedUp[ $index * $columnsInEachRow + 3][2]; - $item[4] = $actionsLookedUp[ $index * $columnsInEachRow + 4][2]; - $item[5] = $actionsLookedUp[ $index * $columnsInEachRow + 5][2]; - $item[6] = $actionsLookedUp[ $index * $columnsInEachRow + 6][2]; - } - return $cleanedItems; - } - - /** - * Updates the cart items in the DB - * that have been modified since the last cart update - * @param $goal - * @param array $itemsToUpdate - * @return - */ - protected function updateEcommerceItems($goal, $itemsToUpdate) - { - if(empty($itemsToUpdate)) - { - return; - } - printDebug("Goal data used to update ecommerce items:"); - printDebug($goal); - - foreach($itemsToUpdate as $item) - { - $newRow = $this->getItemRowEnriched($goal, $item); - printDebug($newRow); - $updateParts = $sqlBind = array(); - foreach($newRow AS $name => $value) - { - $updateParts[] = $name." = ?"; - $sqlBind[] = $value; - } - $sql = 'UPDATE ' . Piwik_Common::prefixTable('log_conversion_item') . " - SET ".implode($updateParts, ', ')." + + $bind = array($goal['idvisit'], + isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, + self::ITEM_IDORDER_ABANDONED_CART + ); + + $itemsInDb = Piwik_Tracker::getDatabase()->fetchAll($sql, $bind); + + printDebug("Items found in current cart, for conversion_item (visit,idorder)=" . var_export($bind, true)); + printDebug($itemsInDb); + // Look at which items need to be deleted, which need to be added or updated, based on the SKU + $skuFoundInDb = $itemsToUpdate = array(); + foreach ($itemsInDb as $itemInDb) { + $skuFoundInDb[] = $itemInDb['idaction_sku']; + + // Ensure price comparisons will have the same assumption + $itemInDb['price'] = $this->getRevenue($itemInDb['price']); + $itemInDbOriginal = $itemInDb; + $itemInDb = array_values($itemInDb); + + // Cast all as string, because what comes out of the fetchAll() are strings + $itemInDb = $this->getItemRowCast($itemInDb); + + //Item in the cart in the DB, but not anymore in the cart + if (!isset($itemInCartBySku[$itemInDb[0]])) { + $itemToUpdate = array_merge($itemInDb, + array('deleted' => 1, + 'idorder_original_value' => $itemInDbOriginal['idorder_original_value'] + ) + ); + + $itemsToUpdate[] = $itemToUpdate; + printDebug("Item found in the previous Cart, but no in the current cart/order"); + printDebug($itemToUpdate); + continue; + } + + $newItem = $itemInCartBySku[$itemInDb[0]]; + $newItem = $this->getItemRowCast($newItem); + + if (count($itemInDb) != count($newItem)) { + printDebug("ERROR: Different format in items from cart and DB"); + throw new Exception(" Item in DB and Item in cart have a different format, this is not expected... " . var_export($itemInDb, true) . var_export($newItem, true)); + } + printDebug("Item has changed since the last cart. Previous item stored in cart in database:"); + printDebug($itemInDb); + printDebug("New item to UPDATE the previous row:"); + $newItem['idorder_original_value'] = $itemInDbOriginal['idorder_original_value']; + printDebug($newItem); + $itemsToUpdate[] = $newItem; + } + + // Items to UPDATE + $this->updateEcommerceItems($goal, $itemsToUpdate); + + // Items to INSERT + $itemsToInsert = array(); + foreach ($items as $item) { + if (!in_array($item[0], $skuFoundInDb)) { + $itemsToInsert[] = $item; + } + } + $this->insertEcommerceItems($goal, $itemsToInsert); + } + + // In the GET items parameter, each item has the following array of information + const INDEX_ITEM_SKU = 0; + const INDEX_ITEM_NAME = 1; + const INDEX_ITEM_CATEGORY = 2; + const INDEX_ITEM_PRICE = 3; + const INDEX_ITEM_QUANTITY = 4; + + // Used in the array of items, internally to this class + const INTERNAL_ITEM_SKU = 0; + const INTERNAL_ITEM_NAME = 1; + const INTERNAL_ITEM_CATEGORY = 2; + const INTERNAL_ITEM_CATEGORY2 = 3; + const INTERNAL_ITEM_CATEGORY3 = 4; + const INTERNAL_ITEM_CATEGORY4 = 5; + const INTERNAL_ITEM_CATEGORY5 = 6; + const INTERNAL_ITEM_PRICE = 7; + const INTERNAL_ITEM_QUANTITY = 8; + + /** + * Reads items from the request, then looks up the names from the lookup table + * and returns a clean array of items ready for the database. + * + * @param array $items + * @return array $cleanedItems + */ + protected function getCleanedEcommerceItems($items) + { + // Clean up the items array + $cleanedItems = array(); + foreach ($items as $item) { + $name = $category = $category2 = $category3 = $category4 = $category5 = false; + $price = 0; + $quantity = 1; + // items are passed in the request as an array: ( $sku, $name, $category, $price, $quantity ) + if (empty($item[self::INDEX_ITEM_SKU])) { + continue; + } + + $sku = $item[self::INDEX_ITEM_SKU]; + if (!empty($item[self::INDEX_ITEM_NAME])) { + $name = $item[self::INDEX_ITEM_NAME]; + } + + if (!empty($item[self::INDEX_ITEM_CATEGORY])) { + $category = $item[self::INDEX_ITEM_CATEGORY]; + } + + if (isset($item[self::INDEX_ITEM_PRICE]) + && is_numeric($item[self::INDEX_ITEM_PRICE]) + ) { + $price = $this->getRevenue($item[self::INDEX_ITEM_PRICE]); + } + if (!empty($item[self::INDEX_ITEM_QUANTITY]) + && is_numeric($item[self::INDEX_ITEM_QUANTITY]) + ) { + $quantity = (int)$item[self::INDEX_ITEM_QUANTITY]; + } + + // self::INDEX_ITEM_* are in order + $cleanedItems[] = array( + self::INTERNAL_ITEM_SKU => $sku, + self::INTERNAL_ITEM_NAME => $name, + self::INTERNAL_ITEM_CATEGORY => $category, + self::INTERNAL_ITEM_CATEGORY2 => $category2, + self::INTERNAL_ITEM_CATEGORY3 => $category3, + self::INTERNAL_ITEM_CATEGORY4 => $category4, + self::INTERNAL_ITEM_CATEGORY5 => $category5, + self::INTERNAL_ITEM_PRICE => $price, + self::INTERNAL_ITEM_QUANTITY => $quantity + ); + } + + // Lookup Item SKUs, Names & Categories Ids + $actionsToLookupAllItems = array(); + + // Each item has 7 potential "ids" to lookup in the lookup table + $columnsInEachRow = 1 + 1 + self::MAXIMUM_PRODUCT_CATEGORIES; + + foreach ($cleanedItems as $item) { + $actionsToLookup = array(); + list($sku, $name, $category, $price, $quantity) = $item; + $actionsToLookup[] = array(trim($sku), Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_SKU); + $actionsToLookup[] = array(trim($name), Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_NAME); + + // Only one category + if (!is_array($category)) { + $actionsToLookup[] = array(trim($category), Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_CATEGORY); + } // Multiple categories + else { + $countCategories = 0; + foreach ($category as $productCategory) { + $productCategory = trim($productCategory); + if (empty($productCategory)) { + continue; + } + $countCategories++; + if ($countCategories > self::MAXIMUM_PRODUCT_CATEGORIES) { + break; + } + $actionsToLookup[] = array($productCategory, Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_CATEGORY); + } + } + // Ensure that each row has the same number of columns, fill in the blanks + for ($i = count($actionsToLookup); $i < $columnsInEachRow; $i++) { + $actionsToLookup[] = array(false, Piwik_Tracker_Action::TYPE_ECOMMERCE_ITEM_CATEGORY); + } + $actionsToLookupAllItems = array_merge($actionsToLookupAllItems, $actionsToLookup); + } + + $actionsLookedUp = Piwik_Tracker_Action::loadActionId($actionsToLookupAllItems); + + // Replace SKU, name & category by their ID action + foreach ($cleanedItems as $index => &$item) { + // SKU + $item[0] = $actionsLookedUp[$index * $columnsInEachRow + 0][2]; + // Name + $item[1] = $actionsLookedUp[$index * $columnsInEachRow + 1][2]; + // Categories + $item[2] = $actionsLookedUp[$index * $columnsInEachRow + 2][2]; + $item[3] = $actionsLookedUp[$index * $columnsInEachRow + 3][2]; + $item[4] = $actionsLookedUp[$index * $columnsInEachRow + 4][2]; + $item[5] = $actionsLookedUp[$index * $columnsInEachRow + 5][2]; + $item[6] = $actionsLookedUp[$index * $columnsInEachRow + 6][2]; + } + return $cleanedItems; + } + + /** + * Updates the cart items in the DB + * that have been modified since the last cart update + * @param $goal + * @param array $itemsToUpdate + * @return + */ + protected function updateEcommerceItems($goal, $itemsToUpdate) + { + if (empty($itemsToUpdate)) { + return; + } + printDebug("Goal data used to update ecommerce items:"); + printDebug($goal); + + foreach ($itemsToUpdate as $item) { + $newRow = $this->getItemRowEnriched($goal, $item); + printDebug($newRow); + $updateParts = $sqlBind = array(); + foreach ($newRow AS $name => $value) { + $updateParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + $sql = 'UPDATE ' . Piwik_Common::prefixTable('log_conversion_item') . " + SET " . implode($updateParts, ', ') . " WHERE idvisit = ? AND idorder = ? AND idaction_sku = ?"; - $sqlBind[] = $newRow['idvisit']; - $sqlBind[] = $item['idorder_original_value']; - $sqlBind[] = $newRow['idaction_sku']; - Piwik_Tracker::getDatabase()->query($sql, $sqlBind); - } - } - - /** - * Inserts in the cart in the DB the new items - * that were not previously in the cart - * @param $goal - * @param array $itemsToInsert - * @return - */ - protected function insertEcommerceItems($goal, $itemsToInsert) - { - if(empty($itemsToInsert)) - { - return; - } - printDebug("Ecommerce items that are added to the cart/order"); - printDebug($itemsToInsert); - - $sql = "INSERT INTO " . Piwik_Common::prefixTable('log_conversion_item') . " + $sqlBind[] = $newRow['idvisit']; + $sqlBind[] = $item['idorder_original_value']; + $sqlBind[] = $newRow['idaction_sku']; + Piwik_Tracker::getDatabase()->query($sql, $sqlBind); + } + } + + /** + * Inserts in the cart in the DB the new items + * that were not previously in the cart + * @param $goal + * @param array $itemsToInsert + * @return + */ + protected function insertEcommerceItems($goal, $itemsToInsert) + { + if (empty($itemsToInsert)) { + return; + } + printDebug("Ecommerce items that are added to the cart/order"); + printDebug($itemsToInsert); + + $sql = "INSERT INTO " . Piwik_Common::prefixTable('log_conversion_item') . " (idaction_sku, idaction_name, idaction_category, idaction_category2, idaction_category3, idaction_category4, idaction_category5, price, quantity, deleted, idorder, idsite, idvisitor, server_time, idvisit) VALUES "; - $i = 0; - $bind = array(); - foreach($itemsToInsert as $item) - { - if($i > 0) { $sql .= ','; } - $newRow = array_values($this->getItemRowEnriched($goal, $item)); - $sql .= " ( ". Piwik_Common::getSqlStringFieldsArray($newRow) . " ) "; - $i++; - $bind = array_merge($bind, $newRow); - } - Piwik_Tracker::getDatabase()->query($sql, $bind); - printDebug($sql);printDebug($bind); - } - - protected function getItemRowEnriched($goal, $item) - { - $newRow = array( - 'idaction_sku' => (int)$item[self::INTERNAL_ITEM_SKU], - 'idaction_name' => (int)$item[self::INTERNAL_ITEM_NAME], - 'idaction_category' => (int)$item[self::INTERNAL_ITEM_CATEGORY], - 'idaction_category2' => (int)$item[self::INTERNAL_ITEM_CATEGORY2], - 'idaction_category3' => (int)$item[self::INTERNAL_ITEM_CATEGORY3], - 'idaction_category4' => (int)$item[self::INTERNAL_ITEM_CATEGORY4], - 'idaction_category5' => (int)$item[self::INTERNAL_ITEM_CATEGORY5], - 'price' => $item[self::INTERNAL_ITEM_PRICE], - 'quantity' => $item[self::INTERNAL_ITEM_QUANTITY], - 'deleted' => isset($item['deleted']) ? $item['deleted'] : 0, //deleted - 'idorder' => isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, //idorder = 0 in log_conversion_item for carts - 'idsite' => $goal['idsite'], - 'idvisitor' => $goal['idvisitor'], - 'server_time' => $goal['server_time'], - 'idvisit' => $goal['idvisit'] - ); - return $newRow; - } - - /** - * Records a standard non-Ecommerce goal in the DB (URL/Title matching), - * linking the conversion to the action that triggered it - * @param $goal - * @param Piwik_Tracker_Action $action - * @param $visitorInformation - */ - protected function recordStandardGoals($goal, $action, $visitorInformation) - { - foreach($this->convertedGoals as $convertedGoal) - { - printDebug("- Goal ".$convertedGoal['idgoal'] ." matched. Recording..."); - $newGoal = $goal; - $newGoal['idgoal'] = $convertedGoal['idgoal']; - $newGoal['url'] = $convertedGoal['url']; - $newGoal['revenue'] = $this->getRevenue($convertedGoal['revenue']); - - if(!is_null($action)) - { - $newGoal['idaction_url'] = $action->getIdActionUrl(); - $newGoal['idlink_va'] = $action->getIdLinkVisitAction(); - } - - // If multiple Goal conversions per visit, set a cache buster - $newGoal['buster'] = $convertedGoal['allow_multiple'] == 0 - ? '0' - : $visitorInformation['visit_last_action_time']; - - $this->recordGoal($newGoal); - - Piwik_PostEvent('Tracker.recordStandardGoals', $newGoal); - } - } - /** - * Helper function used by other record* methods which will INSERT or UPDATE the conversion in the DB - * - * @param array $newGoal - * @param bool $mustUpdateNotInsert If set to true, the previous conversion will be UPDATEd. This is used for the Cart Update conversion (only one cart per visit) - * @param array $updateWhere - * @return bool - */ - protected function recordGoal($newGoal, $mustUpdateNotInsert = false, $updateWhere = array()) - { - $newGoalDebug = $newGoal; - $newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']); - printDebug($newGoalDebug); - - $fields = implode(", ", array_keys($newGoal)); - $bindFields = Piwik_Common::getSqlStringFieldsArray($newGoal); - - if($mustUpdateNotInsert) - { - $updateParts = $sqlBind = $updateWhereParts = array(); - foreach($newGoal AS $name => $value) - { - $updateParts[] = $name." = ?"; - $sqlBind[] = $value; - } - foreach($updateWhere as $name => $value) - { - $updateWhereParts[] = $name." = ?"; - $sqlBind[] = $value; - } - $sql = 'UPDATE ' . Piwik_Common::prefixTable('log_conversion') . " - SET ".implode($updateParts, ', ')." - WHERE ".implode($updateWhereParts, ' AND '); - Piwik_Tracker::getDatabase()->query($sql, $sqlBind); - return true; - } - else - { - $sql = 'INSERT IGNORE INTO ' . Piwik_Common::prefixTable('log_conversion') . " + $i = 0; + $bind = array(); + foreach ($itemsToInsert as $item) { + if ($i > 0) { + $sql .= ','; + } + $newRow = array_values($this->getItemRowEnriched($goal, $item)); + $sql .= " ( " . Piwik_Common::getSqlStringFieldsArray($newRow) . " ) "; + $i++; + $bind = array_merge($bind, $newRow); + } + Piwik_Tracker::getDatabase()->query($sql, $bind); + printDebug($sql); + printDebug($bind); + } + + protected function getItemRowEnriched($goal, $item) + { + $newRow = array( + 'idaction_sku' => (int)$item[self::INTERNAL_ITEM_SKU], + 'idaction_name' => (int)$item[self::INTERNAL_ITEM_NAME], + 'idaction_category' => (int)$item[self::INTERNAL_ITEM_CATEGORY], + 'idaction_category2' => (int)$item[self::INTERNAL_ITEM_CATEGORY2], + 'idaction_category3' => (int)$item[self::INTERNAL_ITEM_CATEGORY3], + 'idaction_category4' => (int)$item[self::INTERNAL_ITEM_CATEGORY4], + 'idaction_category5' => (int)$item[self::INTERNAL_ITEM_CATEGORY5], + 'price' => $item[self::INTERNAL_ITEM_PRICE], + 'quantity' => $item[self::INTERNAL_ITEM_QUANTITY], + 'deleted' => isset($item['deleted']) ? $item['deleted'] : 0, //deleted + 'idorder' => isset($goal['idorder']) ? $goal['idorder'] : self::ITEM_IDORDER_ABANDONED_CART, //idorder = 0 in log_conversion_item for carts + 'idsite' => $goal['idsite'], + 'idvisitor' => $goal['idvisitor'], + 'server_time' => $goal['server_time'], + 'idvisit' => $goal['idvisit'] + ); + return $newRow; + } + + /** + * Records a standard non-Ecommerce goal in the DB (URL/Title matching), + * linking the conversion to the action that triggered it + * @param $goal + * @param Piwik_Tracker_Action $action + * @param $visitorInformation + */ + protected function recordStandardGoals($goal, $action, $visitorInformation) + { + foreach ($this->convertedGoals as $convertedGoal) { + printDebug("- Goal " . $convertedGoal['idgoal'] . " matched. Recording..."); + $newGoal = $goal; + $newGoal['idgoal'] = $convertedGoal['idgoal']; + $newGoal['url'] = $convertedGoal['url']; + $newGoal['revenue'] = $this->getRevenue($convertedGoal['revenue']); + + if (!is_null($action)) { + $newGoal['idaction_url'] = $action->getIdActionUrl(); + $newGoal['idlink_va'] = $action->getIdLinkVisitAction(); + } + + // If multiple Goal conversions per visit, set a cache buster + $newGoal['buster'] = $convertedGoal['allow_multiple'] == 0 + ? '0' + : $visitorInformation['visit_last_action_time']; + + $this->recordGoal($newGoal); + + Piwik_PostEvent('Tracker.recordStandardGoals', $newGoal); + } + } + + /** + * Helper function used by other record* methods which will INSERT or UPDATE the conversion in the DB + * + * @param array $newGoal + * @param bool $mustUpdateNotInsert If set to true, the previous conversion will be UPDATEd. This is used for the Cart Update conversion (only one cart per visit) + * @param array $updateWhere + * @return bool + */ + protected function recordGoal($newGoal, $mustUpdateNotInsert = false, $updateWhere = array()) + { + $newGoalDebug = $newGoal; + $newGoalDebug['idvisitor'] = bin2hex($newGoalDebug['idvisitor']); + printDebug($newGoalDebug); + + $fields = implode(", ", array_keys($newGoal)); + $bindFields = Piwik_Common::getSqlStringFieldsArray($newGoal); + + if ($mustUpdateNotInsert) { + $updateParts = $sqlBind = $updateWhereParts = array(); + foreach ($newGoal AS $name => $value) { + $updateParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + foreach ($updateWhere as $name => $value) { + $updateWhereParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + $sql = 'UPDATE ' . Piwik_Common::prefixTable('log_conversion') . " + SET " . implode($updateParts, ', ') . " + WHERE " . implode($updateWhereParts, ' AND '); + Piwik_Tracker::getDatabase()->query($sql, $sqlBind); + return true; + } else { + $sql = 'INSERT IGNORE INTO ' . Piwik_Common::prefixTable('log_conversion') . " ($fields) VALUES ($bindFields) "; - $bind = array_values($newGoal); - $result = Piwik_Tracker::getDatabase()->query($sql, $bind); - - // If a record was inserted, we return true - return Piwik_Tracker::getDatabase()->rowCount($result) > 0; - } - } - - /** - * Casts the item array so that array comparisons work nicely - * @param array $row - * @return array - */ - protected function getItemRowCast($row) - { - return array( - (string)(int)$row[self::INTERNAL_ITEM_SKU], - (string)(int)$row[self::INTERNAL_ITEM_NAME], - (string)(int)$row[self::INTERNAL_ITEM_CATEGORY], - (string)(int)$row[self::INTERNAL_ITEM_CATEGORY2], - (string)(int)$row[self::INTERNAL_ITEM_CATEGORY3], - (string)(int)$row[self::INTERNAL_ITEM_CATEGORY4], - (string)(int)$row[self::INTERNAL_ITEM_CATEGORY5], - (string)$row[self::INTERNAL_ITEM_PRICE], - (string)$row[self::INTERNAL_ITEM_QUANTITY], - ); - } + $bind = array_values($newGoal); + $result = Piwik_Tracker::getDatabase()->query($sql, $bind); + + // If a record was inserted, we return true + return Piwik_Tracker::getDatabase()->rowCount($result) > 0; + } + } + + /** + * Casts the item array so that array comparisons work nicely + * @param array $row + * @return array + */ + protected function getItemRowCast($row) + { + return array( + (string)(int)$row[self::INTERNAL_ITEM_SKU], + (string)(int)$row[self::INTERNAL_ITEM_NAME], + (string)(int)$row[self::INTERNAL_ITEM_CATEGORY], + (string)(int)$row[self::INTERNAL_ITEM_CATEGORY2], + (string)(int)$row[self::INTERNAL_ITEM_CATEGORY3], + (string)(int)$row[self::INTERNAL_ITEM_CATEGORY4], + (string)(int)$row[self::INTERNAL_ITEM_CATEGORY5], + (string)$row[self::INTERNAL_ITEM_PRICE], + (string)$row[self::INTERNAL_ITEM_QUANTITY], + ); + } } diff --git a/core/Tracker/IgnoreCookie.php b/core/Tracker/IgnoreCookie.php index 510e21a042..3819fa1e15 100644 --- a/core/Tracker/IgnoreCookie.php +++ b/core/Tracker/IgnoreCookie.php @@ -1,76 +1,73 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Tracking cookies. - * + * * @package Piwik * @subpackage Piwik_Tracker */ class Piwik_Tracker_IgnoreCookie { - /** - * Get tracking cookie - * - * @return Piwik_Cookie - */ - static public function getTrackingCookie() - { - $cookie_name = @Piwik_Config::getInstance()->Tracker['cookie_name']; - $cookie_path = @Piwik_Config::getInstance()->Tracker['cookie_path']; + /** + * Get tracking cookie + * + * @return Piwik_Cookie + */ + static public function getTrackingCookie() + { + $cookie_name = @Piwik_Config::getInstance()->Tracker['cookie_name']; + $cookie_path = @Piwik_Config::getInstance()->Tracker['cookie_path']; - return new Piwik_Cookie($cookie_name, null, $cookie_path); - } + return new Piwik_Cookie($cookie_name, null, $cookie_path); + } - /** - * Get ignore (visit) cookie - * - * @return Piwik_Cookie - */ - static public function getIgnoreCookie() - { - $cookie_name = @Piwik_Config::getInstance()->Tracker['ignore_visits_cookie_name']; - $cookie_path = @Piwik_Config::getInstance()->Tracker['cookie_path']; + /** + * Get ignore (visit) cookie + * + * @return Piwik_Cookie + */ + static public function getIgnoreCookie() + { + $cookie_name = @Piwik_Config::getInstance()->Tracker['ignore_visits_cookie_name']; + $cookie_path = @Piwik_Config::getInstance()->Tracker['cookie_path']; - return new Piwik_Cookie($cookie_name, null, $cookie_path); - } + return new Piwik_Cookie($cookie_name, null, $cookie_path); + } - /** - * Set ignore (visit) cookie or deletes it if already present - */ - static public function setIgnoreCookie() - { - $ignoreCookie = self::getIgnoreCookie(); - if($ignoreCookie->isCookieFound()) - { - $ignoreCookie->delete(); - } - else - { - $ignoreCookie->set('ignore', '*'); - $ignoreCookie->save(); + /** + * Set ignore (visit) cookie or deletes it if already present + */ + static public function setIgnoreCookie() + { + $ignoreCookie = self::getIgnoreCookie(); + if ($ignoreCookie->isCookieFound()) { + $ignoreCookie->delete(); + } else { + $ignoreCookie->set('ignore', '*'); + $ignoreCookie->save(); - $trackingCookie = self::getTrackingCookie(); - $trackingCookie->delete(); - } - } + $trackingCookie = self::getTrackingCookie(); + $trackingCookie->delete(); + } + } - /** - * Returns true if ignore (visit) cookie is present - * - * @return bool True if ignore cookie found; false otherwise - */ - static public function isIgnoreCookieFound() - { - $cookie = self::getIgnoreCookie(); - return $cookie->isCookieFound() && $cookie->get('ignore') === '*'; - } + /** + * Returns true if ignore (visit) cookie is present + * + * @return bool True if ignore cookie found; false otherwise + */ + static public function isIgnoreCookieFound() + { + $cookie = self::getIgnoreCookie(); + return $cookie->isCookieFound() && $cookie->get('ignore') === '*'; + } } diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php index ef8b9e3cee..c4a661ec62 100644 --- a/core/Tracker/Visit.php +++ b/core/Tracker/Visit.php @@ -13,9 +13,11 @@ * @package Piwik * @subpackage Piwik_Tracker */ -interface Piwik_Tracker_Visit_Interface { - function setRequest($requestArray); - function handle(); +interface Piwik_Tracker_Visit_Interface +{ + function setRequest($requestArray); + + function handle(); } /** @@ -34,12 +36,12 @@ interface Piwik_Tracker_Visit_Interface { */ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface { - const UNKNOWN_CODE = 'xx'; - - /** - * @var Piwik_Cookie - */ - protected $cookie = null; + const UNKNOWN_CODE = 'xx'; + + /** + * @var Piwik_Cookie + */ + protected $cookie = null; protected $visitorInfo = array(); protected $userSettingsInformation = null; protected $visitorCustomVariables = array(); @@ -53,722 +55,673 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface protected $ip; protected $authenticated = false; - // Set to true when we set some custom variables from the cookie - protected $customVariablesSetFromRequest = false; - - /** - * @var Piwik_Tracker_GoalManager - */ - protected $goalManager; - - public function __construct($forcedIpString = null, $forcedDateTime = null, $authenticated = false) - { - $this->timestamp = time(); - if(!empty($forcedDateTime)) - { - if(!is_numeric($forcedDateTime)) - { - $forcedDateTime = strtotime($forcedDateTime); - } - $this->timestamp = $forcedDateTime; - } - $ipString = $forcedIpString; - if(empty($ipString)) - { - $ipString = Piwik_IP::getIpFromHeader(); - } - - $ip = Piwik_IP::P2N($ipString); - $this->ip = $ip; - - $this->authenticated = $authenticated; - } - - function setForcedVisitorId($visitorId) - { - $this->forcedVisitorId = $visitorId; - } - - function setRequest($requestArray) - { - $this->request = $requestArray; - - $idsite = Piwik_Common::getRequestVar('idsite', 0, 'int', $this->request); - Piwik_PostEvent('Tracker.setRequest.idSite', $idsite, $requestArray); - if($idsite <= 0) - { - throw new Exception('Invalid idSite'); - } - $this->idsite = $idsite; - - // When the 'url' and referer url parameter are not given, we might be in the 'Simple Image Tracker' mode. - // The URL can default to the Referer, which will be in this case - // the URL of the page containing the Simple Image beacon - if(empty($this->request['urlref']) - && empty($this->request['url'])) - { - $this->request['url'] = @$_SERVER['HTTP_REFERER']; - } - } - - /** - * Main algorithm to handle the visit. - * - * Once we have the visitor information, we have to determine if the visit is a new or a known visit. - * - * 1) When the last action was done more than 30min ago, - * or if the visitor is new, then this is a new visit. - * - * 2) If the last action is less than 30min ago, then the same visit is going on. - * Because the visit goes on, we can get the time spent during the last action. - * - * NB: - * - In the case of a new visit, then the time spent - * during the last action of the previous visit is unknown. - * - * - In the case of a new visit but with a known visitor, - * we can set the 'returning visitor' flag. - * - * In all the cases we set a cookie to the visitor with the new information. - */ - public function handle() - { - // the IP is needed by isExcluded() and GoalManager->recordGoals() - $this->visitorInfo['location_ip'] = $this->ip; - - if($this->isExcluded()) - { - return; - } - - // Anonymize IP (after testing for IP exclusion) - $ip = $this->ip; - Piwik_PostEvent('Tracker.Visit.setVisitorIp', $ip); - $this->visitorInfo['location_ip'] = $ip; - - $this->visitorCustomVariables = self::getCustomVariables($scope = 'visit', $this->request); - if(!empty($this->visitorCustomVariables)) - { - printDebug("Visit level Custom Variables: "); - printDebug($this->visitorCustomVariables); - $this->customVariablesSetFromRequest = true; - } - - $this->goalManager = new Piwik_Tracker_GoalManager(); - - $someGoalsConverted = $visitIsConverted = false; - $idActionUrl = $idActionName = $actionType = false; - $action = null; - - $this->goalManager->init($this->request); - - $requestIsManualGoalConversion = ($this->goalManager->idGoal > 0); - $requestIsEcommerce = $this->goalManager->requestIsEcommerce; - if($requestIsEcommerce) - { - $someGoalsConverted = true; - - // Mark the visit as Converted only if it is an order (not for a Cart update) - if($this->goalManager->isGoalAnOrder) - { - $visitIsConverted = true; - } - } - // this request is from the JS call to piwikTracker.trackGoal() - elseif($requestIsManualGoalConversion) - { - $someGoalsConverted = $this->goalManager->detectGoalId($this->idsite); - $visitIsConverted = $someGoalsConverted; - // 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 = '.$this->goalManager->idGoal); - unset($this->goalManager); - return; - } - } - // normal page view, potentially triggering a URL matching goal - else - { - $action = $this->newAction(); - $this->handleAction($action); - $someGoalsConverted = $this->goalManager->detectGoalsMatchingUrl($this->idsite, $action); - $visitIsConverted = $someGoalsConverted; - - $action->loadIdActionNameAndUrl(); - $idActionUrl = $action->getIdActionUrl(); - if ($idActionUrl !== null) { - $idActionUrl = (int)$idActionUrl; - } - $idActionName = (int)$action->getIdActionName(); - $actionType = $action->getActionType(); - } - - // the visitor and session - $this->recognizeTheVisitor(); - - $isLastActionInTheSameVisit = $this->isLastActionInTheSameVisit(); - - if(!$isLastActionInTheSameVisit) - { - printDebug("Visitor detected, but last action was more than 30 minutes ago..."); - } - // 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() - // ) - // AND - // - the last page view for this visitor was less than 30 minutes ago @see isLastActionInTheSameVisit() - if( $this->isVisitorKnown() - && $isLastActionInTheSameVisit) - { - $idRefererActionUrl = $this->visitorInfo['visit_exit_idaction_url']; - $idRefererActionName = $this->visitorInfo['visit_exit_idaction_name']; - try { - $this->handleKnownVisit($idActionUrl, $idActionName, $actionType, $visitIsConverted); - if(!is_null($action)) - { - $action->record( $this->visitorInfo['idvisit'], - $this->visitorInfo['idvisitor'], - $idRefererActionUrl, - $idRefererActionName, - $this->visitorInfo['time_spent_ref_action'] - ); - } - } catch(Piwik_Tracker_Visit_VisitorNotFoundInDatabase $e) { - - // There is an edge case when: - // - two manual goal conversions happen in the same second - // - which result in handleKnownVisit throwing the exception - // because the UPDATE didn't affect any rows (one row was found, but not updated since no field changed) - // - the exception is caught here and will result in a new visit incorrectly - // In this case, we cancel the current conversion to be recorded: - if($requestIsManualGoalConversion - || $requestIsEcommerce) - { - $someGoalsConverted = $visitIsConverted = false; - } - // When the row wasn't found in the logs, and this is a pageview or - // goal matching URL, we force a new visitor - else - { - $this->visitorKnown = false; - } - } - } - - // New visit when: - // - the visitor has the Piwik cookie but the last action was performed more than 30 min ago @see isLastActionInTheSameVisit() - // - the visitor doesn't have the Piwik cookie, and couldn't be matched in @see recognizeTheVisitor() - // - the visitor does have the Piwik cookie but the idcookie and idvisit found in the cookie didn't match to any existing visit in the DB - if(!$this->isVisitorKnown() - || !$isLastActionInTheSameVisit) - { - $this->handleNewVisit($idActionUrl, $idActionName, $actionType, $visitIsConverted); - if(!is_null($action)) - { - $action->record( $this->visitorInfo['idvisit'], $this->visitorInfo['idvisitor'], 0, 0, 0 ); - } - } - - // update the cookie with the new visit information - $this->setThirdPartyCookie(); - - // record the goals if applicable - if($someGoalsConverted) - { - $refererTimestamp = Piwik_Common::getRequestVar('_refts', 0, 'int', $this->request); - $refererUrl = Piwik_Common::getRequestVar('_ref', '', 'string', $this->request); - $refererCampaignName = trim(urldecode(Piwik_Common::getRequestVar('_rcn', '', 'string', $this->request))); - $refererCampaignKeyword = trim(urldecode(Piwik_Common::getRequestVar('_rck', '', 'string', $this->request))); - - $this->goalManager->recordGoals( - $this->idsite, - $this->visitorInfo, - $this->visitorCustomVariables, - $action, - $refererTimestamp, - $refererUrl, - $refererCampaignName, - $refererCampaignKeyword, - $this->getBrowserLanguage() - ); - } - unset($this->goalManager); - unset($action); - $this->printCookie(); - } - - protected function printCookie() - { - printDebug($this->cookie); - } - - protected function handleAction($action) - { - $action->setIdSite($this->idsite); - $action->setRequest($this->request); - $action->setTimestamp($this->getCurrentTimestamp()); - $action->init(); - - if($this->detectActionIsOutlinkOnAliasHost($action)) - { - printDebug("Info: The outlink URL host is one of the known host for this website. "); - } - if(isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) - { - $type = Piwik_Tracker_Action::getActionTypeName($action->getActionType()); - printDebug("Action is a $type, - Action name = ". $action->getActionName() .", - Action URL = ". $action->getActionUrl() ); - } - } - - /** - * In the case of a known visit, we have to do the following actions: - * - * 1) Insert the new action - * 2) Update the visit information - * - * This method triggers two events: - * - * Tracker.knownVisitorUpdate is triggered before the visit information is updated - * Event data is an array with the values to be updated (could be changed by plugins) - * - * Tracker.knownVisitorInformation is triggered after saving the new visit data - * Even data is an array with updated information about the visit - * @param $idActionUrl - * @param $idActionName - * @param $actionType - * @param $visitIsConverted - * @throws Piwik_Tracker_Visit_VisitorNotFoundInDatabase - */ - protected function handleKnownVisit($idActionUrl, $idActionName, $actionType, $visitIsConverted) - { - // gather information that needs to be updated - $valuesToUpdate = array(); - $incrementActions = false; - $sqlActionUpdate = ''; - - if(!empty($idActionName)) - { - $valuesToUpdate['visit_exit_idaction_name'] = (int)$idActionName; - } - if($idActionUrl !== false) - { - $valuesToUpdate['visit_exit_idaction_url'] = $idActionUrl; - $incrementActions = true; - } - if($actionType == Piwik_Tracker_Action::TYPE_SITE_SEARCH) - { - $sqlActionUpdate .= "visit_total_searches = visit_total_searches + 1, "; - $incrementActions = true; - } - if($incrementActions) - { - $sqlActionUpdate .= "visit_total_actions = visit_total_actions + 1, "; - } - - $datetimeServer = Piwik_Tracker::getDatetimeFromTimestamp($this->getCurrentTimestamp()); - printDebug("Visit is known (IP = ".Piwik_IP::N2P($this->getVisitorIp()).")"); + // Set to true when we set some custom variables from the cookie + protected $customVariablesSetFromRequest = false; + + /** + * @var Piwik_Tracker_GoalManager + */ + protected $goalManager; + + public function __construct($forcedIpString = null, $forcedDateTime = null, $authenticated = false) + { + $this->timestamp = time(); + if (!empty($forcedDateTime)) { + if (!is_numeric($forcedDateTime)) { + $forcedDateTime = strtotime($forcedDateTime); + } + $this->timestamp = $forcedDateTime; + } + $ipString = $forcedIpString; + if (empty($ipString)) { + $ipString = Piwik_IP::getIpFromHeader(); + } + + $ip = Piwik_IP::P2N($ipString); + $this->ip = $ip; + + $this->authenticated = $authenticated; + } + + function setForcedVisitorId($visitorId) + { + $this->forcedVisitorId = $visitorId; + } + + function setRequest($requestArray) + { + $this->request = $requestArray; + + $idsite = Piwik_Common::getRequestVar('idsite', 0, 'int', $this->request); + Piwik_PostEvent('Tracker.setRequest.idSite', $idsite, $requestArray); + if ($idsite <= 0) { + throw new Exception('Invalid idSite'); + } + $this->idsite = $idsite; + + // When the 'url' and referer url parameter are not given, we might be in the 'Simple Image Tracker' mode. + // The URL can default to the Referer, which will be in this case + // the URL of the page containing the Simple Image beacon + if (empty($this->request['urlref']) + && empty($this->request['url']) + ) { + $this->request['url'] = @$_SERVER['HTTP_REFERER']; + } + } + + /** + * Main algorithm to handle the visit. + * + * Once we have the visitor information, we have to determine if the visit is a new or a known visit. + * + * 1) When the last action was done more than 30min ago, + * or if the visitor is new, then this is a new visit. + * + * 2) If the last action is less than 30min ago, then the same visit is going on. + * Because the visit goes on, we can get the time spent during the last action. + * + * NB: + * - In the case of a new visit, then the time spent + * during the last action of the previous visit is unknown. + * + * - In the case of a new visit but with a known visitor, + * we can set the 'returning visitor' flag. + * + * In all the cases we set a cookie to the visitor with the new information. + */ + public function handle() + { + // the IP is needed by isExcluded() and GoalManager->recordGoals() + $this->visitorInfo['location_ip'] = $this->ip; + + if ($this->isExcluded()) { + return; + } + + // Anonymize IP (after testing for IP exclusion) + $ip = $this->ip; + Piwik_PostEvent('Tracker.Visit.setVisitorIp', $ip); + $this->visitorInfo['location_ip'] = $ip; + + $this->visitorCustomVariables = self::getCustomVariables($scope = 'visit', $this->request); + if (!empty($this->visitorCustomVariables)) { + printDebug("Visit level Custom Variables: "); + printDebug($this->visitorCustomVariables); + $this->customVariablesSetFromRequest = true; + } + + $this->goalManager = new Piwik_Tracker_GoalManager(); + + $someGoalsConverted = $visitIsConverted = false; + $idActionUrl = $idActionName = $actionType = false; + $action = null; + + $this->goalManager->init($this->request); + + $requestIsManualGoalConversion = ($this->goalManager->idGoal > 0); + $requestIsEcommerce = $this->goalManager->requestIsEcommerce; + if ($requestIsEcommerce) { + $someGoalsConverted = true; + + // Mark the visit as Converted only if it is an order (not for a Cart update) + if ($this->goalManager->isGoalAnOrder) { + $visitIsConverted = true; + } + } // this request is from the JS call to piwikTracker.trackGoal() + elseif ($requestIsManualGoalConversion) { + $someGoalsConverted = $this->goalManager->detectGoalId($this->idsite); + $visitIsConverted = $someGoalsConverted; + // 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 = ' . $this->goalManager->idGoal); + unset($this->goalManager); + return; + } + } // normal page view, potentially triggering a URL matching goal + else { + $action = $this->newAction(); + $this->handleAction($action); + $someGoalsConverted = $this->goalManager->detectGoalsMatchingUrl($this->idsite, $action); + $visitIsConverted = $someGoalsConverted; + + $action->loadIdActionNameAndUrl(); + $idActionUrl = $action->getIdActionUrl(); + if ($idActionUrl !== null) { + $idActionUrl = (int)$idActionUrl; + } + $idActionName = (int)$action->getIdActionName(); + $actionType = $action->getActionType(); + } + + // the visitor and session + $this->recognizeTheVisitor(); + + $isLastActionInTheSameVisit = $this->isLastActionInTheSameVisit(); + + if (!$isLastActionInTheSameVisit) { + printDebug("Visitor detected, but last action was more than 30 minutes ago..."); + } + // 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() + // ) + // AND + // - the last page view for this visitor was less than 30 minutes ago @see isLastActionInTheSameVisit() + if ($this->isVisitorKnown() + && $isLastActionInTheSameVisit + ) { + $idRefererActionUrl = $this->visitorInfo['visit_exit_idaction_url']; + $idRefererActionName = $this->visitorInfo['visit_exit_idaction_name']; + try { + $this->handleKnownVisit($idActionUrl, $idActionName, $actionType, $visitIsConverted); + if (!is_null($action)) { + $action->record($this->visitorInfo['idvisit'], + $this->visitorInfo['idvisitor'], + $idRefererActionUrl, + $idRefererActionName, + $this->visitorInfo['time_spent_ref_action'] + ); + } + } catch (Piwik_Tracker_Visit_VisitorNotFoundInDatabase $e) { + + // There is an edge case when: + // - two manual goal conversions happen in the same second + // - which result in handleKnownVisit throwing the exception + // because the UPDATE didn't affect any rows (one row was found, but not updated since no field changed) + // - the exception is caught here and will result in a new visit incorrectly + // In this case, we cancel the current conversion to be recorded: + if ($requestIsManualGoalConversion + || $requestIsEcommerce + ) { + $someGoalsConverted = $visitIsConverted = false; + } // When the row wasn't found in the logs, and this is a pageview or + // goal matching URL, we force a new visitor + else { + $this->visitorKnown = false; + } + } + } + + // New visit when: + // - the visitor has the Piwik cookie but the last action was performed more than 30 min ago @see isLastActionInTheSameVisit() + // - the visitor doesn't have the Piwik cookie, and couldn't be matched in @see recognizeTheVisitor() + // - the visitor does have the Piwik cookie but the idcookie and idvisit found in the cookie didn't match to any existing visit in the DB + if (!$this->isVisitorKnown() + || !$isLastActionInTheSameVisit + ) { + $this->handleNewVisit($idActionUrl, $idActionName, $actionType, $visitIsConverted); + if (!is_null($action)) { + $action->record($this->visitorInfo['idvisit'], $this->visitorInfo['idvisitor'], 0, 0, 0); + } + } + + // update the cookie with the new visit information + $this->setThirdPartyCookie(); + + // record the goals if applicable + if ($someGoalsConverted) { + $refererTimestamp = Piwik_Common::getRequestVar('_refts', 0, 'int', $this->request); + $refererUrl = Piwik_Common::getRequestVar('_ref', '', 'string', $this->request); + $refererCampaignName = trim(urldecode(Piwik_Common::getRequestVar('_rcn', '', 'string', $this->request))); + $refererCampaignKeyword = trim(urldecode(Piwik_Common::getRequestVar('_rck', '', 'string', $this->request))); + + $this->goalManager->recordGoals( + $this->idsite, + $this->visitorInfo, + $this->visitorCustomVariables, + $action, + $refererTimestamp, + $refererUrl, + $refererCampaignName, + $refererCampaignKeyword, + $this->getBrowserLanguage() + ); + } + unset($this->goalManager); + unset($action); + $this->printCookie(); + } + + protected function printCookie() + { + printDebug($this->cookie); + } + + protected function handleAction($action) + { + $action->setIdSite($this->idsite); + $action->setRequest($this->request); + $action->setTimestamp($this->getCurrentTimestamp()); + $action->init(); + + if ($this->detectActionIsOutlinkOnAliasHost($action)) { + printDebug("Info: The outlink URL host is one of the known host for this website. "); + } + if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) { + $type = Piwik_Tracker_Action::getActionTypeName($action->getActionType()); + printDebug("Action is a $type, + Action name = " . $action->getActionName() . ", + Action URL = " . $action->getActionUrl()); + } + } + + /** + * In the case of a known visit, we have to do the following actions: + * + * 1) Insert the new action + * 2) Update the visit information + * + * This method triggers two events: + * + * Tracker.knownVisitorUpdate is triggered before the visit information is updated + * Event data is an array with the values to be updated (could be changed by plugins) + * + * Tracker.knownVisitorInformation is triggered after saving the new visit data + * Even data is an array with updated information about the visit + * @param $idActionUrl + * @param $idActionName + * @param $actionType + * @param $visitIsConverted + * @throws Piwik_Tracker_Visit_VisitorNotFoundInDatabase + */ + protected function handleKnownVisit($idActionUrl, $idActionName, $actionType, $visitIsConverted) + { + // gather information that needs to be updated + $valuesToUpdate = array(); + $incrementActions = false; + $sqlActionUpdate = ''; + + if (!empty($idActionName)) { + $valuesToUpdate['visit_exit_idaction_name'] = (int)$idActionName; + } + if ($idActionUrl !== false) { + $valuesToUpdate['visit_exit_idaction_url'] = $idActionUrl; + $incrementActions = true; + } + if ($actionType == Piwik_Tracker_Action::TYPE_SITE_SEARCH) { + $sqlActionUpdate .= "visit_total_searches = visit_total_searches + 1, "; + $incrementActions = true; + } + if ($incrementActions) { + $sqlActionUpdate .= "visit_total_actions = visit_total_actions + 1, "; + } + + $datetimeServer = Piwik_Tracker::getDatetimeFromTimestamp($this->getCurrentTimestamp()); + printDebug("Visit is known (IP = " . Piwik_IP::N2P($this->getVisitorIp()) . ")"); // Add 1 so it's always > 0 - $visitTotalTime = 1 + $this->getCurrentTimestamp() - $this->visitorInfo['visit_first_action_time']; - $valuesToUpdate['visit_last_action_time'] = $datetimeServer; - $valuesToUpdate['visit_total_time'] = self::cleanupVisitTotalTime($visitTotalTime); - - // Goal conversion - if($visitIsConverted) - { - $valuesToUpdate['visit_goal_converted'] = 1; - // If a pageview and goal conversion in the same second, with previously a goal conversion recorded - // the request would not "update" the row since all values are the same as previous - // therefore the request below throws exception, instead we make sure the UPDATE will affect the row - $valuesToUpdate['visit_total_time'] = self::cleanupVisitTotalTime( - $valuesToUpdate['visit_total_time'] - + $this->goalManager->idGoal - // +2 to offset idgoal=-1 and idgoal=0 - + 2 ); - } - - // Might update the idvisitor when it was forced or overwritten for this visit - if(strlen($this->visitorInfo['idvisitor']) == Piwik_Tracker::LENGTH_BINARY_ID) - { - $valuesToUpdate['idvisitor'] = $this->visitorInfo['idvisitor']; - } - - // Ecommerce buyer status - $valuesToUpdate['visit_goal_buyer'] = $this->goalManager->getBuyerType($this->visitorInfo['visit_goal_buyer']); - - // Custom Variables overwrite previous values on each page view - $valuesToUpdate = array_merge($valuesToUpdate, $this->visitorCustomVariables); - - // trigger event before update - Piwik_PostEvent('Tracker.knownVisitorUpdate', $valuesToUpdate); - - // Will be updated in cookie - $timeSpentRefererAction = $this->getCurrentTimestamp() - $this->visitorInfo['visit_last_action_time']; - if($timeSpentRefererAction > Piwik_Config::getInstance()->Tracker['visit_standard_length']) - { - $timeSpentRefererAction = 0; - } - $this->visitorInfo['time_spent_ref_action'] = $timeSpentRefererAction; - - // update visitorInfo - foreach($valuesToUpdate AS $name => $value) - { - $this->visitorInfo[$name] = $value; - } - - // build sql query - $updateParts = $sqlBind = array(); - - foreach($valuesToUpdate AS $name => $value) - { - $updateParts[] = $name." = ?"; - $sqlBind[] = $value; - } - $sqlQuery = "UPDATE ". Piwik_Common::prefixTable('log_visit')." - SET $sqlActionUpdate ".implode($updateParts, ', ')." + $visitTotalTime = 1 + $this->getCurrentTimestamp() - $this->visitorInfo['visit_first_action_time']; + $valuesToUpdate['visit_last_action_time'] = $datetimeServer; + $valuesToUpdate['visit_total_time'] = self::cleanupVisitTotalTime($visitTotalTime); + + // Goal conversion + if ($visitIsConverted) { + $valuesToUpdate['visit_goal_converted'] = 1; + // If a pageview and goal conversion in the same second, with previously a goal conversion recorded + // the request would not "update" the row since all values are the same as previous + // therefore the request below throws exception, instead we make sure the UPDATE will affect the row + $valuesToUpdate['visit_total_time'] = self::cleanupVisitTotalTime( + $valuesToUpdate['visit_total_time'] + + $this->goalManager->idGoal + // +2 to offset idgoal=-1 and idgoal=0 + + 2); + } + + // Might update the idvisitor when it was forced or overwritten for this visit + if (strlen($this->visitorInfo['idvisitor']) == Piwik_Tracker::LENGTH_BINARY_ID) { + $valuesToUpdate['idvisitor'] = $this->visitorInfo['idvisitor']; + } + + // Ecommerce buyer status + $valuesToUpdate['visit_goal_buyer'] = $this->goalManager->getBuyerType($this->visitorInfo['visit_goal_buyer']); + + // Custom Variables overwrite previous values on each page view + $valuesToUpdate = array_merge($valuesToUpdate, $this->visitorCustomVariables); + + // trigger event before update + Piwik_PostEvent('Tracker.knownVisitorUpdate', $valuesToUpdate); + + // Will be updated in cookie + $timeSpentRefererAction = $this->getCurrentTimestamp() - $this->visitorInfo['visit_last_action_time']; + if ($timeSpentRefererAction > Piwik_Config::getInstance()->Tracker['visit_standard_length']) { + $timeSpentRefererAction = 0; + } + $this->visitorInfo['time_spent_ref_action'] = $timeSpentRefererAction; + + // update visitorInfo + foreach ($valuesToUpdate AS $name => $value) { + $this->visitorInfo[$name] = $value; + } + + // build sql query + $updateParts = $sqlBind = array(); + + foreach ($valuesToUpdate AS $name => $value) { + $updateParts[] = $name . " = ?"; + $sqlBind[] = $value; + } + $sqlQuery = "UPDATE " . Piwik_Common::prefixTable('log_visit') . " + SET $sqlActionUpdate " . implode($updateParts, ', ') . " WHERE idsite = ? AND idvisit = ?"; - array_push($sqlBind, $this->idsite, (int)$this->visitorInfo['idvisit'] ); - - $result = Piwik_Tracker::getDatabase()->query($sqlQuery, $sqlBind); - - $this->visitorInfo['visit_last_action_time'] = $this->getCurrentTimestamp(); - - // Debug output - if(isset($valuesToUpdate['idvisitor'])) - { - $valuesToUpdate['idvisitor'] = bin2hex($valuesToUpdate['idvisitor']); - } - printDebug('Updating existing visit: '. var_export($valuesToUpdate, true) ); - - if(Piwik_Tracker::getDatabase()->rowCount($result) == 0) - { - printDebug("Visitor with this idvisit wasn't found in the DB."); - printDebug("$sqlQuery --- ");printDebug($sqlBind); - throw new Piwik_Tracker_Visit_VisitorNotFoundInDatabase( - "The visitor with idvisitor=".bin2hex($this->visitorInfo['idvisitor'])." and idvisit=".$this->visitorInfo['idvisit'] - ." wasn't found in the DB, we fallback to a new visitor"); - } - - Piwik_PostEvent('Tracker.knownVisitorInformation', $this->visitorInfo); - } - - protected function isTimestampValid($time) - { - return $time <= $this->getCurrentTimestamp() - && $time > $this->getCurrentTimestamp() - 10*365*86400; - } - - /** - * In the case of a new visit, we have to do the following actions: - * - * 1) Insert the new action - * - * 2) Insert the visit information - * @param $idActionUrl - * @param $idActionName - * @param $actionType - * @param $visitIsConverted - */ - protected function handleNewVisit($idActionUrl, $idActionName, $actionType, $visitIsConverted) - { - printDebug("New Visit (IP = ".Piwik_IP::N2P($this->getVisitorIp()).")"); - - $localTimes = array( - 'h' => (string) Piwik_Common::getRequestVar( 'h', $this->getCurrentDate("H"), 'int', $this->request), - 'i' => (string) Piwik_Common::getRequestVar( 'm', $this->getCurrentDate("i"), 'int', $this->request), - 's' => (string) Piwik_Common::getRequestVar( 's', $this->getCurrentDate("s"), 'int', $this->request) - ); - foreach($localTimes as $k => $time) - { - if(strlen($time) == 1) - { - $localTimes[$k] = '0' . $time; - } - } - $localTime = $localTimes['h'] .':'. $localTimes['i'] .':'. $localTimes['s']; - - $idcookie = $this->getVisitorIdcookie(); - - $defaultTimeOnePageVisit = Piwik_Config::getInstance()->Tracker['default_time_one_page_visit']; - - // Days since first visit - $cookieFirstVisitTimestamp = Piwik_Common::getRequestVar('_idts', 0, 'int', $this->request); - if(!$this->isTimestampValid($cookieFirstVisitTimestamp)) - { - $cookieFirstVisitTimestamp = $this->getCurrentTimestamp(); - } - $daysSinceFirstVisit = round(($this->getCurrentTimestamp() - $cookieFirstVisitTimestamp)/86400, $precision = 0); - if($daysSinceFirstVisit < 0) $daysSinceFirstVisit = 0; - - // Number of Visits - $visitCount = Piwik_Common::getRequestVar('_idvc', 1, 'int', $this->request); - if($visitCount < 1) $visitCount = 1; - - // Days since last visit - $daysSinceLastVisit = 0; - $lastVisitTimestamp = Piwik_Common::getRequestVar('_viewts', 0, 'int', $this->request); - if($this->isTimestampValid($lastVisitTimestamp)) - { - $daysSinceLastVisit = round(($this->getCurrentTimestamp() - $lastVisitTimestamp)/86400, $precision = 0); - if($daysSinceLastVisit < 0) $daysSinceLastVisit = 0; - } - - $daysSinceLastOrder = 0; - $isReturningCustomer = false; - $lastOrderTimestamp = Piwik_Common::getRequestVar('_ects', 0, 'int', $this->request); - if($this->isTimestampValid($lastOrderTimestamp)) - { - $daysSinceLastOrder = round(($this->getCurrentTimestamp() - $lastOrderTimestamp)/86400, $precision = 0); - if($daysSinceLastOrder < 0) - { - $daysSinceLastOrder = 0; - } - $isReturningCustomer = true; - } - - // User settings - $userInfo = $this->getUserSettingsInformation(); - - // Referrer data - $referrer = new Piwik_Tracker_Visit_Referer(); - $refererUrl = Piwik_Common::getRequestVar( 'urlref', '', 'string', $this->request); - $currentUrl = Piwik_Common::getRequestVar( 'url', '', 'string', $this->request); - $refererInfo = $referrer->getRefererInformation($refererUrl, $currentUrl, $this->idsite); - - $visitorReturning = $isReturningCustomer - ? 2 /* Returning customer */ - : ($visitCount > 1 || $this->isVisitorKnown() || $daysSinceLastVisit > 0 - ? 1 /* Returning */ - : 0 /* New */ ); - /** - * Save the visitor - */ - $this->visitorInfo = array( - 'idsite' => $this->idsite, - 'visitor_localtime' => $localTime, - 'idvisitor' => $idcookie, - 'visitor_returning' => $visitorReturning, - 'visitor_count_visits' => $visitCount, - 'visitor_days_since_last' => $daysSinceLastVisit, - 'visitor_days_since_order' => $daysSinceLastOrder, - 'visitor_days_since_first' => $daysSinceFirstVisit, - 'visit_first_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($this->getCurrentTimestamp()), - 'visit_last_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($this->getCurrentTimestamp()), - 'visit_entry_idaction_url' => (int)$idActionUrl, - 'visit_entry_idaction_name' => (int)$idActionName, - 'visit_exit_idaction_url' => (int)$idActionUrl, - 'visit_exit_idaction_name' => (int)$idActionName, - 'visit_total_actions' => in_array($actionType, - array(Piwik_Tracker_Action::TYPE_ACTION_URL, - Piwik_Tracker_Action::TYPE_DOWNLOAD, - Piwik_Tracker_Action::TYPE_OUTLINK, - Piwik_Tracker_Action::TYPE_SITE_SEARCH)) - ? 1 : 0, // if visit starts with something else (e.g. ecommerce order), don't record as an action - 'visit_total_searches' => $actionType == Piwik_Tracker_Action::TYPE_SITE_SEARCH ? 1 : 0, - 'visit_total_time' => self::cleanupVisitTotalTime($defaultTimeOnePageVisit), - 'visit_goal_converted' => $visitIsConverted ? 1: 0, - 'visit_goal_buyer' => $this->goalManager->getBuyerType(), - 'referer_type' => $refererInfo['referer_type'], - 'referer_name' => $refererInfo['referer_name'], - 'referer_url' => $refererInfo['referer_url'], - 'referer_keyword' => $refererInfo['referer_keyword'], - 'config_id' => $userInfo['config_id'], - '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'], - ); - - // add optional location components - $location = $this->getVisitorLocation($userInfo['location_browser_lang']); - $this->updateVisitInfoWithLocation($location); - - // Add Custom variable key,value to the visitor array - $this->visitorInfo = array_merge($this->visitorInfo, $this->visitorCustomVariables); - - Piwik_PostEvent('Tracker.newVisitorInformation', $this->visitorInfo); - - $debugVisitInfo = $this->visitorInfo; - $debugVisitInfo['idvisitor'] = bin2hex($debugVisitInfo['idvisitor']); - $debugVisitInfo['config_id'] = bin2hex($debugVisitInfo['config_id']); - printDebug($debugVisitInfo); - - $this->saveVisitorInformation(); - } + array_push($sqlBind, $this->idsite, (int)$this->visitorInfo['idvisit']); + + $result = Piwik_Tracker::getDatabase()->query($sqlQuery, $sqlBind); + + $this->visitorInfo['visit_last_action_time'] = $this->getCurrentTimestamp(); + + // Debug output + if (isset($valuesToUpdate['idvisitor'])) { + $valuesToUpdate['idvisitor'] = bin2hex($valuesToUpdate['idvisitor']); + } + printDebug('Updating existing visit: ' . var_export($valuesToUpdate, true)); + + if (Piwik_Tracker::getDatabase()->rowCount($result) == 0) { + printDebug("Visitor with this idvisit wasn't found in the DB."); + printDebug("$sqlQuery --- "); + printDebug($sqlBind); + throw new Piwik_Tracker_Visit_VisitorNotFoundInDatabase( + "The visitor with idvisitor=" . bin2hex($this->visitorInfo['idvisitor']) . " and idvisit=" . $this->visitorInfo['idvisit'] + . " wasn't found in the DB, we fallback to a new visitor"); + } + + Piwik_PostEvent('Tracker.knownVisitorInformation', $this->visitorInfo); + } + + protected function isTimestampValid($time) + { + return $time <= $this->getCurrentTimestamp() + && $time > $this->getCurrentTimestamp() - 10 * 365 * 86400; + } + + /** + * In the case of a new visit, we have to do the following actions: + * + * 1) Insert the new action + * + * 2) Insert the visit information + * @param $idActionUrl + * @param $idActionName + * @param $actionType + * @param $visitIsConverted + */ + protected function handleNewVisit($idActionUrl, $idActionName, $actionType, $visitIsConverted) + { + printDebug("New Visit (IP = " . Piwik_IP::N2P($this->getVisitorIp()) . ")"); + + $localTimes = array( + 'h' => (string)Piwik_Common::getRequestVar('h', $this->getCurrentDate("H"), 'int', $this->request), + 'i' => (string)Piwik_Common::getRequestVar('m', $this->getCurrentDate("i"), 'int', $this->request), + 's' => (string)Piwik_Common::getRequestVar('s', $this->getCurrentDate("s"), 'int', $this->request) + ); + foreach ($localTimes as $k => $time) { + if (strlen($time) == 1) { + $localTimes[$k] = '0' . $time; + } + } + $localTime = $localTimes['h'] . ':' . $localTimes['i'] . ':' . $localTimes['s']; + + $idcookie = $this->getVisitorIdcookie(); + + $defaultTimeOnePageVisit = Piwik_Config::getInstance()->Tracker['default_time_one_page_visit']; + + // Days since first visit + $cookieFirstVisitTimestamp = Piwik_Common::getRequestVar('_idts', 0, 'int', $this->request); + if (!$this->isTimestampValid($cookieFirstVisitTimestamp)) { + $cookieFirstVisitTimestamp = $this->getCurrentTimestamp(); + } + $daysSinceFirstVisit = round(($this->getCurrentTimestamp() - $cookieFirstVisitTimestamp) / 86400, $precision = 0); + if ($daysSinceFirstVisit < 0) $daysSinceFirstVisit = 0; + + // Number of Visits + $visitCount = Piwik_Common::getRequestVar('_idvc', 1, 'int', $this->request); + if ($visitCount < 1) $visitCount = 1; + + // Days since last visit + $daysSinceLastVisit = 0; + $lastVisitTimestamp = Piwik_Common::getRequestVar('_viewts', 0, 'int', $this->request); + if ($this->isTimestampValid($lastVisitTimestamp)) { + $daysSinceLastVisit = round(($this->getCurrentTimestamp() - $lastVisitTimestamp) / 86400, $precision = 0); + if ($daysSinceLastVisit < 0) $daysSinceLastVisit = 0; + } + + $daysSinceLastOrder = 0; + $isReturningCustomer = false; + $lastOrderTimestamp = Piwik_Common::getRequestVar('_ects', 0, 'int', $this->request); + if ($this->isTimestampValid($lastOrderTimestamp)) { + $daysSinceLastOrder = round(($this->getCurrentTimestamp() - $lastOrderTimestamp) / 86400, $precision = 0); + if ($daysSinceLastOrder < 0) { + $daysSinceLastOrder = 0; + } + $isReturningCustomer = true; + } + + // User settings + $userInfo = $this->getUserSettingsInformation(); + + // Referrer data + $referrer = new Piwik_Tracker_Visit_Referer(); + $refererUrl = Piwik_Common::getRequestVar('urlref', '', 'string', $this->request); + $currentUrl = Piwik_Common::getRequestVar('url', '', 'string', $this->request); + $refererInfo = $referrer->getRefererInformation($refererUrl, $currentUrl, $this->idsite); + + $visitorReturning = $isReturningCustomer + ? 2 /* Returning customer */ + : ($visitCount > 1 || $this->isVisitorKnown() || $daysSinceLastVisit > 0 + ? 1 /* Returning */ + : 0 /* New */); + /** + * Save the visitor + */ + $this->visitorInfo = array( + 'idsite' => $this->idsite, + 'visitor_localtime' => $localTime, + 'idvisitor' => $idcookie, + 'visitor_returning' => $visitorReturning, + 'visitor_count_visits' => $visitCount, + 'visitor_days_since_last' => $daysSinceLastVisit, + 'visitor_days_since_order' => $daysSinceLastOrder, + 'visitor_days_since_first' => $daysSinceFirstVisit, + 'visit_first_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($this->getCurrentTimestamp()), + 'visit_last_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($this->getCurrentTimestamp()), + 'visit_entry_idaction_url' => (int)$idActionUrl, + 'visit_entry_idaction_name' => (int)$idActionName, + 'visit_exit_idaction_url' => (int)$idActionUrl, + 'visit_exit_idaction_name' => (int)$idActionName, + 'visit_total_actions' => in_array($actionType, + array(Piwik_Tracker_Action::TYPE_ACTION_URL, + Piwik_Tracker_Action::TYPE_DOWNLOAD, + Piwik_Tracker_Action::TYPE_OUTLINK, + Piwik_Tracker_Action::TYPE_SITE_SEARCH)) + ? 1 : 0, // if visit starts with something else (e.g. ecommerce order), don't record as an action + 'visit_total_searches' => $actionType == Piwik_Tracker_Action::TYPE_SITE_SEARCH ? 1 : 0, + 'visit_total_time' => self::cleanupVisitTotalTime($defaultTimeOnePageVisit), + 'visit_goal_converted' => $visitIsConverted ? 1 : 0, + 'visit_goal_buyer' => $this->goalManager->getBuyerType(), + 'referer_type' => $refererInfo['referer_type'], + 'referer_name' => $refererInfo['referer_name'], + 'referer_url' => $refererInfo['referer_url'], + 'referer_keyword' => $refererInfo['referer_keyword'], + 'config_id' => $userInfo['config_id'], + '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'], + ); + + // add optional location components + $location = $this->getVisitorLocation($userInfo['location_browser_lang']); + $this->updateVisitInfoWithLocation($location); + + // Add Custom variable key,value to the visitor array + $this->visitorInfo = array_merge($this->visitorInfo, $this->visitorCustomVariables); + + Piwik_PostEvent('Tracker.newVisitorInformation', $this->visitorInfo); + + $debugVisitInfo = $this->visitorInfo; + $debugVisitInfo['idvisitor'] = bin2hex($debugVisitInfo['idvisitor']); + $debugVisitInfo['config_id'] = bin2hex($debugVisitInfo['config_id']); + printDebug($debugVisitInfo); + + $this->saveVisitorInformation(); + } static private function cleanupVisitTotalTime($t) { $t = (int)$t; $smallintMysqlLimit = 65534; - if($t > $smallintMysqlLimit) { + if ($t > $smallintMysqlLimit) { $t = $smallintMysqlLimit; } return $t; } - - /** - * Returns the location of the visitor, based on the visitor's IP and browser language. - * - * @param string $browserLang - * @return array See Piwik_UserCountry_LocationProvider::getLocation for more info. - */ - private function getVisitorLocation( $browserLang ) - { - $location = array(); - $userInfo = array('lang' => $browserLang, 'ip' => Piwik_IP::N2P($this->getVisitorIp())); - Piwik_PostEvent('Tracker.getVisitorLocation', $location, $userInfo); - - if($this->authenticated) - { - // check for location override query parameters (ie, lat, long, country, region, city) - $locationOverrideParams = array( - 'country' => array('string', Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY), - 'region' => array('string', Piwik_UserCountry_LocationProvider::REGION_CODE_KEY), - 'city' => array('string', Piwik_UserCountry_LocationProvider::CITY_NAME_KEY), - 'lat' => array('float', Piwik_UserCountry_LocationProvider::LATITUDE_KEY), - 'long' => array('float', Piwik_UserCountry_LocationProvider::LONGITUDE_KEY), - ); - foreach ($locationOverrideParams as $queryParamName => $info) - { - list($type, $locationResultKey) = $info; - - $value = Piwik_Common::getRequestVar($queryParamName, false, $type, $this->request); - if (!empty($value)) - { - $location[$locationResultKey] = $value; - } - } - } - - if (empty($location['country_code'])) // sanity check - { - $location['country_code'] = self::UNKNOWN_CODE; - } - - return $location; - } - - /** - * Sets visitor info array with location info. - * - * @param array $location See Piwik_UserCountry_LocationProvider::getLocation for more info. - */ - private function updateVisitInfoWithLocation( $location ) - { - static $logVisitToLowerLocationMapping = array( - 'location_country' => Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY, - ); - - static $logVisitToLocationMapping = array( - 'location_region' => Piwik_UserCountry_LocationProvider::REGION_CODE_KEY, - 'location_city' => Piwik_UserCountry_LocationProvider::CITY_NAME_KEY, - 'location_latitude' => Piwik_UserCountry_LocationProvider::LATITUDE_KEY, - 'location_longitude' => Piwik_UserCountry_LocationProvider::LONGITUDE_KEY, - ); - - foreach ($logVisitToLowerLocationMapping as $column => $locationKey) - { - if (!empty($location[$locationKey])) - { - $this->visitorInfo[$column] = strtolower($location[$locationKey]); - } - } - - foreach ($logVisitToLocationMapping as $column => $locationKey) - { - if (!empty($location[$locationKey])) - { - $this->visitorInfo[$column] = $location[$locationKey]; - } - } - - // if the location has provider/organization info, set it - if (!empty($location[Piwik_UserCountry_LocationProvider::ISP_KEY])) - { - $providerValue = $location[Piwik_UserCountry_LocationProvider::ISP_KEY]; - - // if the org is set and not the same as the isp, add it to the provider value - if (!empty($location[Piwik_UserCountry_LocationProvider::ORG_KEY]) - && $location[Piwik_UserCountry_LocationProvider::ORG_KEY] != $providerValue) - { - $providerValue .= ' - ' . $location[Piwik_UserCountry_LocationProvider::ORG_KEY]; - } - } - else if (!empty($location[Piwik_UserCountry_LocationProvider::ORG_KEY])) - { - $providerValue = $location[Piwik_UserCountry_LocationProvider::ORG_KEY]; - } - - if (isset($providerValue)) - { - $this->visitorInfo['location_provider'] = $providerValue; - } - } - - /** - * Save new visitor information to log_visit table. - * Provides pre- and post- event hooks (Tracker.saveVisitorInformation and Tracker.saveVisitorInformation.end) for plugins - */ - protected function saveVisitorInformation() - { - Piwik_PostEvent('Tracker.saveVisitorInformation', $this->visitorInfo); - - $this->visitorInfo['location_browser_lang'] = substr($this->visitorInfo['location_browser_lang'], 0, 20); - $this->visitorInfo['referer_name'] = substr($this->visitorInfo['referer_name'], 0, 70); - $this->visitorInfo['referer_keyword'] = substr($this->visitorInfo['referer_keyword'], 0, 255); - $this->visitorInfo['config_resolution'] = substr($this->visitorInfo['config_resolution'], 0, 9); - - $fields = implode(", ", array_keys($this->visitorInfo)); - $values = Piwik_Common::getSqlStringFieldsArray($this->visitorInfo); - - $sql = "INSERT INTO ".Piwik_Common::prefixTable('log_visit'). " ($fields) VALUES ($values)"; - $bind = array_values($this->visitorInfo); - Piwik_Tracker::getDatabase()->query( $sql, $bind); - - $idVisit = Piwik_Tracker::getDatabase()->lastInsertId(); - $this->visitorInfo['idvisit'] = $idVisit; - - $this->visitorInfo['visit_first_action_time'] = $this->getCurrentTimestamp(); - $this->visitorInfo['visit_last_action_time'] = $this->getCurrentTimestamp(); - - Piwik_PostEvent('Tracker.saveVisitorInformation.end', $this->visitorInfo); - } - - /** - * Returns visitor cookie - * - * @return binary - */ - protected function getVisitorIdcookie() - { - if($this->isVisitorKnown()) - { - return $this->visitorInfo['idvisitor']; - } - // If the visitor had a first party ID cookie, then we use this value - if(!empty($this->visitorInfo['idvisitor']) - && strlen($this->visitorInfo['idvisitor']) == Piwik_Tracker::LENGTH_BINARY_ID) - { - return $this->visitorInfo['idvisitor']; - } + + /** + * Returns the location of the visitor, based on the visitor's IP and browser language. + * + * @param string $browserLang + * @return array See Piwik_UserCountry_LocationProvider::getLocation for more info. + */ + private function getVisitorLocation($browserLang) + { + $location = array(); + $userInfo = array('lang' => $browserLang, 'ip' => Piwik_IP::N2P($this->getVisitorIp())); + Piwik_PostEvent('Tracker.getVisitorLocation', $location, $userInfo); + + if ($this->authenticated) { + // check for location override query parameters (ie, lat, long, country, region, city) + $locationOverrideParams = array( + 'country' => array('string', Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY), + 'region' => array('string', Piwik_UserCountry_LocationProvider::REGION_CODE_KEY), + 'city' => array('string', Piwik_UserCountry_LocationProvider::CITY_NAME_KEY), + 'lat' => array('float', Piwik_UserCountry_LocationProvider::LATITUDE_KEY), + 'long' => array('float', Piwik_UserCountry_LocationProvider::LONGITUDE_KEY), + ); + foreach ($locationOverrideParams as $queryParamName => $info) { + list($type, $locationResultKey) = $info; + + $value = Piwik_Common::getRequestVar($queryParamName, false, $type, $this->request); + if (!empty($value)) { + $location[$locationResultKey] = $value; + } + } + } + + if (empty($location['country_code'])) // sanity check + { + $location['country_code'] = self::UNKNOWN_CODE; + } + + return $location; + } + + /** + * Sets visitor info array with location info. + * + * @param array $location See Piwik_UserCountry_LocationProvider::getLocation for more info. + */ + private function updateVisitInfoWithLocation($location) + { + static $logVisitToLowerLocationMapping = array( + 'location_country' => Piwik_UserCountry_LocationProvider::COUNTRY_CODE_KEY, + ); + + static $logVisitToLocationMapping = array( + 'location_region' => Piwik_UserCountry_LocationProvider::REGION_CODE_KEY, + 'location_city' => Piwik_UserCountry_LocationProvider::CITY_NAME_KEY, + 'location_latitude' => Piwik_UserCountry_LocationProvider::LATITUDE_KEY, + 'location_longitude' => Piwik_UserCountry_LocationProvider::LONGITUDE_KEY, + ); + + foreach ($logVisitToLowerLocationMapping as $column => $locationKey) { + if (!empty($location[$locationKey])) { + $this->visitorInfo[$column] = strtolower($location[$locationKey]); + } + } + + foreach ($logVisitToLocationMapping as $column => $locationKey) { + if (!empty($location[$locationKey])) { + $this->visitorInfo[$column] = $location[$locationKey]; + } + } + + // if the location has provider/organization info, set it + if (!empty($location[Piwik_UserCountry_LocationProvider::ISP_KEY])) { + $providerValue = $location[Piwik_UserCountry_LocationProvider::ISP_KEY]; + + // if the org is set and not the same as the isp, add it to the provider value + if (!empty($location[Piwik_UserCountry_LocationProvider::ORG_KEY]) + && $location[Piwik_UserCountry_LocationProvider::ORG_KEY] != $providerValue + ) { + $providerValue .= ' - ' . $location[Piwik_UserCountry_LocationProvider::ORG_KEY]; + } + } else if (!empty($location[Piwik_UserCountry_LocationProvider::ORG_KEY])) { + $providerValue = $location[Piwik_UserCountry_LocationProvider::ORG_KEY]; + } + + if (isset($providerValue)) { + $this->visitorInfo['location_provider'] = $providerValue; + } + } + + /** + * Save new visitor information to log_visit table. + * Provides pre- and post- event hooks (Tracker.saveVisitorInformation and Tracker.saveVisitorInformation.end) for plugins + */ + protected function saveVisitorInformation() + { + Piwik_PostEvent('Tracker.saveVisitorInformation', $this->visitorInfo); + + $this->visitorInfo['location_browser_lang'] = substr($this->visitorInfo['location_browser_lang'], 0, 20); + $this->visitorInfo['referer_name'] = substr($this->visitorInfo['referer_name'], 0, 70); + $this->visitorInfo['referer_keyword'] = substr($this->visitorInfo['referer_keyword'], 0, 255); + $this->visitorInfo['config_resolution'] = substr($this->visitorInfo['config_resolution'], 0, 9); + + $fields = implode(", ", array_keys($this->visitorInfo)); + $values = Piwik_Common::getSqlStringFieldsArray($this->visitorInfo); + + $sql = "INSERT INTO " . Piwik_Common::prefixTable('log_visit') . " ($fields) VALUES ($values)"; + $bind = array_values($this->visitorInfo); + Piwik_Tracker::getDatabase()->query($sql, $bind); + + $idVisit = Piwik_Tracker::getDatabase()->lastInsertId(); + $this->visitorInfo['idvisit'] = $idVisit; + + $this->visitorInfo['visit_first_action_time'] = $this->getCurrentTimestamp(); + $this->visitorInfo['visit_last_action_time'] = $this->getCurrentTimestamp(); + + Piwik_PostEvent('Tracker.saveVisitorInformation.end', $this->visitorInfo); + } + + /** + * Returns visitor cookie + * + * @return binary + */ + protected function getVisitorIdcookie() + { + if ($this->isVisitorKnown()) { + return $this->visitorInfo['idvisitor']; + } + // If the visitor had a first party ID cookie, then we use this value + if (!empty($this->visitorInfo['idvisitor']) + && strlen($this->visitorInfo['idvisitor']) == Piwik_Tracker::LENGTH_BINARY_ID + ) { + return $this->visitorInfo['idvisitor']; + } return Piwik_Common::hex2bin($this->generateUniqueVisitorId()); } @@ -782,333 +735,310 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface } /** - * Returns the visitor's IP address - * - * @return long - */ - protected function getVisitorIp() - { - return $this->visitorInfo['location_ip']; - } - - /** - * Returns the visitor's browser (user agent) - * - * @return string - */ - static public function getUserAgent($request) - { - $default = @$_SERVER['HTTP_USER_AGENT']; - return Piwik_Common::getRequestVar('ua', is_null($default) ? false : $default, 'string', $request); - } - - /** - * Returns the language the visitor is viewing. - * - * @return string browser language code, eg. "en-gb,en;q=0.5" - */ - protected function getBrowserLanguage() - { - return Piwik_Common::getRequestVar('lang', Piwik_Common::getBrowserLanguage(), 'string', $this->request); - } - - /** - * Returns the current date in the "Y-m-d" PHP format - * - * @param string $format - * @return string - */ - protected function getCurrentDate( $format = "Y-m-d") - { - return date($format, $this->getCurrentTimestamp() ); - } - - /** - * Returns the current Timestamp - * - * @return int - */ - protected function getCurrentTimestamp() - { - return $this->timestamp; - } - - /** - * Test if the current visitor is excluded from the statistics. - * - * Plugins can for example exclude visitors based on the - * - IP - * - If a given cookie is found - * - * @return bool True if the visit must not be saved, false otherwise - */ - protected function isExcluded() - { - $excluded = false; - - $ip = $this->getVisitorIp(); - $ua = $this->getUserAgent($this->request); - - /* - * 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). - */ - $allowBots = Piwik_Common::getRequestVar('bots', false) != false; - if ( !$allowBots - && (strpos($ua, 'Googlebot') !== false // Googlebot - || strpos($ua, 'Google Web Preview') !== false // Google Instant - || strpos($ua, 'bingbot') !== false // Bingbot - || strpos($ua, 'YottaaMonitor') !== false // Yottaa - || Piwik_IP::isIpInRange($ip, - array( - // Live/Bing/MSN - '64.4.0.0/18', - '65.52.0.0/14', - '157.54.0.0/15', - '157.56.0.0/14', - '157.60.0.0/16', - '207.46.0.0/16', - '207.68.128.0/18', - '207.68.192.0/20', - '131.253.26.0/20', - '131.253.24.0/20', - // Yahoo - '72.30.198.0/20', - '72.30.196.0/20', - '98.137.207.0/20', - // Chinese bot hammering websites - '1.202.218.8' - )))) - { - 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', $this->request); - if(!$toRecord) - { - printDebug(@$_SERVER['REQUEST_METHOD'].' parameter '.$parameterForceRecord.' not found in URL, request excluded'); - $excluded = true; - printDebug("'$parameterForceRecord' parameter not found."); - } - } - - /* 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(); - if($excluded) - { - printDebug("Ignore cookie found."); - } - } - - // Checking for excluded IPs - if(!$excluded) - { - $excluded = $this->isVisitorIpExcluded($ip); - if($excluded) - { - printDebug("IP excluded."); - } - } - - // Check if user agent should be excluded - if (!$excluded) - { - $excluded = $this->isUserAgentExcluded($ua); - if ($excluded) - { - printDebug("User agent excluded."); - } - } - - if(!$excluded) - { - if( (isset($_SERVER["HTTP_X_PURPOSE"]) - && in_array($_SERVER["HTTP_X_PURPOSE"], array("preview", "instant"))) - || (isset($_SERVER['HTTP_X_MOZ']) - && $_SERVER['HTTP_X_MOZ'] == "prefetch")) - { - $excluded = true; - printDebug("Prefetch request detected, not a real visit so we Ignore this visit/pageview"); - } - } - - if($excluded) - { - printDebug("Visitor excluded."); - return true; - } - - return false; - } - - /** - * Looks for the ignore cookie that users can set in the Piwik admin screen. - * @return bool - */ - protected function isIgnoreCookieFound() - { - if(Piwik_Tracker_IgnoreCookie::isIgnoreCookieFound()) - { - printDebug('Piwik ignore cookie was found, visit not tracked.'); - return true; - } - return false; - } - - /** - * Checks if the visitor ip is in the excluded list - * - * @param string $ip Long IP - * @return bool - */ - protected function isVisitorIpExcluded($ip) - { - $websiteAttributes = Piwik_Tracker_Cache::getCacheWebsiteAttributes( $this->idsite ); - if(!empty($websiteAttributes['excluded_ips'])) - { - if(Piwik_IP::isIpInRange($ip, $websiteAttributes['excluded_ips'])) - { - printDebug('Visitor IP '.Piwik_IP::N2P($ip).' is excluded from being tracked'); - return true; - } - } - return false; - } - - /** - * Returns true if the specified user agent should be excluded for the current site or not. - * - * Visits whose user agent string contains one of the excluded_user_agents strings for the - * site being tracked (or one of the global strings) will be excluded. - * - * @param string $ua The user agent string. - * @return bool - */ - protected function isUserAgentExcluded( $ua ) - { - $websiteAttributes = Piwik_Tracker_Cache::getCacheWebsiteAttributes($this->idsite); - if (!empty($websiteAttributes['excluded_user_agents'])) - { - foreach ($websiteAttributes['excluded_user_agents'] as $excludedUserAgent) - { - // if the excluded user agent string part is in this visit's user agent, this visit should be excluded - if (stripos($ua, $excludedUserAgent) !== false) - { - return true; - } - } - } - return false; - } - - /** - * Returns the cookie name used for the Piwik Tracker cookie - * - * @return string - */ - protected function getCookieName() - { - return Piwik_Config::getInstance()->Tracker['cookie_name']; - } - - /** - * Returns the cookie expiration date. - * - * @return int - */ - protected function getCookieExpire() - { - return $this->getCurrentTimestamp() + Piwik_Config::getInstance()->Tracker['cookie_expire']; - } - - /** - * Returns cookie path - * - * @return string - */ - protected function getCookiePath() - { - return Piwik_Config::getInstance()->Tracker['cookie_path']; - } - - protected function shouldUseThirdPartyCookie() - { - return (bool)Piwik_Config::getInstance()->Tracker['use_third_party_id_cookie']; - } - - /** - * Is the request for a known VisitorId, based on 1st party, 3rd party (optional) cookies or Tracking API forced Visitor ID - * @throws Exception - */ - protected function assignVisitorIdFromRequest() - { - $found = false; - - // Was a Visitor ID "forced" (@see Tracking API setVisitorId()) for this request? - $idVisitor = $this->getForcedVisitorId(); - if(!empty($idVisitor)) - { - if(strlen($idVisitor) != Piwik_Tracker::LENGTH_HEX_ID_STRING) - { - throw new Exception("Visitor ID (cid) $idVisitor must be ".Piwik_Tracker::LENGTH_HEX_ID_STRING." characters long"); - } - printDebug("Request will be recorded for this idvisitor = ".$idVisitor); - $found = true; - } - - // - If set to use 3rd party cookies for Visit ID, read the cookie - if(!$found) - { - // - By default, reads the first party cookie ID - $useThirdPartyCookie = $this->shouldUseThirdPartyCookie(); - if($useThirdPartyCookie) - { - $idVisitor = $this->cookie->get(0); - if($idVisitor !== false - && strlen($idVisitor) == Piwik_Tracker::LENGTH_HEX_ID_STRING) - { - $found = true; - } - } - } - // If a third party cookie was not found, we default to the first party cookie - if(!$found) - { - $idVisitor = Piwik_Common::getRequestVar('_id', '', 'string', $this->request); - $found = strlen($idVisitor) >= Piwik_Tracker::LENGTH_HEX_ID_STRING; - } - - if( $found ) - { - $truncated = substr($idVisitor, 0, Piwik_Tracker::LENGTH_HEX_ID_STRING); - $binVisitorId = @Piwik_Common::hex2bin($truncated); - if(!empty($binVisitorId)) - { - $this->visitorInfo['idvisitor'] = $binVisitorId; - } - - } - } + * Returns the visitor's IP address + * + * @return long + */ + protected function getVisitorIp() + { + return $this->visitorInfo['location_ip']; + } + + /** + * Returns the visitor's browser (user agent) + * + * @return string + */ + static public function getUserAgent($request) + { + $default = @$_SERVER['HTTP_USER_AGENT']; + return Piwik_Common::getRequestVar('ua', is_null($default) ? false : $default, 'string', $request); + } + + /** + * Returns the language the visitor is viewing. + * + * @return string browser language code, eg. "en-gb,en;q=0.5" + */ + protected function getBrowserLanguage() + { + return Piwik_Common::getRequestVar('lang', Piwik_Common::getBrowserLanguage(), 'string', $this->request); + } + + /** + * Returns the current date in the "Y-m-d" PHP format + * + * @param string $format + * @return string + */ + protected function getCurrentDate($format = "Y-m-d") + { + return date($format, $this->getCurrentTimestamp()); + } + + /** + * Returns the current Timestamp + * + * @return int + */ + protected function getCurrentTimestamp() + { + return $this->timestamp; + } + + /** + * Test if the current visitor is excluded from the statistics. + * + * Plugins can for example exclude visitors based on the + * - IP + * - If a given cookie is found + * + * @return bool True if the visit must not be saved, false otherwise + */ + protected function isExcluded() + { + $excluded = false; + + $ip = $this->getVisitorIp(); + $ua = $this->getUserAgent($this->request); + + /* + * 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). + */ + $allowBots = Piwik_Common::getRequestVar('bots', false) != false; + if (!$allowBots + && (strpos($ua, 'Googlebot') !== false // Googlebot + || strpos($ua, 'Google Web Preview') !== false // Google Instant + || strpos($ua, 'bingbot') !== false // Bingbot + || strpos($ua, 'YottaaMonitor') !== false // Yottaa + || Piwik_IP::isIpInRange($ip, + array( + // Live/Bing/MSN + '64.4.0.0/18', + '65.52.0.0/14', + '157.54.0.0/15', + '157.56.0.0/14', + '157.60.0.0/16', + '207.46.0.0/16', + '207.68.128.0/18', + '207.68.192.0/20', + '131.253.26.0/20', + '131.253.24.0/20', + // Yahoo + '72.30.198.0/20', + '72.30.196.0/20', + '98.137.207.0/20', + // Chinese bot hammering websites + '1.202.218.8' + ))) + ) { + 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', $this->request); + if (!$toRecord) { + printDebug(@$_SERVER['REQUEST_METHOD'] . ' parameter ' . $parameterForceRecord . ' not found in URL, request excluded'); + $excluded = true; + printDebug("'$parameterForceRecord' parameter not found."); + } + } + + /* 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(); + if ($excluded) { + printDebug("Ignore cookie found."); + } + } + + // Checking for excluded IPs + if (!$excluded) { + $excluded = $this->isVisitorIpExcluded($ip); + if ($excluded) { + printDebug("IP excluded."); + } + } + + // Check if user agent should be excluded + if (!$excluded) { + $excluded = $this->isUserAgentExcluded($ua); + if ($excluded) { + printDebug("User agent excluded."); + } + } + + if (!$excluded) { + if ((isset($_SERVER["HTTP_X_PURPOSE"]) + && in_array($_SERVER["HTTP_X_PURPOSE"], array("preview", "instant"))) + || (isset($_SERVER['HTTP_X_MOZ']) + && $_SERVER['HTTP_X_MOZ'] == "prefetch") + ) { + $excluded = true; + printDebug("Prefetch request detected, not a real visit so we Ignore this visit/pageview"); + } + } + + if ($excluded) { + printDebug("Visitor excluded."); + return true; + } + + return false; + } + + /** + * Looks for the ignore cookie that users can set in the Piwik admin screen. + * @return bool + */ + protected function isIgnoreCookieFound() + { + if (Piwik_Tracker_IgnoreCookie::isIgnoreCookieFound()) { + printDebug('Piwik ignore cookie was found, visit not tracked.'); + return true; + } + return false; + } + + /** + * Checks if the visitor ip is in the excluded list + * + * @param string $ip Long IP + * @return bool + */ + protected function isVisitorIpExcluded($ip) + { + $websiteAttributes = Piwik_Tracker_Cache::getCacheWebsiteAttributes($this->idsite); + if (!empty($websiteAttributes['excluded_ips'])) { + if (Piwik_IP::isIpInRange($ip, $websiteAttributes['excluded_ips'])) { + printDebug('Visitor IP ' . Piwik_IP::N2P($ip) . ' is excluded from being tracked'); + return true; + } + } + return false; + } + + /** + * Returns true if the specified user agent should be excluded for the current site or not. + * + * Visits whose user agent string contains one of the excluded_user_agents strings for the + * site being tracked (or one of the global strings) will be excluded. + * + * @param string $ua The user agent string. + * @return bool + */ + protected function isUserAgentExcluded($ua) + { + $websiteAttributes = Piwik_Tracker_Cache::getCacheWebsiteAttributes($this->idsite); + if (!empty($websiteAttributes['excluded_user_agents'])) { + foreach ($websiteAttributes['excluded_user_agents'] as $excludedUserAgent) { + // if the excluded user agent string part is in this visit's user agent, this visit should be excluded + if (stripos($ua, $excludedUserAgent) !== false) { + return true; + } + } + } + return false; + } + + /** + * Returns the cookie name used for the Piwik Tracker cookie + * + * @return string + */ + protected function getCookieName() + { + return Piwik_Config::getInstance()->Tracker['cookie_name']; + } + + /** + * Returns the cookie expiration date. + * + * @return int + */ + protected function getCookieExpire() + { + return $this->getCurrentTimestamp() + Piwik_Config::getInstance()->Tracker['cookie_expire']; + } + + /** + * Returns cookie path + * + * @return string + */ + protected function getCookiePath() + { + return Piwik_Config::getInstance()->Tracker['cookie_path']; + } + + protected function shouldUseThirdPartyCookie() + { + return (bool)Piwik_Config::getInstance()->Tracker['use_third_party_id_cookie']; + } + + /** + * Is the request for a known VisitorId, based on 1st party, 3rd party (optional) cookies or Tracking API forced Visitor ID + * @throws Exception + */ + protected function assignVisitorIdFromRequest() + { + $found = false; + + // Was a Visitor ID "forced" (@see Tracking API setVisitorId()) for this request? + $idVisitor = $this->getForcedVisitorId(); + if (!empty($idVisitor)) { + if (strlen($idVisitor) != Piwik_Tracker::LENGTH_HEX_ID_STRING) { + throw new Exception("Visitor ID (cid) $idVisitor must be " . Piwik_Tracker::LENGTH_HEX_ID_STRING . " characters long"); + } + printDebug("Request will be recorded for this idvisitor = " . $idVisitor); + $found = true; + } + + // - If set to use 3rd party cookies for Visit ID, read the cookie + if (!$found) { + // - By default, reads the first party cookie ID + $useThirdPartyCookie = $this->shouldUseThirdPartyCookie(); + if ($useThirdPartyCookie) { + $idVisitor = $this->cookie->get(0); + if ($idVisitor !== false + && strlen($idVisitor) == Piwik_Tracker::LENGTH_HEX_ID_STRING + ) { + $found = true; + } + } + } + // If a third party cookie was not found, we default to the first party cookie + if (!$found) { + $idVisitor = Piwik_Common::getRequestVar('_id', '', 'string', $this->request); + $found = strlen($idVisitor) >= Piwik_Tracker::LENGTH_HEX_ID_STRING; + } + + if ($found) { + $truncated = substr($idVisitor, 0, Piwik_Tracker::LENGTH_HEX_ID_STRING); + $binVisitorId = @Piwik_Common::hex2bin($truncated); + if (!empty($binVisitorId)) { + $this->visitorInfo['idvisitor'] = $binVisitorId; + } + + } + } protected function getForcedVisitorId() { @@ -1116,49 +1046,45 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface } /** - * This methods tries to see if the visitor has visited the website before. - * - * We have to split the visitor into one of the category - * - Known visitor - * - New visitor - */ - protected function recognizeTheVisitor() - { - $this->visitorKnown = false; - $this->setCookie( new Piwik_Cookie( - $this->getCookieName(), - $this->getCookieExpire(), - $this->getCookiePath()) ); - $this->printCookie(); - - $userInfo = $this->getUserSettingsInformation(); - $configId = $userInfo['config_id']; - - $this->assignVisitorIdFromRequest(); - $isVisitorIdToLookup = !empty($this->visitorInfo['idvisitor']); - - if($isVisitorIdToLookup) - { - printDebug("Matching visitors with: visitorId=".bin2hex($this->visitorInfo['idvisitor'])." OR configId=".bin2hex($configId)); - } - else - { - printDebug("Visitor doesn't have the piwik cookie..."); - } - - $selectCustomVariables = ''; - // No custom var were found in the request, so let's copy the previous one in a potential conversion later - if(!$this->customVariablesSetFromRequest) - { - $selectCustomVariables = ', + * This methods tries to see if the visitor has visited the website before. + * + * We have to split the visitor into one of the category + * - Known visitor + * - New visitor + */ + protected function recognizeTheVisitor() + { + $this->visitorKnown = false; + $this->setCookie(new Piwik_Cookie( + $this->getCookieName(), + $this->getCookieExpire(), + $this->getCookiePath())); + $this->printCookie(); + + $userInfo = $this->getUserSettingsInformation(); + $configId = $userInfo['config_id']; + + $this->assignVisitorIdFromRequest(); + $isVisitorIdToLookup = !empty($this->visitorInfo['idvisitor']); + + if ($isVisitorIdToLookup) { + printDebug("Matching visitors with: visitorId=" . bin2hex($this->visitorInfo['idvisitor']) . " OR configId=" . bin2hex($configId)); + } else { + printDebug("Visitor doesn't have the piwik cookie..."); + } + + $selectCustomVariables = ''; + // No custom var were found in the request, so let's copy the previous one in a potential conversion later + if (!$this->customVariablesSetFromRequest) { + $selectCustomVariables = ', custom_var_k1, custom_var_v1, custom_var_k2, custom_var_v2, custom_var_k3, custom_var_v3, custom_var_k4, custom_var_v4, custom_var_k5, custom_var_v5'; - } - - $select = "SELECT idvisitor, + } + + $select = "SELECT idvisitor, visit_last_action_time, visit_first_action_time, idvisit, @@ -1179,9 +1105,9 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface visit_goal_buyer $selectCustomVariables "; - $from = "FROM ".Piwik_Common::prefixTable('log_visit'); + $from = "FROM " . Piwik_Common::prefixTable('log_visit'); - $bindSql = array(); + $bindSql = array(); $timeLookBack = $this->getWindowLookupPreviousVisit(); @@ -1189,142 +1115,132 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $shouldMatchOneFieldOnly = $this->shouldLookupOneVisitorFieldOnly($isVisitorIdToLookup); // Two use cases: - // 1) there is no visitor ID so we try to match only on config_id (heuristics) - // Possible causes of no visitor ID: no browser cookie support, direct Tracking API request without visitor ID passed, etc. - // We can use config_id heuristics to try find the visitor in the past, there is a risk to assign - // this page view to the wrong visitor, but this is better than creating artificial visits. - // 2) there is a visitor ID and we trust it (config setting trust_visitors_cookies, OR it was set using &cid= in tracking API), + // 1) there is no visitor ID so we try to match only on config_id (heuristics) + // Possible causes of no visitor ID: no browser cookie support, direct Tracking API request without visitor ID passed, etc. + // We can use config_id heuristics to try find the visitor in the past, there is a risk to assign + // this page view to the wrong visitor, but this is better than creating artificial visits. + // 2) there is a visitor ID and we trust it (config setting trust_visitors_cookies, OR it was set using &cid= in tracking API), // and in these cases, we force to look up this visitor id - if($shouldMatchOneFieldOnly) - { - $where = "visit_last_action_time >= ? AND idsite = ?"; - $bindSql[] = $timeLookBack; - $bindSql[] = $this->idsite; - if(!$isVisitorIdToLookup) - { - $where .= ' AND config_id = ?'; - $bindSql[] = $configId; - } - else - { - $where .= ' AND idvisitor = ?'; - $bindSql[] = $this->visitorInfo['idvisitor']; - } - - $sql = "$select + if ($shouldMatchOneFieldOnly) { + $where = "visit_last_action_time >= ? AND idsite = ?"; + $bindSql[] = $timeLookBack; + $bindSql[] = $this->idsite; + if (!$isVisitorIdToLookup) { + $where .= ' AND config_id = ?'; + $bindSql[] = $configId; + } else { + $where .= ' AND idvisitor = ?'; + $bindSql[] = $this->visitorInfo['idvisitor']; + } + + $sql = "$select $from - WHERE ".$where." + WHERE " . $where . " ORDER BY visit_last_action_time DESC LIMIT 1"; - } - // We have a config_id AND a visitor_id. We match on either of these. - // Why do we also match on config_id? - // we do not trust the visitor ID only. Indeed, some browsers, or browser addons, - // cause the visitor id from the 1st party cookie to be different on each page view! - // It is not acceptable to create a new visit every time such browser does a page view, - // so we also backup by searching for matching config_id. - // We use a UNION here so that each sql query uses its own INDEX - else - { - $whereSameBothQueries = "visit_last_action_time >= ? AND idsite = ?"; - - - // will use INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time) - $bindSql[] = $timeLookBack; - $bindSql[] = $this->idsite; - $where = ' AND config_id = ?'; - $bindSql[] = $configId; - $sqlConfigId = "$select , + } // We have a config_id AND a visitor_id. We match on either of these. + // Why do we also match on config_id? + // we do not trust the visitor ID only. Indeed, some browsers, or browser addons, + // cause the visitor id from the 1st party cookie to be different on each page view! + // It is not acceptable to create a new visit every time such browser does a page view, + // so we also backup by searching for matching config_id. + // We use a UNION here so that each sql query uses its own INDEX + else { + $whereSameBothQueries = "visit_last_action_time >= ? AND idsite = ?"; + + + // will use INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time) + $bindSql[] = $timeLookBack; + $bindSql[] = $this->idsite; + $where = ' AND config_id = ?'; + $bindSql[] = $configId; + $sqlConfigId = "$select , 0 as priority $from WHERE $whereSameBothQueries $where ORDER BY visit_last_action_time DESC LIMIT 1 "; - - // will use INDEX index_idsite_idvisitor (idsite, idvisitor) - $bindSql[] = $timeLookBack; - $bindSql[] = $this->idsite; - $where = ' AND idvisitor = ?'; - $bindSql[] = $this->visitorInfo['idvisitor']; - $sqlVisitorId = "$select , + + // will use INDEX index_idsite_idvisitor (idsite, idvisitor) + $bindSql[] = $timeLookBack; + $bindSql[] = $this->idsite; + $where = ' AND idvisitor = ?'; + $bindSql[] = $this->visitorInfo['idvisitor']; + $sqlVisitorId = "$select , 1 as priority $from WHERE $whereSameBothQueries $where LIMIT 1 "; - - // We join both queries and favor the one matching the visitor_id if it did match - $sql = " ( $sqlConfigId ) + + // We join both queries and favor the one matching the visitor_id if it did match + $sql = " ( $sqlConfigId ) UNION ( $sqlVisitorId ) ORDER BY priority DESC LIMIT 1"; - } - - - $visitRow = Piwik_Tracker::getDatabase()->fetch($sql, $bindSql); - - if( !Piwik_Config::getInstance()->Debug['tracker_always_new_visitor'] - && $visitRow - && count($visitRow) > 0) - { - // These values will be used throughout the request - $this->visitorInfo['visit_last_action_time'] = strtotime($visitRow['visit_last_action_time']); - $this->visitorInfo['visit_first_action_time'] = strtotime($visitRow['visit_first_action_time']); - - // We always keep the first idvisitor seen for this visit (so that all page views for this visit have the same idvisitor) - $this->visitorInfo['idvisitor'] = $visitRow['idvisitor']; - $this->visitorInfo['idvisit'] = $visitRow['idvisit']; - $this->visitorInfo['visit_exit_idaction_url'] = $visitRow['visit_exit_idaction_url']; - $this->visitorInfo['visit_exit_idaction_name'] = $visitRow['visit_exit_idaction_name']; - $this->visitorInfo['visitor_returning'] = $visitRow['visitor_returning']; - $this->visitorInfo['visitor_days_since_first'] = $visitRow['visitor_days_since_first']; - $this->visitorInfo['visitor_days_since_order'] = $visitRow['visitor_days_since_order']; - $this->visitorInfo['visitor_count_visits'] = $visitRow['visitor_count_visits']; - $this->visitorInfo['visit_goal_buyer'] = $visitRow['visit_goal_buyer']; - $this->visitorInfo['location_country'] = $visitRow['location_country']; - $this->visitorInfo['location_region'] = $visitRow['location_region']; - $this->visitorInfo['location_city'] = $visitRow['location_city']; - $this->visitorInfo['location_latitude'] = $visitRow['location_latitude']; - $this->visitorInfo['location_longitude'] = $visitRow['location_longitude']; - - // Referer information will be potentially used for Goal Conversion attribution - $this->visitorInfo['referer_name'] = $visitRow['referer_name']; - $this->visitorInfo['referer_keyword'] = $visitRow['referer_keyword']; - $this->visitorInfo['referer_type'] = $visitRow['referer_type']; - - // Custom Variables copied from Visit in potential later conversion - if(!empty($selectCustomVariables)) - { - for($i=1; $i<=Piwik_Tracker::MAX_CUSTOM_VARIABLES; $i++) - { - if(isset($visitRow['custom_var_k'.$i]) - && strlen($visitRow['custom_var_k'.$i])) - { - $this->visitorInfo['custom_var_k'.$i] = $visitRow['custom_var_k'.$i]; - } - if(isset($visitRow['custom_var_v'.$i]) - && strlen($visitRow['custom_var_v'.$i])) - { - $this->visitorInfo['custom_var_v'.$i] = $visitRow['custom_var_v'.$i]; - } - } - } - - $this->visitorKnown = true; - printDebug("The visitor is known (idvisitor = ".bin2hex($this->visitorInfo['idvisitor']).", - config_id = ".bin2hex($configId).", + } + + + $visitRow = Piwik_Tracker::getDatabase()->fetch($sql, $bindSql); + + if (!Piwik_Config::getInstance()->Debug['tracker_always_new_visitor'] + && $visitRow + && count($visitRow) > 0 + ) { + // These values will be used throughout the request + $this->visitorInfo['visit_last_action_time'] = strtotime($visitRow['visit_last_action_time']); + $this->visitorInfo['visit_first_action_time'] = strtotime($visitRow['visit_first_action_time']); + + // We always keep the first idvisitor seen for this visit (so that all page views for this visit have the same idvisitor) + $this->visitorInfo['idvisitor'] = $visitRow['idvisitor']; + $this->visitorInfo['idvisit'] = $visitRow['idvisit']; + $this->visitorInfo['visit_exit_idaction_url'] = $visitRow['visit_exit_idaction_url']; + $this->visitorInfo['visit_exit_idaction_name'] = $visitRow['visit_exit_idaction_name']; + $this->visitorInfo['visitor_returning'] = $visitRow['visitor_returning']; + $this->visitorInfo['visitor_days_since_first'] = $visitRow['visitor_days_since_first']; + $this->visitorInfo['visitor_days_since_order'] = $visitRow['visitor_days_since_order']; + $this->visitorInfo['visitor_count_visits'] = $visitRow['visitor_count_visits']; + $this->visitorInfo['visit_goal_buyer'] = $visitRow['visit_goal_buyer']; + $this->visitorInfo['location_country'] = $visitRow['location_country']; + $this->visitorInfo['location_region'] = $visitRow['location_region']; + $this->visitorInfo['location_city'] = $visitRow['location_city']; + $this->visitorInfo['location_latitude'] = $visitRow['location_latitude']; + $this->visitorInfo['location_longitude'] = $visitRow['location_longitude']; + + // Referer information will be potentially used for Goal Conversion attribution + $this->visitorInfo['referer_name'] = $visitRow['referer_name']; + $this->visitorInfo['referer_keyword'] = $visitRow['referer_keyword']; + $this->visitorInfo['referer_type'] = $visitRow['referer_type']; + + // Custom Variables copied from Visit in potential later conversion + if (!empty($selectCustomVariables)) { + for ($i = 1; $i <= Piwik_Tracker::MAX_CUSTOM_VARIABLES; $i++) { + if (isset($visitRow['custom_var_k' . $i]) + && strlen($visitRow['custom_var_k' . $i]) + ) { + $this->visitorInfo['custom_var_k' . $i] = $visitRow['custom_var_k' . $i]; + } + if (isset($visitRow['custom_var_v' . $i]) + && strlen($visitRow['custom_var_v' . $i]) + ) { + $this->visitorInfo['custom_var_v' . $i] = $visitRow['custom_var_v' . $i]; + } + } + } + + $this->visitorKnown = true; + printDebug("The visitor is known (idvisitor = " . bin2hex($this->visitorInfo['idvisitor']) . ", + config_id = " . bin2hex($configId) . ", idvisit = {$this->visitorInfo['idvisit']}, - last action = ".date("r", $this->visitorInfo['visit_last_action_time']).", - first action = ".date("r", $this->visitorInfo['visit_first_action_time']) .", - visit_goal_buyer' = ".$this->visitorInfo['visit_goal_buyer'].")"); - } - else - { - printDebug("The visitor was not matched with an existing visitor..."); - } - } + last action = " . date("r", $this->visitorInfo['visit_last_action_time']) . ", + first action = " . date("r", $this->visitorInfo['visit_first_action_time']) . ", + visit_goal_buyer' = " . $this->visitorInfo['visit_goal_buyer'] . ")"); + } else { + printDebug("The visitor was not matched with an existing visitor..."); + } + } /** * By default, we look back 30 minutes to find a previous visitor (for performance reasons). @@ -1356,280 +1272,266 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $isForcedVisitorIdMustMatch = ($this->getForcedVisitorId() != null); $shouldMatchOneFieldOnly = (($isVisitorIdToLookup && $trustCookiesOnly) - || $isForcedVisitorIdMustMatch - || !$isVisitorIdToLookup); + || $isForcedVisitorIdMustMatch + || !$isVisitorIdToLookup); return $shouldMatchOneFieldOnly; } static public function getCustomVariables($scope, $request) - { - if($scope == 'visit') { - $parameter = '_cvar'; - } else { - $parameter = 'cvar'; - } - - $customVar = Piwik_Common::unsanitizeInputValues(Piwik_Common::getRequestVar($parameter, '', 'json', $request)); - if(!is_array($customVar)) - { - return array(); - } - $customVariables = array(); - foreach($customVar as $id => $keyValue) - { - $id = (int)$id; - if($id < 1 - || $id > Piwik_Tracker::MAX_CUSTOM_VARIABLES - || count($keyValue) != 2 - || (!is_string($keyValue[0]) && !is_numeric($keyValue[0])) - ) - { - printDebug("Invalid custom variables detected (id=$id)"); - continue; - } - if(strlen($keyValue[1]) == 0) - { - $keyValue[1] = ""; - } - // We keep in the URL when Custom Variable have empty names - // and values, as it means they can be deleted server side - - $key = self::truncateCustomVariable($keyValue[0]); - $value = self::truncateCustomVariable($keyValue[1]); - $customVariables['custom_var_k'.$id] = $key; - $customVariables['custom_var_v'.$id] = $value; - } - - return $customVariables; - } - - static public function truncateCustomVariable($input) - { - return substr(trim($input), 0, Piwik_Tracker::MAX_LENGTH_CUSTOM_VARIABLE); - } - - /** - * Gets the UserSettings information and returns them in an array of name => value - * - * @return array - */ - protected function getUserSettingsInformation() - { - // we already called this method before, simply returns the result - if(is_array($this->userSettingsInformation)) - { - return $this->userSettingsInformation; - } - require_once PIWIK_INCLUDE_PATH . '/libs/UserAgentParser/UserAgentParser.php'; - - $plugin_Flash = Piwik_Common::getRequestVar( 'fla', 0, 'int', $this->request); - $plugin_Java = Piwik_Common::getRequestVar( 'java', 0, 'int', $this->request); - $plugin_Director = Piwik_Common::getRequestVar( 'dir', 0, 'int', $this->request); - $plugin_Quicktime = Piwik_Common::getRequestVar( 'qt', 0, 'int', $this->request); - $plugin_RealPlayer = Piwik_Common::getRequestVar( 'realp', 0, 'int', $this->request); - $plugin_PDF = Piwik_Common::getRequestVar( 'pdf', 0, 'int', $this->request); - $plugin_WindowsMedia = Piwik_Common::getRequestVar( 'wma', 0, 'int', $this->request); - $plugin_Gears = Piwik_Common::getRequestVar( 'gears', 0, 'int', $this->request); - $plugin_Silverlight = Piwik_Common::getRequestVar( 'ag', 0, 'int', $this->request); - $plugin_Cookie = Piwik_Common::getRequestVar( 'cookie', 0, 'int', $this->request); - - $userAgent = $this->getUserAgent($this->request); - $aBrowserInfo = UserAgentParser::getBrowser($userAgent); - - $browserName = ($aBrowserInfo !== false && $aBrowserInfo['id'] !== false) ? $aBrowserInfo['id'] : 'UNK'; - $browserVersion = ($aBrowserInfo !== false && $aBrowserInfo['version'] !== false) ? $aBrowserInfo['version'] : ''; - - $os = UserAgentParser::getOperatingSystem($userAgent); - $os = $os === false ? 'UNK' : $os['id']; - - $resolution = Piwik_Common::getRequestVar('res', 'unknown', 'string', $this->request); - - $browserLang = $this->getBrowserLanguage(); - - $configurationHash = $this->getConfigHash( - $os, - $browserName, - $browserVersion, - $resolution, - $plugin_Flash, - $plugin_Java, - $plugin_Director, - $plugin_Quicktime, - $plugin_RealPlayer, - $plugin_PDF, - $plugin_WindowsMedia, - $plugin_Gears, - $plugin_Silverlight, - $plugin_Cookie, - $this->getVisitorIp(), - $browserLang); - - $this->userSettingsInformation = array( - 'config_id' => $configurationHash, - 'config_os' => $os, - 'config_browser_name' => $browserName, - 'config_browser_version' => $browserVersion, - 'config_resolution' => $resolution, - 'config_pdf' => $plugin_PDF, - 'config_flash' => $plugin_Flash, - 'config_java' => $plugin_Java, - 'config_director' => $plugin_Director, - 'config_quicktime' => $plugin_Quicktime, - 'config_realplayer' => $plugin_RealPlayer, - 'config_windowsmedia' => $plugin_WindowsMedia, - 'config_gears' => $plugin_Gears, - 'config_silverlight' => $plugin_Silverlight, - 'config_cookie' => $plugin_Cookie, - 'location_browser_lang' => $browserLang, - ); - - return $this->userSettingsInformation; - } - - /** - * Returns true if the last action was done during the last 30 minutes - * @return bool - */ - protected function isLastActionInTheSameVisit() - { - return isset($this->visitorInfo['visit_last_action_time']) - && ($this->visitorInfo['visit_last_action_time'] - > ($this->getCurrentTimestamp() - Piwik_Config::getInstance()->Tracker['visit_standard_length'])); - } - - /** - * Returns true if the recognizeTheVisitor() method did recognize the visitor - * @return bool - */ - protected function isVisitorKnown() - { - return $this->visitorKnown === true; - } - - /** - * Update the cookie information. - */ - protected function setThirdPartyCookie() - { - if(!$this->shouldUseThirdPartyCookie()) - { - return; - } - printDebug("We manage the cookie..."); - - // idcookie has been generated in handleNewVisit or we simply propagate the old value - $this->cookie->set(0, bin2hex($this->visitorInfo['idvisitor']) ); - $this->cookie->save(); - } - - /** - * Returns an object able to handle the current action - * Plugins can return an override Action that for example, does not record the action in the DB - * - * @throws Exception - * @return Piwik_Tracker_Action child or fake but with same public interface - */ - protected function newAction() - { - $action = null; - Piwik_PostEvent('Tracker.newAction', $action); - - if(is_null($action)) - { - $action = new Piwik_Tracker_Action(); - } - elseif(!($action instanceof Piwik_Tracker_Action_Interface)) - { - throw new Exception("The Action object set in the plugin must implement the interface Piwik_Tracker_Action_Interface"); - } - return $action; - } - - /** - * Detect whether action is an outlink given host aliases - * - * @param Piwik_Tracker_Action_Interface $action - * @return bool true if the outlink the visitor clicked on points to one of the known hosts for this website - */ - protected function detectActionIsOutlinkOnAliasHost(Piwik_Tracker_Action_Interface $action) - { - if($action->getActionType() != Piwik_Tracker_Action_Interface::TYPE_OUTLINK) - { - return false; - } - $decodedActionUrl = $action->getActionUrl(); - $actionUrlParsed = @parse_url($decodedActionUrl); - if(!isset($actionUrlParsed['host'])) - { - return false; - } - return Piwik_Tracker_Visit::isHostKnownAliasHost($actionUrlParsed['host'], $this->idsite); - } - - /** - * Returns a 64-bit hash of all the configuration settings - * @param $os - * @param $browserName - * @param $browserVersion - * @param $resolution - * @param $plugin_Flash - * @param $plugin_Java - * @param $plugin_Director - * @param $plugin_Quicktime - * @param $plugin_RealPlayer - * @param $plugin_PDF - * @param $plugin_WindowsMedia - * @param $plugin_Gears - * @param $plugin_Silverlight - * @param $plugin_Cookie - * @param $ip - * @param $browserLang - * @return string - */ - protected function getConfigHash( $os, $browserName, $browserVersion, $resolution, $plugin_Flash, $plugin_Java, $plugin_Director, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF, $plugin_WindowsMedia, $plugin_Gears, $plugin_Silverlight, $plugin_Cookie, $ip, $browserLang) - { - $hash = md5( $os . $browserName . $browserVersion . $plugin_Flash . $plugin_Java . $plugin_Director . $plugin_Quicktime . $plugin_RealPlayer . $plugin_PDF . $plugin_WindowsMedia . $plugin_Gears . $plugin_Silverlight . $plugin_Cookie . $ip . $browserLang, $raw_output = true ); - return Piwik_Common::substr( $hash, 0, Piwik_Tracker::LENGTH_BINARY_ID ); - } - - /** - * Returns either - * - "-1" for a known visitor - * - at least 16 char identifier in hex @see Piwik_Common::generateUniqId() - * @return int|string - */ - protected function getVisitorUniqueId() - { - if($this->isVisitorKnown()) - { - return -1; - } - return Piwik_Common::generateUniqId(); - } - - protected function setCookie( $cookie ) - { - $this->cookie = $cookie; - } - - // is the referer host any of the registered URLs for this website? - static public function isHostKnownAliasHost($urlHost, $idSite) - { - $websiteData = Piwik_Tracker_Cache::getCacheWebsiteAttributes($idSite); - if(isset($websiteData['hosts'])) - { - $canonicalHosts = array(); - foreach($websiteData['hosts'] as $host) { - $canonicalHosts[] = str_replace('www.', '' , mb_strtolower($host, 'UTF-8')); - } - $canonicalHost = str_replace('www.', '', mb_strtolower($urlHost, 'UTF-8')); - if(in_array($canonicalHost, $canonicalHosts)) - { - return true; - } - } - return false; - } + { + if ($scope == 'visit') { + $parameter = '_cvar'; + } else { + $parameter = 'cvar'; + } + + $customVar = Piwik_Common::unsanitizeInputValues(Piwik_Common::getRequestVar($parameter, '', 'json', $request)); + if (!is_array($customVar)) { + return array(); + } + $customVariables = array(); + foreach ($customVar as $id => $keyValue) { + $id = (int)$id; + if ($id < 1 + || $id > Piwik_Tracker::MAX_CUSTOM_VARIABLES + || count($keyValue) != 2 + || (!is_string($keyValue[0]) && !is_numeric($keyValue[0])) + ) { + printDebug("Invalid custom variables detected (id=$id)"); + continue; + } + if (strlen($keyValue[1]) == 0) { + $keyValue[1] = ""; + } + // We keep in the URL when Custom Variable have empty names + // and values, as it means they can be deleted server side + + $key = self::truncateCustomVariable($keyValue[0]); + $value = self::truncateCustomVariable($keyValue[1]); + $customVariables['custom_var_k' . $id] = $key; + $customVariables['custom_var_v' . $id] = $value; + } + + return $customVariables; + } + + static public function truncateCustomVariable($input) + { + return substr(trim($input), 0, Piwik_Tracker::MAX_LENGTH_CUSTOM_VARIABLE); + } + + /** + * Gets the UserSettings information and returns them in an array of name => value + * + * @return array + */ + protected function getUserSettingsInformation() + { + // we already called this method before, simply returns the result + if (is_array($this->userSettingsInformation)) { + return $this->userSettingsInformation; + } + require_once PIWIK_INCLUDE_PATH . '/libs/UserAgentParser/UserAgentParser.php'; + + $plugin_Flash = Piwik_Common::getRequestVar('fla', 0, 'int', $this->request); + $plugin_Java = Piwik_Common::getRequestVar('java', 0, 'int', $this->request); + $plugin_Director = Piwik_Common::getRequestVar('dir', 0, 'int', $this->request); + $plugin_Quicktime = Piwik_Common::getRequestVar('qt', 0, 'int', $this->request); + $plugin_RealPlayer = Piwik_Common::getRequestVar('realp', 0, 'int', $this->request); + $plugin_PDF = Piwik_Common::getRequestVar('pdf', 0, 'int', $this->request); + $plugin_WindowsMedia = Piwik_Common::getRequestVar('wma', 0, 'int', $this->request); + $plugin_Gears = Piwik_Common::getRequestVar('gears', 0, 'int', $this->request); + $plugin_Silverlight = Piwik_Common::getRequestVar('ag', 0, 'int', $this->request); + $plugin_Cookie = Piwik_Common::getRequestVar('cookie', 0, 'int', $this->request); + + $userAgent = $this->getUserAgent($this->request); + $aBrowserInfo = UserAgentParser::getBrowser($userAgent); + + $browserName = ($aBrowserInfo !== false && $aBrowserInfo['id'] !== false) ? $aBrowserInfo['id'] : 'UNK'; + $browserVersion = ($aBrowserInfo !== false && $aBrowserInfo['version'] !== false) ? $aBrowserInfo['version'] : ''; + + $os = UserAgentParser::getOperatingSystem($userAgent); + $os = $os === false ? 'UNK' : $os['id']; + + $resolution = Piwik_Common::getRequestVar('res', 'unknown', 'string', $this->request); + + $browserLang = $this->getBrowserLanguage(); + + $configurationHash = $this->getConfigHash( + $os, + $browserName, + $browserVersion, + $resolution, + $plugin_Flash, + $plugin_Java, + $plugin_Director, + $plugin_Quicktime, + $plugin_RealPlayer, + $plugin_PDF, + $plugin_WindowsMedia, + $plugin_Gears, + $plugin_Silverlight, + $plugin_Cookie, + $this->getVisitorIp(), + $browserLang); + + $this->userSettingsInformation = array( + 'config_id' => $configurationHash, + 'config_os' => $os, + 'config_browser_name' => $browserName, + 'config_browser_version' => $browserVersion, + 'config_resolution' => $resolution, + 'config_pdf' => $plugin_PDF, + 'config_flash' => $plugin_Flash, + 'config_java' => $plugin_Java, + 'config_director' => $plugin_Director, + 'config_quicktime' => $plugin_Quicktime, + 'config_realplayer' => $plugin_RealPlayer, + 'config_windowsmedia' => $plugin_WindowsMedia, + 'config_gears' => $plugin_Gears, + 'config_silverlight' => $plugin_Silverlight, + 'config_cookie' => $plugin_Cookie, + 'location_browser_lang' => $browserLang, + ); + + return $this->userSettingsInformation; + } + + /** + * Returns true if the last action was done during the last 30 minutes + * @return bool + */ + protected function isLastActionInTheSameVisit() + { + return isset($this->visitorInfo['visit_last_action_time']) + && ($this->visitorInfo['visit_last_action_time'] + > ($this->getCurrentTimestamp() - Piwik_Config::getInstance()->Tracker['visit_standard_length'])); + } + + /** + * Returns true if the recognizeTheVisitor() method did recognize the visitor + * @return bool + */ + protected function isVisitorKnown() + { + return $this->visitorKnown === true; + } + + /** + * Update the cookie information. + */ + protected function setThirdPartyCookie() + { + if (!$this->shouldUseThirdPartyCookie()) { + return; + } + printDebug("We manage the cookie..."); + + // idcookie has been generated in handleNewVisit or we simply propagate the old value + $this->cookie->set(0, bin2hex($this->visitorInfo['idvisitor'])); + $this->cookie->save(); + } + + /** + * Returns an object able to handle the current action + * Plugins can return an override Action that for example, does not record the action in the DB + * + * @throws Exception + * @return Piwik_Tracker_Action child or fake but with same public interface + */ + protected function newAction() + { + $action = null; + Piwik_PostEvent('Tracker.newAction', $action); + + if (is_null($action)) { + $action = new Piwik_Tracker_Action(); + } elseif (!($action instanceof Piwik_Tracker_Action_Interface)) { + throw new Exception("The Action object set in the plugin must implement the interface Piwik_Tracker_Action_Interface"); + } + return $action; + } + + /** + * Detect whether action is an outlink given host aliases + * + * @param Piwik_Tracker_Action_Interface $action + * @return bool true if the outlink the visitor clicked on points to one of the known hosts for this website + */ + protected function detectActionIsOutlinkOnAliasHost(Piwik_Tracker_Action_Interface $action) + { + if ($action->getActionType() != Piwik_Tracker_Action_Interface::TYPE_OUTLINK) { + return false; + } + $decodedActionUrl = $action->getActionUrl(); + $actionUrlParsed = @parse_url($decodedActionUrl); + if (!isset($actionUrlParsed['host'])) { + return false; + } + return Piwik_Tracker_Visit::isHostKnownAliasHost($actionUrlParsed['host'], $this->idsite); + } + + /** + * Returns a 64-bit hash of all the configuration settings + * @param $os + * @param $browserName + * @param $browserVersion + * @param $resolution + * @param $plugin_Flash + * @param $plugin_Java + * @param $plugin_Director + * @param $plugin_Quicktime + * @param $plugin_RealPlayer + * @param $plugin_PDF + * @param $plugin_WindowsMedia + * @param $plugin_Gears + * @param $plugin_Silverlight + * @param $plugin_Cookie + * @param $ip + * @param $browserLang + * @return string + */ + protected function getConfigHash($os, $browserName, $browserVersion, $resolution, $plugin_Flash, $plugin_Java, $plugin_Director, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF, $plugin_WindowsMedia, $plugin_Gears, $plugin_Silverlight, $plugin_Cookie, $ip, $browserLang) + { + $hash = md5($os . $browserName . $browserVersion . $plugin_Flash . $plugin_Java . $plugin_Director . $plugin_Quicktime . $plugin_RealPlayer . $plugin_PDF . $plugin_WindowsMedia . $plugin_Gears . $plugin_Silverlight . $plugin_Cookie . $ip . $browserLang, $raw_output = true); + return Piwik_Common::substr($hash, 0, Piwik_Tracker::LENGTH_BINARY_ID); + } + + /** + * Returns either + * - "-1" for a known visitor + * - at least 16 char identifier in hex @see Piwik_Common::generateUniqId() + * @return int|string + */ + protected function getVisitorUniqueId() + { + if ($this->isVisitorKnown()) { + return -1; + } + return Piwik_Common::generateUniqId(); + } + + protected function setCookie($cookie) + { + $this->cookie = $cookie; + } + + // is the referer host any of the registered URLs for this website? + static public function isHostKnownAliasHost($urlHost, $idSite) + { + $websiteData = Piwik_Tracker_Cache::getCacheWebsiteAttributes($idSite); + if (isset($websiteData['hosts'])) { + $canonicalHosts = array(); + foreach ($websiteData['hosts'] as $host) { + $canonicalHosts[] = str_replace('www.', '', mb_strtolower($host, 'UTF-8')); + } + $canonicalHost = str_replace('www.', '', mb_strtolower($urlHost, 'UTF-8')); + if (in_array($canonicalHost, $canonicalHosts)) { + return true; + } + } + return false; + } } /** @@ -1638,273 +1540,256 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface */ class Piwik_Tracker_Visit_Referer { - // @see detect*() referer methods - protected $typeRefererAnalyzed; - protected $nameRefererAnalyzed; - protected $keywordRefererAnalyzed; - protected $refererHost; - protected $refererUrl; - protected $refererUrlParse; - protected $currentUrlParse; - protected $idsite; - - // Used to prefix when a adsense referer is detected - const LABEL_PREFIX_ADSENSE_KEYWORD = '(adsense) '; - - - /** - * Returns an array containing the following information: - * - referer_type - * - direct -- absence of referer URL OR referer URL has the same host - * - site -- based on the referer URL - * - search_engine -- based on the referer URL - * - campaign -- based on campaign URL parameter - * - * - referer_name - * - () - * - piwik.net -- site host name - * - google.fr -- search engine host name - * - adwords-search -- campaign name - * - * - referer_keyword - * - () - * - () - * - my keyword - * - my paid keyword - * - () - * - () - * - * - referer_url : the same for all the referer types - * - * @param $refererUrl must be URL Encoded - * @param $currentUrl - * @param $idSite - * @return array - */ - public function getRefererInformation($refererUrl, $currentUrl, $idSite) - { - $this->idsite = $idSite; - - // default values for the referer_* fields - $refererUrl = Piwik_Common::unsanitizeInputValue($refererUrl); - if(!empty($refererUrl) - && !Piwik_Common::isLookLikeUrl($refererUrl)) - { - $refererUrl = ''; - } - - $currentUrl = Piwik_Tracker_Action::cleanupUrl($currentUrl); - - $this->refererUrl = $refererUrl; - $this->refererUrlParse = @parse_url($this->refererUrl); - $this->currentUrlParse = @parse_url($currentUrl); - $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; - $this->nameRefererAnalyzed = ''; - $this->keywordRefererAnalyzed = ''; - $this->refererHost = ''; - - if(isset($this->refererUrlParse['host'])) - { - $this->refererHost = $this->refererUrlParse['host']; - } - - $refererDetected = false; - - if( !empty($this->currentUrlParse['host']) - && $this->detectRefererCampaign() ) - { - $refererDetected = true; - } - - if(!$refererDetected) - { - if( $this->detectRefererDirectEntry() - || $this->detectRefererSearchEngine() ) - { - $refererDetected = true; - } - } - - if(!empty($this->refererHost) - && !$refererDetected) - { - $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_WEBSITE; - $this->nameRefererAnalyzed = mb_strtolower($this->refererHost, 'UTF-8'); - } - - $refererInformation = array( - 'referer_type' => $this->typeRefererAnalyzed, - 'referer_name' => $this->nameRefererAnalyzed, - 'referer_keyword' => $this->keywordRefererAnalyzed, - 'referer_url' => $this->refererUrl, - ); - - return $refererInformation; - } - - /** - * Search engine detection + // @see detect*() referer methods + protected $typeRefererAnalyzed; + protected $nameRefererAnalyzed; + protected $keywordRefererAnalyzed; + protected $refererHost; + protected $refererUrl; + protected $refererUrlParse; + protected $currentUrlParse; + protected $idsite; + + // Used to prefix when a adsense referer is detected + const LABEL_PREFIX_ADSENSE_KEYWORD = '(adsense) '; + + + /** + * Returns an array containing the following information: + * - referer_type + * - direct -- absence of referer URL OR referer URL has the same host + * - site -- based on the referer URL + * - search_engine -- based on the referer URL + * - campaign -- based on campaign URL parameter + * + * - referer_name + * - () + * - piwik.net -- site host name + * - google.fr -- search engine host name + * - adwords-search -- campaign name + * + * - referer_keyword + * - () + * - () + * - my keyword + * - my paid keyword + * - () + * - () + * + * - referer_url : the same for all the referer types + * + * @param $refererUrl must be URL Encoded + * @param $currentUrl + * @param $idSite + * @return array + */ + public function getRefererInformation($refererUrl, $currentUrl, $idSite) + { + $this->idsite = $idSite; + + // default values for the referer_* fields + $refererUrl = Piwik_Common::unsanitizeInputValue($refererUrl); + if (!empty($refererUrl) + && !Piwik_Common::isLookLikeUrl($refererUrl) + ) { + $refererUrl = ''; + } + + $currentUrl = Piwik_Tracker_Action::cleanupUrl($currentUrl); + + $this->refererUrl = $refererUrl; + $this->refererUrlParse = @parse_url($this->refererUrl); + $this->currentUrlParse = @parse_url($currentUrl); + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; + $this->nameRefererAnalyzed = ''; + $this->keywordRefererAnalyzed = ''; + $this->refererHost = ''; + + if (isset($this->refererUrlParse['host'])) { + $this->refererHost = $this->refererUrlParse['host']; + } + + $refererDetected = false; + + if (!empty($this->currentUrlParse['host']) + && $this->detectRefererCampaign() + ) { + $refererDetected = true; + } + + if (!$refererDetected) { + if ($this->detectRefererDirectEntry() + || $this->detectRefererSearchEngine() + ) { + $refererDetected = true; + } + } + + if (!empty($this->refererHost) + && !$refererDetected + ) { + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_WEBSITE; + $this->nameRefererAnalyzed = mb_strtolower($this->refererHost, 'UTF-8'); + } + + $refererInformation = array( + 'referer_type' => $this->typeRefererAnalyzed, + 'referer_name' => $this->nameRefererAnalyzed, + 'referer_keyword' => $this->keywordRefererAnalyzed, + 'referer_url' => $this->refererUrl, + ); + + return $refererInformation; + } + + /** + * Search engine detection * @return bool - */ - protected function detectRefererSearchEngine() - { - $searchEngineInformation = Piwik_Common::extractSearchEngineInformationFromUrl($this->refererUrl); - Piwik_PostEvent('Tracker.detectRefererSearchEngine', $searchEngineInformation, $this->refererUrl); - if($searchEngineInformation === false) - { - return false; - } - $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_SEARCH_ENGINE; - $this->nameRefererAnalyzed = $searchEngineInformation['name']; - $this->keywordRefererAnalyzed = $searchEngineInformation['keywords']; - return true; - } + */ + protected function detectRefererSearchEngine() + { + $searchEngineInformation = Piwik_Common::extractSearchEngineInformationFromUrl($this->refererUrl); + Piwik_PostEvent('Tracker.detectRefererSearchEngine', $searchEngineInformation, $this->refererUrl); + if ($searchEngineInformation === false) { + return false; + } + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_SEARCH_ENGINE; + $this->nameRefererAnalyzed = $searchEngineInformation['name']; + $this->keywordRefererAnalyzed = $searchEngineInformation['keywords']; + return true; + } /** * @param string $string * @return bool */ protected function detectCampaignFromString($string) - { - foreach($this->campaignNames as $campaignNameParameter) - { - $campaignName = trim(urldecode(Piwik_Common::getParameterFromQueryString($string, $campaignNameParameter))); - if( !empty($campaignName)) - { - break; - } - } - - if(!empty($campaignName)) - { - $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_CAMPAIGN; - $this->nameRefererAnalyzed = $campaignName; - - foreach($this->campaignKeywords as $campaignKeywordParameter) - { - $campaignKeyword = Piwik_Common::getParameterFromQueryString($string, $campaignKeywordParameter); - if( !empty($campaignKeyword)) - { - $this->keywordRefererAnalyzed = trim(urldecode($campaignKeyword)); - break; - } - } - - // if the campaign keyword is empty, try to get a keyword from the referrer URL - if (empty($this->keywordRefererAnalyzed)) - { - // Set the Campaign keyword to the keyword found in the Referer URL if any - $referrerUrlInfo = Piwik_Common::extractSearchEngineInformationFromUrl($this->refererUrl); - if (!empty($referrerUrlInfo['keywords'])) - { - $this->keywordRefererAnalyzed = $referrerUrlInfo['keywords']; - } - - // Set the keyword, to the hostname found, in a Adsense Referer URL '&url=' parameter - if(empty($this->keywordRefererAnalyzed) - && !empty($this->refererUrlParse['query']) - && !empty($this->refererHost) - && (strpos($this->refererHost, 'google') !== false || strpos($this->refererHost,'doubleclick') !== false) - ) - { - // This parameter sometimes is found & contains the page with the adsense ad bringing visitor to our site - $adsenseReferrerParameter = 'url'; - $value = trim(urldecode(Piwik_Common::getParameterFromQueryString($this->refererUrlParse['query'], $adsenseReferrerParameter))); - if(!empty($value)) - { - $parsedAdsenseReferrerUrl = parse_url($value); - if(!empty($parsedAdsenseReferrerUrl['host'])) - { - $this->keywordRefererAnalyzed = self::LABEL_PREFIX_ADSENSE_KEYWORD . $parsedAdsenseReferrerUrl['host']; - } - } - } - - // or we default to the referrer hostname otherwise - if(empty($this->keywordRefererAnalyzed)) - { - $this->keywordRefererAnalyzed = $this->refererHost; - } - } - - return true; - } - return false; - } - - /** - * Campaign analysis + { + foreach ($this->campaignNames as $campaignNameParameter) { + $campaignName = trim(urldecode(Piwik_Common::getParameterFromQueryString($string, $campaignNameParameter))); + if (!empty($campaignName)) { + break; + } + } + + if (!empty($campaignName)) { + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_CAMPAIGN; + $this->nameRefererAnalyzed = $campaignName; + + foreach ($this->campaignKeywords as $campaignKeywordParameter) { + $campaignKeyword = Piwik_Common::getParameterFromQueryString($string, $campaignKeywordParameter); + if (!empty($campaignKeyword)) { + $this->keywordRefererAnalyzed = trim(urldecode($campaignKeyword)); + break; + } + } + + // if the campaign keyword is empty, try to get a keyword from the referrer URL + if (empty($this->keywordRefererAnalyzed)) { + // Set the Campaign keyword to the keyword found in the Referer URL if any + $referrerUrlInfo = Piwik_Common::extractSearchEngineInformationFromUrl($this->refererUrl); + if (!empty($referrerUrlInfo['keywords'])) { + $this->keywordRefererAnalyzed = $referrerUrlInfo['keywords']; + } + + // Set the keyword, to the hostname found, in a Adsense Referer URL '&url=' parameter + if (empty($this->keywordRefererAnalyzed) + && !empty($this->refererUrlParse['query']) + && !empty($this->refererHost) + && (strpos($this->refererHost, 'google') !== false || strpos($this->refererHost, 'doubleclick') !== false) + ) { + // This parameter sometimes is found & contains the page with the adsense ad bringing visitor to our site + $adsenseReferrerParameter = 'url'; + $value = trim(urldecode(Piwik_Common::getParameterFromQueryString($this->refererUrlParse['query'], $adsenseReferrerParameter))); + if (!empty($value)) { + $parsedAdsenseReferrerUrl = parse_url($value); + if (!empty($parsedAdsenseReferrerUrl['host'])) { + $this->keywordRefererAnalyzed = self::LABEL_PREFIX_ADSENSE_KEYWORD . $parsedAdsenseReferrerUrl['host']; + } + } + } + + // or we default to the referrer hostname otherwise + if (empty($this->keywordRefererAnalyzed)) { + $this->keywordRefererAnalyzed = $this->refererHost; + } + } + + return true; + } + return false; + } + + /** + * Campaign analysis * @return bool - */ - protected function detectRefererCampaign() - { - if(!isset($this->currentUrlParse['query']) - && !isset($this->currentUrlParse['fragment'])) - { - return false; - } - $campaignParameters = Piwik_Common::getCampaignParameters(); - $this->campaignNames = $campaignParameters[0]; - $this->campaignKeywords = $campaignParameters[1]; - - $found = false; - - // 1) Detect campaign from query string - if(isset($this->currentUrlParse['query'])) - { - $found = $this->detectCampaignFromString($this->currentUrlParse['query']); - } - - // 2) Detect from fragment #hash - if(!$found - && isset($this->currentUrlParse['fragment'])) - { - $found = $this->detectCampaignFromString($this->currentUrlParse['fragment']); - } - return $found; - } - - /** - * We have previously tried to detect the campaign variables in the URL - * so at this stage, if the referer host is the current host, - * or if the referer host is any of the registered URL for this website, - * it is considered a direct entry + */ + protected function detectRefererCampaign() + { + if (!isset($this->currentUrlParse['query']) + && !isset($this->currentUrlParse['fragment']) + ) { + return false; + } + $campaignParameters = Piwik_Common::getCampaignParameters(); + $this->campaignNames = $campaignParameters[0]; + $this->campaignKeywords = $campaignParameters[1]; + + $found = false; + + // 1) Detect campaign from query string + if (isset($this->currentUrlParse['query'])) { + $found = $this->detectCampaignFromString($this->currentUrlParse['query']); + } + + // 2) Detect from fragment #hash + if (!$found + && isset($this->currentUrlParse['fragment']) + ) { + $found = $this->detectCampaignFromString($this->currentUrlParse['fragment']); + } + return $found; + } + + /** + * We have previously tried to detect the campaign variables in the URL + * so at this stage, if the referer host is the current host, + * or if the referer host is any of the registered URL for this website, + * it is considered a direct entry * @return bool - */ - protected function detectRefererDirectEntry() - { - if(!empty($this->refererHost)) - { - // is the referer host the current host? - if(isset($this->currentUrlParse['host'])) - { - $currentHost = mb_strtolower($this->currentUrlParse['host'], 'UTF-8'); - if($currentHost == mb_strtolower($this->refererHost, 'UTF-8')) - { - $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; - return true; - } - } - if(Piwik_Tracker_Visit::isHostKnownAliasHost($this->refererHost, $this->idsite)) - { - $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; - return true; - } - } - return false; - } + */ + protected function detectRefererDirectEntry() + { + if (!empty($this->refererHost)) { + // is the referer host the current host? + if (isset($this->currentUrlParse['host'])) { + $currentHost = mb_strtolower($this->currentUrlParse['host'], 'UTF-8'); + if ($currentHost == mb_strtolower($this->refererHost, 'UTF-8')) { + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; + return true; + } + } + if (Piwik_Tracker_Visit::isHostKnownAliasHost($this->refererHost, $this->idsite)) { + $this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY; + return true; + } + } + return false; + } } /** * @package Piwik * @subpackage Piwik_Tracker */ -class Piwik_Tracker_Visit_VisitorNotFoundInDatabase extends Exception { +class Piwik_Tracker_Visit_VisitorNotFoundInDatabase extends Exception +{ } /** * @package Piwik * @subpackage Piwik_Tracker */ -class Piwik_Tracker_Visit_Excluded extends Exception { +class Piwik_Tracker_Visit_Excluded extends Exception +{ } diff --git a/core/Translate.php b/core/Translate.php index 693c4cc916..e19ea422fc 100644 --- a/core/Translate.php +++ b/core/Translate.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -14,178 +14,167 @@ */ class Piwik_Translate { - static private $instance = null; - static private $languageToLoad = null; - private $loadedLanguage = false; - - /** - * @return Piwik_Translate - */ - static public function getInstance() - { - if (self::$instance == null) - { - self::$instance = new self; - } - return self::$instance; - } - - public function loadEnglishTranslation() - { - $this->loadTranslation('en'); - } - - public function unloadEnglishTranslation() - { - $GLOBALS['Piwik_translations'] = array(); - } - - public function reloadLanguage($language = false) - { - if(empty($language)) - { - $language = $this->getLanguageToLoad(); - } - $this->unloadEnglishTranslation(); - $this->loadEnglishTranslation(); - $this->loadCoreTranslation($language); - Piwik_PluginsManager::getInstance()->loadPluginTranslations($language); - } - - /** - * Reads the specified code translation file in memory. - * - * @param bool|string $language 2 letter language code. If not specified, will detect current user translation, or load default translation. - * @return void - */ - public function loadCoreTranslation($language = false) - { - if(empty($language)) - { - $language = $this->getLanguageToLoad(); - } - if($this->loadedLanguage == $language) - { - return; - } - $this->loadTranslation($language); - } - - private function loadTranslation($language) - { - $path = PIWIK_INCLUDE_PATH . '/lang/' . $language . '.php'; - if(!Piwik_Common::isValidFilename($language) || !is_readable($path)) - { - throw new Exception(Piwik_TranslateException('General_ExceptionLanguageFileNotFound', array($language))); - } - require $path; - $this->mergeTranslationArray($translations); - $this->setLocale(); - $this->loadedLanguage = $language; - } - - public function mergeTranslationArray($translation) - { - if(!isset($GLOBALS['Piwik_translations'])) - { - $GLOBALS['Piwik_translations'] = array(); - } - // we could check that no string overlap here - $GLOBALS['Piwik_translations'] = array_merge($GLOBALS['Piwik_translations'], array_filter($translation, 'strlen')); - } - - /** - * @return string the language filename prefix, eg 'en' for english - * @throws exception if the language set is not a valid filename - */ - public function getLanguageToLoad() - { - if(is_null(self::$languageToLoad)) - { - $lang = Piwik_Common::getRequestVar('language', '', 'string'); - - Piwik_PostEvent('Translate.getLanguageToLoad', $lang); - - self::$languageToLoad = $lang; - } - - return self::$languageToLoad; - } - - /** Reset the cached language to load. Used in tests. */ - static public function reset() - { - self::$languageToLoad = null; - } - - public function getLanguageLoaded() - { - return $this->loadedLanguage; - } - - public function getLanguageDefault() - { - return Piwik_Config::getInstance()->General['default_language']; - } - - /** - * Generate javascript translations array - * - * @param array $moduleList - * @return string containing javascript code with translations array (including <script> tag) - */ - public function getJavascriptTranslations(array $moduleList) - { - if(!in_array('General', $moduleList)) - { - $moduleList[] = 'General'; - } - - $js = 'var translations = {'; - - $moduleRegex = '#^('; - foreach($moduleList as $module) - { - $moduleRegex .= $module.'|'; - } - $moduleRegex = substr($moduleRegex, 0, -1); - $moduleRegex .= ')_.*_js$#i'; - - // Hack: common translations used in JS but not only, force as them to be defined in JS - $translations = $GLOBALS['Piwik_translations']; - $toSetInJs = array('General_Save', 'General_OrCancel'); - foreach($toSetInJs as $toSetId) - { - $translations[$toSetId.'_js'] = $translations[$toSetId]; - } - foreach($translations as $key => $value) - { - if( preg_match($moduleRegex,$key) ) { - $js .= '"'.$key.'": "'.str_replace('"','\"',$value).'",'; - } - } - $js = substr($js,0,-1); - $js .= '};'; - $js .= "\n".'if(typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }'. - '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 suffixed with _js and that you called %7BloadJavascriptTranslations plugins=\'\$YOUR_PLUGIN_NAME\'%7D before your javascript code.";}'; - return $js; - } - - /** - * Set locale - * - * @see http://php.net/setlocale - */ - private function setLocale() - { - $locale = $GLOBALS['Piwik_translations']['General_Locale']; - $locale_variant = str_replace('UTF-8', 'UTF8', $locale); - setlocale(LC_ALL, $locale, $locale_variant); - setlocale(LC_CTYPE, ''); - } + static private $instance = null; + static private $languageToLoad = null; + private $loadedLanguage = false; + + /** + * @return Piwik_Translate + */ + static public function getInstance() + { + if (self::$instance == null) { + self::$instance = new self; + } + return self::$instance; + } + + public function loadEnglishTranslation() + { + $this->loadTranslation('en'); + } + + public function unloadEnglishTranslation() + { + $GLOBALS['Piwik_translations'] = array(); + } + + public function reloadLanguage($language = false) + { + if (empty($language)) { + $language = $this->getLanguageToLoad(); + } + $this->unloadEnglishTranslation(); + $this->loadEnglishTranslation(); + $this->loadCoreTranslation($language); + Piwik_PluginsManager::getInstance()->loadPluginTranslations($language); + } + + /** + * Reads the specified code translation file in memory. + * + * @param bool|string $language 2 letter language code. If not specified, will detect current user translation, or load default translation. + * @return void + */ + public function loadCoreTranslation($language = false) + { + if (empty($language)) { + $language = $this->getLanguageToLoad(); + } + if ($this->loadedLanguage == $language) { + return; + } + $this->loadTranslation($language); + } + + private function loadTranslation($language) + { + $path = PIWIK_INCLUDE_PATH . '/lang/' . $language . '.php'; + if (!Piwik_Common::isValidFilename($language) || !is_readable($path)) { + throw new Exception(Piwik_TranslateException('General_ExceptionLanguageFileNotFound', array($language))); + } + require $path; + $this->mergeTranslationArray($translations); + $this->setLocale(); + $this->loadedLanguage = $language; + } + + public function mergeTranslationArray($translation) + { + if (!isset($GLOBALS['Piwik_translations'])) { + $GLOBALS['Piwik_translations'] = array(); + } + // we could check that no string overlap here + $GLOBALS['Piwik_translations'] = array_merge($GLOBALS['Piwik_translations'], array_filter($translation, 'strlen')); + } + + /** + * @return string the language filename prefix, eg 'en' for english + * @throws exception if the language set is not a valid filename + */ + public function getLanguageToLoad() + { + if (is_null(self::$languageToLoad)) { + $lang = Piwik_Common::getRequestVar('language', '', 'string'); + + Piwik_PostEvent('Translate.getLanguageToLoad', $lang); + + self::$languageToLoad = $lang; + } + + return self::$languageToLoad; + } + + /** Reset the cached language to load. Used in tests. */ + static public function reset() + { + self::$languageToLoad = null; + } + + public function getLanguageLoaded() + { + return $this->loadedLanguage; + } + + public function getLanguageDefault() + { + return Piwik_Config::getInstance()->General['default_language']; + } + + /** + * Generate javascript translations array + * + * @param array $moduleList + * @return string containing javascript code with translations array (including <script> tag) + */ + public function getJavascriptTranslations(array $moduleList) + { + if (!in_array('General', $moduleList)) { + $moduleList[] = 'General'; + } + + $js = 'var translations = {'; + + $moduleRegex = '#^('; + foreach ($moduleList as $module) { + $moduleRegex .= $module . '|'; + } + $moduleRegex = substr($moduleRegex, 0, -1); + $moduleRegex .= ')_.*_js$#i'; + + // Hack: common translations used in JS but not only, force as them to be defined in JS + $translations = $GLOBALS['Piwik_translations']; + $toSetInJs = array('General_Save', 'General_OrCancel'); + foreach ($toSetInJs as $toSetId) { + $translations[$toSetId . '_js'] = $translations[$toSetId]; + } + foreach ($translations as $key => $value) { + if (preg_match($moduleRegex, $key)) { + $js .= '"' . $key . '": "' . str_replace('"', '\"', $value) . '",'; + } + } + $js = substr($js, 0, -1); + $js .= '};'; + $js .= "\n" . 'if(typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }' . + '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 suffixed with _js and that you called %7BloadJavascriptTranslations plugins=\'\$YOUR_PLUGIN_NAME\'%7D before your javascript code.";}'; + return $js; + } + + /** + * Set locale + * + * @see http://php.net/setlocale + */ + private function setLocale() + { + $locale = $GLOBALS['Piwik_translations']['General_Locale']; + $locale_variant = str_replace('UTF-8', 'UTF8', $locale); + setlocale(LC_ALL, $locale, $locale_variant); + setlocale(LC_CTYPE, ''); + } } /** @@ -197,19 +186,16 @@ class Piwik_Translate */ function Piwik_Translate($string, $args = array()) { - if(!is_array($args)) - { - $args = array($args); - } - if(isset($GLOBALS['Piwik_translations'][$string])) - { - $string = $GLOBALS['Piwik_translations'][$string]; - } - if(count($args) == 0) - { - return $string; - } - return vsprintf($string, $args); + if (!is_array($args)) { + $args = array($args); + } + if (isset($GLOBALS['Piwik_translations'][$string])) { + $string = $GLOBALS['Piwik_translations'][$string]; + } + if (count($args) == 0) { + return $string; + } + return vsprintf($string, $args); } /** @@ -222,11 +208,9 @@ function Piwik_Translate($string, $args = array()) */ function Piwik_TranslateException($message, $args = array()) { - try { - return Piwik_Translate($message, $args); - } - catch(Exception $e) - { - return $message; - } + try { + return Piwik_Translate($message, $args); + } catch (Exception $e) { + return $message; + } } diff --git a/core/TranslationWriter.php b/core/TranslationWriter.php index 911044a96f..e2fbb9578c 100644 --- a/core/TranslationWriter.php +++ b/core/TranslationWriter.php @@ -1,13 +1,13 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik - * + * */ /** @@ -17,120 +17,118 @@ */ class Piwik_TranslationWriter { - static private $baseTranslation = null; + static private $baseTranslation = null; - /** - * Clean a string that may contain HTML special chars, single/double quotes, HTML entities, leading/trailing whitespace - * - * @param string $s - * @return string - */ - static public function clean($s) - { - return html_entity_decode(trim($s), ENT_QUOTES, 'UTF-8'); - } + /** + * Clean a string that may contain HTML special chars, single/double quotes, HTML entities, leading/trailing whitespace + * + * @param string $s + * @return string + */ + static public function clean($s) + { + return html_entity_decode(trim($s), ENT_QUOTES, 'UTF-8'); + } - /** - * Quote a "C" string - * - * @param string $s - * @return string - */ - static public function quote($s) - { - return "'" . addcslashes($s, "'") . "'"; - } + /** + * Quote a "C" string + * + * @param string $s + * @return string + */ + static public function quote($s) + { + return "'" . addcslashes($s, "'") . "'"; + } - /** - * Get translation file path - * - * @param string $lang ISO 639-1 alpha-2 language code - * @param string $base Optional base directory (either 'lang' or 'tmp') - * @throws Exception - * @return string path - */ - static public function getTranslationPath($lang, $base = 'lang') - { - if(!Piwik_Common::isValidFilename($lang) || - ($base !== 'lang' && $base !== 'tmp')) - { - throw new Exception(Piwik_TranslateException('General_ExceptionLanguageFileNotFound', array($lang))); - } + /** + * Get translation file path + * + * @param string $lang ISO 639-1 alpha-2 language code + * @param string $base Optional base directory (either 'lang' or 'tmp') + * @throws Exception + * @return string path + */ + static public function getTranslationPath($lang, $base = 'lang') + { + if (!Piwik_Common::isValidFilename($lang) || + ($base !== 'lang' && $base !== 'tmp') + ) { + throw new Exception(Piwik_TranslateException('General_ExceptionLanguageFileNotFound', array($lang))); + } - return PIWIK_INCLUDE_PATH . '/' . $base . '/' . $lang . '.php'; - } + return PIWIK_INCLUDE_PATH . '/' . $base . '/' . $lang . '.php'; + } - /** - * Load translations from file - * - * @param string $lang ISO 639-1 alpha-2 language code - * @throws Exception - * @return array $translations Array of translations ( key => translated string ) - */ - static public function loadTranslation($lang) - { - $path = self::getTranslationPath($lang); - if(!is_readable($path)) - { - throw new Exception(Piwik_TranslateException('General_ExceptionLanguageFileNotFound', array($lang))); - } + /** + * Load translations from file + * + * @param string $lang ISO 639-1 alpha-2 language code + * @throws Exception + * @return array $translations Array of translations ( key => translated string ) + */ + static public function loadTranslation($lang) + { + $path = self::getTranslationPath($lang); + if (!is_readable($path)) { + throw new Exception(Piwik_TranslateException('General_ExceptionLanguageFileNotFound', array($lang))); + } - require $path; - return $translations; - } + require $path; + return $translations; + } - /** - * Output translations to string - * - * @param array $translations Array of translations ( key => translated string ) - * @return string - */ - static public function outputTranslation($translations) - { - if(!self::$baseTranslation) - { - self::$baseTranslation = self::loadTranslation('en'); - } - $en = self::$baseTranslation; + /** + * Output translations to string + * + * @param array $translations Array of translations ( key => translated string ) + * @return string + */ + static public function outputTranslation($translations) + { + if (!self::$baseTranslation) { + self::$baseTranslation = self::loadTranslation('en'); + } + $en = self::$baseTranslation; - $output = array('<?php', '$translations = array('); - $deferoutput = array(); + $output = array('<?php', '$translations = array('); + $deferoutput = array(); - /* write all the translations that exist in en.php */ - foreach($en as $key => $en_translation) { - if(isset($translations[$key]) && !empty($translations[$key])) { - $tmp = self::quote($translations[$key]); - $output[] = "\t'$key' => $tmp,"; - } - } + /* write all the translations that exist in en.php */ + foreach ($en as $key => $en_translation) { + if (isset($translations[$key]) && !empty($translations[$key])) { + $tmp = self::quote($translations[$key]); + $output[] = "\t'$key' => $tmp,"; + } + } - /* write the remaining translations (that don't exist in en.php) */ - foreach($translations as $key => $translation) { - if(!isset($en[$key]) && !empty($translation)) { - $tmp = self::quote($translation); - $deferoutput[] = "\t'$key' => $tmp,"; - } - } + /* write the remaining translations (that don't exist in en.php) */ + foreach ($translations as $key => $translation) { + if (!isset($en[$key]) && !empty($translation)) { + $tmp = self::quote($translation); + $deferoutput[] = "\t'$key' => $tmp,"; + } + } - if(count($deferoutput) > 0) { - $output[] = "\n\t// FOR REVIEW"; - $output = array_merge($output, $deferoutput); - } + if (count($deferoutput) > 0) { + $output[] = "\n\t// FOR REVIEW"; + $output = array_merge($output, $deferoutput); + } - $output[] = ');'; + $output[] = ');'; - return implode($output, "\n") . "\n"; - } + return implode($output, "\n") . "\n"; + } - /** - * Save translations to file; translations should already be cleansed. - * - * @param array $translations Array of translations ( key => translated string ) - * @param string $destinationPath Path of file to save translations to - * @return bool|int False if failure, or number of bytes written - */ - static public function saveTranslation($translations, $destinationPath) - { - return file_put_contents($destinationPath, self::outputTranslation($translations)); - } + /** + * Save translations to file; translations should already be cleansed. + * + * @param array $translations Array of translations ( key => translated string ) + * @param string $destinationPath Path of file to save translations to + * @return bool|int False if failure, or number of bytes written + */ + static public function saveTranslation($translations, $destinationPath) + { + return file_put_contents($destinationPath, self::outputTranslation($translations)); + } } diff --git a/core/Unzip.php b/core/Unzip.php index 5b21542519..ed915b27f7 100644 --- a/core/Unzip.php +++ b/core/Unzip.php @@ -1,7 +1,7 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @@ -16,34 +16,33 @@ */ class Piwik_Unzip { - /** - * Factory method to create an unarchiver - * - * @param string $name Name of unarchiver - * @param string $filename Name of .zip archive - * @return Piwik_Unzip_Interface - */ - static public function factory($name, $filename) - { - switch($name) - { - case 'ZipArchive': - if(class_exists('ZipArchive', false)) - return new Piwik_Unzip_ZipArchive($filename); - break; - case 'tar.gz': - return new Piwik_Unzip_Tar($filename, 'gz'); - case 'tar.bz2': - return new Piwik_Unzip_Tar($filename, 'bz2'); - case 'gz': - if (function_exists('gzopen')) - return new Piwik_Unzip_Gzip($filename); - break; - case 'PclZip': - default: - return new Piwik_Unzip_PclZip($filename); - } - - return new Piwik_Unzip_PclZip($filename); - } + /** + * Factory method to create an unarchiver + * + * @param string $name Name of unarchiver + * @param string $filename Name of .zip archive + * @return Piwik_Unzip_Interface + */ + static public function factory($name, $filename) + { + switch ($name) { + case 'ZipArchive': + if (class_exists('ZipArchive', false)) + return new Piwik_Unzip_ZipArchive($filename); + break; + case 'tar.gz': + return new Piwik_Unzip_Tar($filename, 'gz'); + case 'tar.bz2': + return new Piwik_Unzip_Tar($filename, 'bz2'); + case 'gz': + if (function_exists('gzopen')) + return new Piwik_Unzip_Gzip($filename); + break; + case 'PclZip': + default: + return new Piwik_Unzip_PclZip($filename); + } + + return new Piwik_Unzip_PclZip($filename); + } } diff --git a/core/Unzip/Gzip.php b/core/Unzip/Gzip.php index 71badb87b4..b7d7c14a35 100755 --- a/core/Unzip/Gzip.php +++ b/core/Unzip/Gzip.php @@ -1,7 +1,7 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @@ -11,76 +11,73 @@ /** * Unzip implementation for .gz files. - * + * * @package Piwik * @subpackage Piwik_Unzip */ class Piwik_Unzip_Gzip implements Piwik_Unzip_Interface { - /** - * Name of .gz file. - * - * @var string - */ - private $filename = null; - - /** - * Error string. - * - * @var string - */ - private $error = null; - - /** - * Constructor. - * - * @param string $filename Name of .gz file. - */ - public function __construct($filename) - { - $this->filename = $filename; - } - - /** - * Extracts the contents of the .gz file to $pathExtracted. - * - * @param string $pathExtracted Must be file, not directory. - * @return bool true if successful, false if otherwise. - */ - public function extract($pathExtracted) - { - $file = gzopen($this->filename, 'r'); - if ($file === false) - { - $this->error = "gzopen failed"; - return false; - } - - $output = fopen($pathExtracted, 'w'); - while (!feof($file)) - { - fwrite($output, fread($file, 1024*1024)); - } - fclose($output); - - $success = gzclose($file); - if ($success === false) - { - $this->error = "gzclose failed"; - return false; - } - - return true; - } + /** + * Name of .gz file. + * + * @var string + */ + private $filename = null; + + /** + * Error string. + * + * @var string + */ + private $error = null; + + /** + * Constructor. + * + * @param string $filename Name of .gz file. + */ + public function __construct($filename) + { + $this->filename = $filename; + } + + /** + * Extracts the contents of the .gz file to $pathExtracted. + * + * @param string $pathExtracted Must be file, not directory. + * @return bool true if successful, false if otherwise. + */ + public function extract($pathExtracted) + { + $file = gzopen($this->filename, 'r'); + if ($file === false) { + $this->error = "gzopen failed"; + return false; + } + + $output = fopen($pathExtracted, 'w'); + while (!feof($file)) { + fwrite($output, fread($file, 1024 * 1024)); + } + fclose($output); + + $success = gzclose($file); + if ($success === false) { + $this->error = "gzclose failed"; + return false; + } + + return true; + } - /** - * Get error status string for the latest error. - * - * @return string - */ - public function errorInfo() - { - return $this->error; - } + /** + * Get error status string for the latest error. + * + * @return string + */ + public function errorInfo() + { + return $this->error; + } } diff --git a/core/Unzip/Interface.php b/core/Unzip/Interface.php index 4935789f02..5c9bccd82d 100644 --- a/core/Unzip/Interface.php +++ b/core/Unzip/Interface.php @@ -1,7 +1,7 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @@ -17,25 +17,25 @@ */ interface Piwik_Unzip_Interface { - /** - * Constructor - * - * @param string $filename Name of the .zip archive - */ - public function __construct($filename); + /** + * Constructor + * + * @param string $filename Name of the .zip archive + */ + public function __construct($filename); - /** - * Extract files from archive to target directory - * - * @param string $pathExtracted Absolute path of target directory - * @return mixed Array of filenames if successful; or 0 if an error occurred - */ - public function extract($pathExtracted); + /** + * Extract files from archive to target directory + * + * @param string $pathExtracted Absolute path of target directory + * @return mixed Array of filenames if successful; or 0 if an error occurred + */ + public function extract($pathExtracted); - /** - * Get error status string for the latest error - * - * @return string - */ - public function errorInfo(); + /** + * Get error status string for the latest error + * + * @return string + */ + public function errorInfo(); } diff --git a/core/Unzip/PclZip.php b/core/Unzip/PclZip.php index 1bcb83c29d..d6351cdca9 100644 --- a/core/Unzip/PclZip.php +++ b/core/Unzip/PclZip.php @@ -1,7 +1,7 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @@ -22,69 +22,71 @@ require_once PIWIK_INCLUDE_PATH . '/libs/PclZip/pclzip.lib.php'; */ class Piwik_Unzip_PclZip implements Piwik_Unzip_Interface { - /** - * @var PclZip - */ - private $pclzip; - /** - * @var string - */ - public $filename; + /** + * @var PclZip + */ + private $pclzip; + /** + * @var string + */ + public $filename; - /** - * Constructor - * - * @param string $filename Name of the .zip archive - */ - public function __construct($filename) { - $this->pclzip = new PclZip($filename); - $this->filename = $filename; - } + /** + * Constructor + * + * @param string $filename Name of the .zip archive + */ + public function __construct($filename) + { + $this->pclzip = new PclZip($filename); + $this->filename = $filename; + } - /** - * Extract files from archive to target directory - * - * @param string $pathExtracted Absolute path of target directory - * @return mixed Array of filenames if successful; or 0 if an error occurred - */ - public function extract($pathExtracted) { - $pathExtracted = str_replace('\\', '/', $pathExtracted); - $list = $this->pclzip->listContent(); - if (empty($list)) - { - return 0; - } + /** + * Extract files from archive to target directory + * + * @param string $pathExtracted Absolute path of target directory + * @return mixed Array of filenames if successful; or 0 if an error occurred + */ + public function extract($pathExtracted) + { + $pathExtracted = str_replace('\\', '/', $pathExtracted); + $list = $this->pclzip->listContent(); + if (empty($list)) { + return 0; + } - foreach($list as $entry) { - $filename = str_replace('\\', '/', $entry['stored_filename']); - $parts = explode('/', $filename); + foreach ($list as $entry) { + $filename = str_replace('\\', '/', $entry['stored_filename']); + $parts = explode('/', $filename); - if(!strncmp($filename, '/', 1) || - array_search('..', $parts) !== false || - strpos($filename, ':') !== false) - { - return 0; - } - } + if (!strncmp($filename, '/', 1) || + array_search('..', $parts) !== false || + strpos($filename, ':') !== false + ) { + return 0; + } + } - // PCLZIP_CB_PRE_EXTRACT callback returns 0 to skip, 1 to resume, or 2 to abort - return $this->pclzip->extract( - PCLZIP_OPT_PATH, $pathExtracted, - PCLZIP_OPT_STOP_ON_ERROR, - PCLZIP_OPT_REPLACE_NEWER, - PCLZIP_CB_PRE_EXTRACT, create_function( - '$p_event, &$p_header', - "return strncmp(\$p_header['filename'], '$pathExtracted', strlen('$pathExtracted')) ? 0 : 1;" - ) - ); - } + // PCLZIP_CB_PRE_EXTRACT callback returns 0 to skip, 1 to resume, or 2 to abort + return $this->pclzip->extract( + PCLZIP_OPT_PATH, $pathExtracted, + PCLZIP_OPT_STOP_ON_ERROR, + PCLZIP_OPT_REPLACE_NEWER, + PCLZIP_CB_PRE_EXTRACT, create_function( + '$p_event, &$p_header', + "return strncmp(\$p_header['filename'], '$pathExtracted', strlen('$pathExtracted')) ? 0 : 1;" + ) + ); + } - /** - * Get error status string for the latest error - * - * @return string - */ - public function errorInfo() { - return $this->pclzip->errorInfo(true); - } + /** + * Get error status string for the latest error + * + * @return string + */ + public function errorInfo() + { + return $this->pclzip->errorInfo(true); + } } diff --git a/core/Unzip/Tar.php b/core/Unzip/Tar.php index e565b12482..ad7aebf6b9 100755 --- a/core/Unzip/Tar.php +++ b/core/Unzip/Tar.php @@ -1,7 +1,7 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @@ -16,70 +16,70 @@ require_once PIWIK_INCLUDE_PATH . '/libs/Archive_Tar/Tar.php'; /** * Unzip implementation for Archive_Tar PEAR lib. - * + * * @package Piwik * @subpackage Piwik_Unzip */ class Piwik_Unzip_Tar implements Piwik_Unzip_Interface { - /** - * Archive_Tar instance. - * - * @var Archive_Tar - */ - private $tarArchive = null; - - /** - * Constructor. - * - * @param string $filename Path to tar file. - * @param string|null $compression Either 'gz', 'bz2' or null for no compression. - */ - public function __construct($filename, $compression = null) - { - $this->tarArchive = new Archive_Tar($filename, $compression); - } - - /** - * Extracts the contents of the tar file to $pathExtracted. - * - * @param string $pathExtracted Directory to extract into. - * @return bool true if successful, false if otherwise. - */ - public function extract($pathExtracted) - { - return $this->tarArchive->extract($pathExtracted); - } - - /** - * Extracts one file held in a tar archive and returns the deflated file - * as a string. - * - * @param string $inArchivePath Path to file in the tar archive. - * @return bool true if successful, false if otherwise. - */ - public function extractInString($inArchivePath) - { - return $this->tarArchive->extractInString($inArchivePath); - } - - /** - * Lists the files held in the tar archive. - * - * @return array List of paths describing everything held in the tar archive. - */ - public function listContent() - { - return $this->tarArchive->listContent(); - } + /** + * Archive_Tar instance. + * + * @var Archive_Tar + */ + private $tarArchive = null; + + /** + * Constructor. + * + * @param string $filename Path to tar file. + * @param string|null $compression Either 'gz', 'bz2' or null for no compression. + */ + public function __construct($filename, $compression = null) + { + $this->tarArchive = new Archive_Tar($filename, $compression); + } + + /** + * Extracts the contents of the tar file to $pathExtracted. + * + * @param string $pathExtracted Directory to extract into. + * @return bool true if successful, false if otherwise. + */ + public function extract($pathExtracted) + { + return $this->tarArchive->extract($pathExtracted); + } + + /** + * Extracts one file held in a tar archive and returns the deflated file + * as a string. + * + * @param string $inArchivePath Path to file in the tar archive. + * @return bool true if successful, false if otherwise. + */ + public function extractInString($inArchivePath) + { + return $this->tarArchive->extractInString($inArchivePath); + } + + /** + * Lists the files held in the tar archive. + * + * @return array List of paths describing everything held in the tar archive. + */ + public function listContent() + { + return $this->tarArchive->listContent(); + } - /** - * Get error status string for the latest error. - * - * @return string - */ - public function errorInfo() - { - return $this->tarArchive->error_object->getMessage(); - } + /** + * Get error status string for the latest error. + * + * @return string + */ + public function errorInfo() + { + return $this->tarArchive->error_object->getMessage(); + } } diff --git a/core/Unzip/ZipArchive.php b/core/Unzip/ZipArchive.php index eb25a79cd4..c542f5f259 100644 --- a/core/Unzip/ZipArchive.php +++ b/core/Unzip/ZipArchive.php @@ -1,7 +1,7 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later * @@ -17,117 +17,117 @@ */ class Piwik_Unzip_ZipArchive implements Piwik_Unzip_Interface { - /** - * @var ZipArchive - */ - private $ziparchive; - /** - * @var string - */ - public $filename; + /** + * @var ZipArchive + */ + private $ziparchive; + /** + * @var string + */ + public $filename; - /** - * Constructor - * - * @param string $filename Name of the .zip archive - * @throws Exception - */ - public function __construct($filename) { - $this->filename = $filename; - $this->ziparchive = new ZipArchive; - if($this->ziparchive->open($filename) !== true) { - throw new Exception('Error opening '.$filename); - } - } + /** + * Constructor + * + * @param string $filename Name of the .zip archive + * @throws Exception + */ + public function __construct($filename) + { + $this->filename = $filename; + $this->ziparchive = new ZipArchive; + if ($this->ziparchive->open($filename) !== true) { + throw new Exception('Error opening ' . $filename); + } + } - /** - * Extract files from archive to target directory - * - * @param string $pathExtracted Absolute path of target directory - * @return mixed Array of filenames if successful; or 0 if an error occurred - */ - public function extract($pathExtracted) { - if(substr_compare($pathExtracted, '/', -1)) - $pathExtracted .= '/'; + /** + * Extract files from archive to target directory + * + * @param string $pathExtracted Absolute path of target directory + * @return mixed Array of filenames if successful; or 0 if an error occurred + */ + public function extract($pathExtracted) + { + if (substr_compare($pathExtracted, '/', -1)) + $pathExtracted .= '/'; - $fileselector = array(); - $list = array(); - $count = $this->ziparchive->numFiles; - if ($count === 0) - { - return 0; - } + $fileselector = array(); + $list = array(); + $count = $this->ziparchive->numFiles; + if ($count === 0) { + return 0; + } - for($i = 0; $i < $count; $i++) { - $entry = $this->ziparchive->statIndex($i); + for ($i = 0; $i < $count; $i++) { + $entry = $this->ziparchive->statIndex($i); - $filename = str_replace('\\', '/', $entry['name']); - $parts = explode('/', $filename); + $filename = str_replace('\\', '/', $entry['name']); + $parts = explode('/', $filename); - if(!strncmp($filename, '/', 1) || - array_search('..', $parts) !== false || - strpos($filename, ':') !== false) - { - return 0; - } - $fileselector[] = $entry['name']; - $list[] = array( - 'filename' => $pathExtracted . $entry['name'], - 'stored_filename' => $entry['name'], - 'size' => $entry['size'], - 'compressed_size' => $entry['comp_size'], - 'mtime' => $entry['mtime'], - 'index' => $i, - 'crc' => $entry['crc'], - ); - } + if (!strncmp($filename, '/', 1) || + array_search('..', $parts) !== false || + strpos($filename, ':') !== false + ) { + return 0; + } + $fileselector[] = $entry['name']; + $list[] = array( + 'filename' => $pathExtracted . $entry['name'], + 'stored_filename' => $entry['name'], + 'size' => $entry['size'], + 'compressed_size' => $entry['comp_size'], + 'mtime' => $entry['mtime'], + 'index' => $i, + 'crc' => $entry['crc'], + ); + } - $res = $this->ziparchive->extractTo($pathExtracted, $fileselector); - if($res === false) - return 0; - return $list; - } + $res = $this->ziparchive->extractTo($pathExtracted, $fileselector); + if ($res === false) + return 0; + return $list; + } - /** - * Get error status string for the latest error - * - * @return string - */ - public function errorInfo() { - static $statusStrings = array( - ZIPARCHIVE::ER_OK => 'No error', - ZIPARCHIVE::ER_MULTIDISK => 'Multi-disk zip archives not supported', - ZIPARCHIVE::ER_RENAME => 'Renaming temporary file failed', - ZIPARCHIVE::ER_CLOSE => 'Closing zip archive failed', - ZIPARCHIVE::ER_SEEK => 'Seek error', - ZIPARCHIVE::ER_READ => 'Read error', - ZIPARCHIVE::ER_WRITE => 'Write error', - ZIPARCHIVE::ER_CRC => 'CRC error', - ZIPARCHIVE::ER_ZIPCLOSED => 'Containing zip archive was closed', - ZIPARCHIVE::ER_NOENT => 'No such file', - ZIPARCHIVE::ER_EXISTS => 'File already exists', - ZIPARCHIVE::ER_OPEN => 'Can\'t open file', - ZIPARCHIVE::ER_TMPOPEN => 'Failure to create temporary file', - ZIPARCHIVE::ER_ZLIB => 'Zlib error', - ZIPARCHIVE::ER_MEMORY => 'Malloc failure', - ZIPARCHIVE::ER_CHANGED => 'Entry has been changed', - ZIPARCHIVE::ER_COMPNOTSUPP => 'Compression method not supported', - ZIPARCHIVE::ER_EOF => 'Premature EOF', - ZIPARCHIVE::ER_INVAL => 'Invalid argument', - ZIPARCHIVE::ER_NOZIP => 'Not a zip archive', - ZIPARCHIVE::ER_INTERNAL => 'Internal error', - ZIPARCHIVE::ER_INCONS => 'Zip archive inconsistent', - ZIPARCHIVE::ER_REMOVE => 'Can\'t remove file', - ZIPARCHIVE::ER_DELETED => 'Entry has been deleted', - ); + /** + * Get error status string for the latest error + * + * @return string + */ + public function errorInfo() + { + static $statusStrings = array( + ZIPARCHIVE::ER_OK => 'No error', + ZIPARCHIVE::ER_MULTIDISK => 'Multi-disk zip archives not supported', + ZIPARCHIVE::ER_RENAME => 'Renaming temporary file failed', + ZIPARCHIVE::ER_CLOSE => 'Closing zip archive failed', + ZIPARCHIVE::ER_SEEK => 'Seek error', + ZIPARCHIVE::ER_READ => 'Read error', + ZIPARCHIVE::ER_WRITE => 'Write error', + ZIPARCHIVE::ER_CRC => 'CRC error', + ZIPARCHIVE::ER_ZIPCLOSED => 'Containing zip archive was closed', + ZIPARCHIVE::ER_NOENT => 'No such file', + ZIPARCHIVE::ER_EXISTS => 'File already exists', + ZIPARCHIVE::ER_OPEN => 'Can\'t open file', + ZIPARCHIVE::ER_TMPOPEN => 'Failure to create temporary file', + ZIPARCHIVE::ER_ZLIB => 'Zlib error', + ZIPARCHIVE::ER_MEMORY => 'Malloc failure', + ZIPARCHIVE::ER_CHANGED => 'Entry has been changed', + ZIPARCHIVE::ER_COMPNOTSUPP => 'Compression method not supported', + ZIPARCHIVE::ER_EOF => 'Premature EOF', + ZIPARCHIVE::ER_INVAL => 'Invalid argument', + ZIPARCHIVE::ER_NOZIP => 'Not a zip archive', + ZIPARCHIVE::ER_INTERNAL => 'Internal error', + ZIPARCHIVE::ER_INCONS => 'Zip archive inconsistent', + ZIPARCHIVE::ER_REMOVE => 'Can\'t remove file', + ZIPARCHIVE::ER_DELETED => 'Entry has been deleted', + ); - if(isset($statusStrings[$this->ziparchive->status])) { - $statusString = $statusStrings[$this->ziparchive->status]; - } - else - { - $statusString = 'Unknown status'; - } - return $statusString . '(' . $this->ziparchive->status . ')'; - } + if (isset($statusStrings[$this->ziparchive->status])) { + $statusString = $statusStrings[$this->ziparchive->status]; + } else { + $statusString = 'Unknown status'; + } + return $statusString . '(' . $this->ziparchive->status . ')'; + } } diff --git a/core/UpdateCheck.php b/core/UpdateCheck.php index 2597ea2956..bbe2ad786d 100644 --- a/core/UpdateCheck.php +++ b/core/UpdateCheck.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -14,79 +14,76 @@ * * @package Piwik */ -class Piwik_UpdateCheck +class Piwik_UpdateCheck { - const CHECK_INTERVAL = 28800; // every 8 hours - const UI_CLICK_CHECK_INTERVAL = 10; // every 10s when user clicks UI link - const LAST_TIME_CHECKED = 'UpdateCheck_LastTimeChecked'; - const LATEST_VERSION = 'UpdateCheck_LatestVersion'; - const SOCKET_TIMEOUT = 2; + const CHECK_INTERVAL = 28800; // every 8 hours + const UI_CLICK_CHECK_INTERVAL = 10; // every 10s when user clicks UI link + const LAST_TIME_CHECKED = 'UpdateCheck_LastTimeChecked'; + const LATEST_VERSION = 'UpdateCheck_LatestVersion'; + const SOCKET_TIMEOUT = 2; + + /** + * Check for a newer version + * + * @param bool $force Force check + */ + public static function check($force = false, $interval = null) + { + if ($interval === null) { + $interval = self::CHECK_INTERVAL; + } + + $lastTimeChecked = Piwik_GetOption(self::LAST_TIME_CHECKED); + if ($force + || $lastTimeChecked === false + || time() - $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' => PHP_VERSION, + 'url' => Piwik_Url::getCurrentUrlWithoutQueryString(), + 'trigger' => Piwik_Common::getRequestVar('module', '', 'string'), + 'timezone' => Piwik_SitesManager_API::getInstance()->getDefaultTimezone(), + ); + + $url = Piwik_Config::getInstance()->General['api_service_url'] + . '/1.0/getLatestVersion/' + . '?' . http_build_query($parameters, '', '&'); + $timeout = self::SOCKET_TIMEOUT; + + if (@Piwik_Config::getInstance()->Debug['allow_upgrades_to_beta']) { + $url = 'http://builds.piwik.org/LATEST_BETA'; + } - /** - * Check for a newer version - * - * @param bool $force Force check - */ - public static function check($force = false, $interval = null) - { - if ($interval === null) - { - $interval = self::CHECK_INTERVAL; - } - - $lastTimeChecked = Piwik_GetOption(self::LAST_TIME_CHECKED); - if($force - || $lastTimeChecked === false - || time() - $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' => PHP_VERSION, - 'url' => Piwik_Url::getCurrentUrlWithoutQueryString(), - 'trigger' => Piwik_Common::getRequestVar('module','','string'), - 'timezone' => Piwik_SitesManager_API::getInstance()->getDefaultTimezone(), - ); + try { + $latestVersion = Piwik_Http::sendHttpRequest($url, $timeout); + if (!preg_match('~^[0-9][0-9a-zA-Z_.-]*$~D', $latestVersion)) { + $latestVersion = ''; + } + } catch (Exception $e) { + // e.g., disable_functions = fsockopen; allow_url_open = Off + $latestVersion = ''; + } + Piwik_SetOption(self::LATEST_VERSION, $latestVersion); + } + } - $url = Piwik_Config::getInstance()->General['api_service_url'] - . '/1.0/getLatestVersion/' - . '?' . http_build_query($parameters, '', '&'); - $timeout = self::SOCKET_TIMEOUT; - - if(@Piwik_Config::getInstance()->Debug['allow_upgrades_to_beta']) - { - $url = 'http://builds.piwik.org/LATEST_BETA'; - } - - try { - $latestVersion = Piwik_Http::sendHttpRequest($url, $timeout); - if (!preg_match('~^[0-9][0-9a-zA-Z_.-]*$~D', $latestVersion)) - { - $latestVersion = ''; - } - } catch(Exception $e) { - // e.g., disable_functions = fsockopen; allow_url_open = Off - $latestVersion = ''; - } - Piwik_SetOption(self::LATEST_VERSION, $latestVersion); - } - } - - /** - * Returns version number of a newer Piwik release. - * - * @return string|false false if current version is the latest available, - * or the latest version number if a newest release is available - */ - public static function isNewestVersionAvailable() - { - $latestVersion = Piwik_GetOption(self::LATEST_VERSION); - if(!empty($latestVersion) - && version_compare(Piwik_Version::VERSION, $latestVersion) == -1) - { - return $latestVersion; - } - return false; - } + /** + * Returns version number of a newer Piwik release. + * + * @return string|false false if current version is the latest available, + * or the latest version number if a newest release is available + */ + public static function isNewestVersionAvailable() + { + $latestVersion = Piwik_GetOption(self::LATEST_VERSION); + if (!empty($latestVersion) + && version_compare(Piwik_Version::VERSION, $latestVersion) == -1 + ) { + return $latestVersion; + } + return false; + } } diff --git a/core/Updater.php b/core/Updater.php index 47dec55198..1b91f70349 100644 --- a/core/Updater.php +++ b/core/Updater.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -22,310 +22,282 @@ require_once PIWIK_INCLUDE_PATH . '/core/Option.php'; */ class Piwik_Updater { - const INDEX_CURRENT_VERSION = 0; - const INDEX_NEW_VERSION = 1; - - public $pathUpdateFileCore; - public $pathUpdateFilePlugins; - private $componentsToCheck = array(); - private $hasMajorDbUpdate = false; - - public function __construct() - { - $this->pathUpdateFileCore = PIWIK_INCLUDE_PATH . '/core/Updates/'; - $this->pathUpdateFilePlugins = PIWIK_INCLUDE_PATH . '/plugins/%s/Updates/'; - } + const INDEX_CURRENT_VERSION = 0; + const INDEX_NEW_VERSION = 1; + + public $pathUpdateFileCore; + public $pathUpdateFilePlugins; + private $componentsToCheck = array(); + private $hasMajorDbUpdate = false; + + public function __construct() + { + $this->pathUpdateFileCore = PIWIK_INCLUDE_PATH . '/core/Updates/'; + $this->pathUpdateFilePlugins = PIWIK_INCLUDE_PATH . '/plugins/%s/Updates/'; + } + + /** + * Add component to check + * + * @param string $name + * @param string $version + */ + public function addComponentToCheck($name, $version) + { + $this->componentsToCheck[$name] = $version; + } + + /** + * Record version of successfully completed component update + * + * @param string $name + * @param string $version + */ + public function recordComponentSuccessfullyUpdated($name, $version) + { + try { + 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, [...]), [...]) + */ + public function getComponentsWithUpdateFile() + { + $this->componentsWithNewVersion = $this->getComponentsWithNewVersion(); + $this->componentsWithUpdateFile = $this->loadComponentsWithUpdateFile(); + return $this->componentsWithUpdateFile; + } + + /** + * 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]); + } - /** - * Add component to check - * - * @param string $name - * @param string $version - */ - public function addComponentToCheck($name, $version) - { - $this->componentsToCheck[$name] = $version; - } - - /** - * Record version of successfully completed component update - * - * @param string $name - * @param string $version - */ - public function recordComponentSuccessfullyUpdated($name, $version) - { - try { - 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, [...]), [...]) - */ - public function getComponentsWithUpdateFile() - { - $this->componentsWithNewVersion = $this->getComponentsWithNewVersion(); - $this->componentsWithUpdateFile = $this->loadComponentsWithUpdateFile(); - return $this->componentsWithUpdateFile; - } + /** + * Does one of the new versions involve a major database update? + * Note: getSqlQueriesToExecute() must be called before this method! + * + * @return bool + */ + public function hasMajorDbUpdate() + { + return $this->hasMajorDbUpdate; + } - /** - * 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 - /** - * Does one of the new versions involve a major database update? - * Note: getSqlQueriesToExecute() must be called before this method! - * - * @return bool - */ - public function hasMajorDbUpdate() - { - return $this->hasMajorDbUpdate; - } + $className = $this->getUpdateClassName($componentName, $fileVersion); + if (class_exists($className, false)) { + $queriesForComponent = call_user_func(array($className, 'getSql')); + foreach ($queriesForComponent as $query => $error) { + $queries[] = $query . ';'; + } - /** - * 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 + $this->hasMajorDbUpdate = $this->hasMajorDbUpdate || call_user_func(array($className, 'isMajorUpdate')); + } + } + // 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; + } - $className = $this->getUpdateClassName($componentName, $fileVersion); - if(class_exists($className, false)) - { - $queriesForComponent = call_user_func( array($className, 'getSql')); - foreach($queriesForComponent as $query => $error) { - $queries[] = $query.';'; - } - - $this->hasMajorDbUpdate = $this->hasMajorDbUpdate || call_user_func( array($className, 'isMajorUpdate')); - } - } - // 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; - } + 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 $componentName 'core', or plugin name - * @throws Exception|Piwik_Updater_UpdateErrorException - * @return array of warning strings if applicable - */ - public function update($componentName) - { - $warningMessages = array(); - foreach($this->componentsWithUpdateFile[$componentName] as $file => $fileVersion) - { - try { - require_once $file; // prefixed by PIWIK_INCLUDE_PATH + /** + * Update the named component + * + * @param string $componentName 'core', or plugin name + * @throws Exception|Piwik_Updater_UpdateErrorException + * @return array of warning strings if applicable + */ + public function update($componentName) + { + $warningMessages = array(); + foreach ($this->componentsWithUpdateFile[$componentName] as $file => $fileVersion) { + try { + require_once $file; // prefixed by PIWIK_INCLUDE_PATH - $className = $this->getUpdateClassName($componentName, $fileVersion); - if(class_exists($className, false)) - { - call_user_func( array($className, 'update') ); - } + $className = $this->getUpdateClassName($componentName, $fileVersion); + if (class_exists($className, false)) { + call_user_func(array($className, 'update')); + } - $this->recordComponentSuccessfullyUpdated($componentName, $fileVersion); - } catch( Piwik_Updater_UpdateErrorException $e) { - throw $e; - } catch( Exception $e) { - $warningMessages[] = $e->getMessage(); - } - } - - // 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($componentName, $this->componentsWithNewVersion[$componentName][self::INDEX_NEW_VERSION]); - return $warningMessages; - } + $this->recordComponentSuccessfullyUpdated($componentName, $fileVersion); + } catch (Piwik_Updater_UpdateErrorException $e) { + throw $e; + } catch (Exception $e) { + $warningMessages[] = $e->getMessage(); + } + } - /** - * Construct list of update files for the outdated components - * - * @return array( componentName => array( file1 => version1, [...]), [...]) - */ - private function loadComponentsWithUpdateFile() - { - $componentsWithUpdateFile = array(); - foreach($this->componentsWithNewVersion as $name => $versions) - { - $currentVersion = $versions[self::INDEX_CURRENT_VERSION]; - $newVersion = $versions[self::INDEX_NEW_VERSION]; - - if($name == 'core') - { - $pathToUpdates = $this->pathUpdateFileCore . '*.php'; - } - else - { - $pathToUpdates = sprintf($this->pathUpdateFilePlugins, $name) . '*.php'; - } - - $files = _glob( $pathToUpdates ); - if($files == false) - { - $files = array(); - } + // 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($componentName, $this->componentsWithNewVersion[$componentName][self::INDEX_NEW_VERSION]); + return $warningMessages; + } - foreach( $files as $file) - { - $fileVersion = basename($file, '.php'); - 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; - } - } - - if(isset($componentsWithUpdateFile[$name])) - { - // order the update files by version asc - uasort($componentsWithUpdateFile[$name], "version_compare"); - } - else - { - // there are no update file => nothing to do, update to the new version is successful - $this->recordComponentSuccessfullyUpdated($name, $newVersion); - } - } - return $componentsWithUpdateFile; - } + /** + * Construct list of update files for the outdated components + * + * @return array( componentName => array( file1 => version1, [...]), [...]) + */ + private function loadComponentsWithUpdateFile() + { + $componentsWithUpdateFile = array(); + foreach ($this->componentsWithNewVersion as $name => $versions) { + $currentVersion = $versions[self::INDEX_CURRENT_VERSION]; + $newVersion = $versions[self::INDEX_NEW_VERSION]; - /** - * Construct list of outdated components - * - * @throws Exception - * @return array array( componentName => array( oldVersion, newVersion), [...]) - */ - public function getComponentsWithNewVersion() - { - $componentsToUpdate = array(); - - // we make sure core updates are processed before any plugin updates - if(isset($this->componentsToCheck['core'])) - { - $coreVersions = $this->componentsToCheck['core']; - unset($this->componentsToCheck['core']); - $this->componentsToCheck = array_merge( array('core' => $coreVersions), $this->componentsToCheck); - } - - foreach($this->componentsToCheck as $name => $version) - { - try { - $currentVersion = Piwik_GetOption('version_'.$name); - } catch( Exception $e) { - // mysql error 1146: table doesn't exist - if(Zend_Registry::get('db')->isErrNo($e, '1146')) - { - // case when the option table is not yet created (before 0.2.10) - $currentVersion = false; - } - else - { - // failed for some other reason - throw $e; - } - } - if($currentVersion === false) - { - if($name === 'core') - { - // This should not happen - $currentVersion = Piwik_Version::VERSION; - } - else - { - $currentVersion = '0.0.1'; - } - $this->recordComponentSuccessfullyUpdated($name, $currentVersion); - } + if ($name == 'core') { + $pathToUpdates = $this->pathUpdateFileCore . '*.php'; + } else { + $pathToUpdates = sprintf($this->pathUpdateFilePlugins, $name) . '*.php'; + } - $versionCompare = version_compare($currentVersion, $version); - if($versionCompare == -1) - { - $componentsToUpdate[$name] = array( - self::INDEX_CURRENT_VERSION => $currentVersion, - self::INDEX_NEW_VERSION => $version - ); - } - else if($versionCompare == 1) - { - // the version in the DB is newest.. we choose to ignore (for the time being) - } - } - return $componentsToUpdate; - } + $files = _glob($pathToUpdates); + if ($files == false) { + $files = array(); + } - /** - * Performs database update(s) - * - * @param string $file Update script filename - * @param array $sqlarray An array of SQL queries to be executed - * @throws Piwik_Updater_UpdateErrorException - */ - static function updateDatabase($file, $sqlarray) - { - foreach($sqlarray as $update => $ignoreError) - { - try { - Piwik_Exec( $update ); - } catch(Exception $e) { - 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); - } - } - } - } + foreach ($files as $file) { + $fileVersion = basename($file, '.php'); + 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; + } + } + + if (isset($componentsWithUpdateFile[$name])) { + // order the update files by version asc + uasort($componentsWithUpdateFile[$name], "version_compare"); + } else { + // there are no update file => nothing to do, update to the new version is successful + $this->recordComponentSuccessfullyUpdated($name, $newVersion); + } + } + return $componentsWithUpdateFile; + } + + /** + * Construct list of outdated components + * + * @throws Exception + * @return array array( componentName => array( oldVersion, newVersion), [...]) + */ + public function getComponentsWithNewVersion() + { + $componentsToUpdate = array(); + + // we make sure core updates are processed before any plugin updates + if (isset($this->componentsToCheck['core'])) { + $coreVersions = $this->componentsToCheck['core']; + unset($this->componentsToCheck['core']); + $this->componentsToCheck = array_merge(array('core' => $coreVersions), $this->componentsToCheck); + } + + foreach ($this->componentsToCheck as $name => $version) { + try { + $currentVersion = Piwik_GetOption('version_' . $name); + } catch (Exception $e) { + // mysql error 1146: table doesn't exist + if (Zend_Registry::get('db')->isErrNo($e, '1146')) { + // case when the option table is not yet created (before 0.2.10) + $currentVersion = false; + } else { + // failed for some other reason + throw $e; + } + } + if ($currentVersion === false) { + if ($name === 'core') { + // This should not happen + $currentVersion = Piwik_Version::VERSION; + } else { + $currentVersion = '0.0.1'; + } + $this->recordComponentSuccessfullyUpdated($name, $currentVersion); + } + + $versionCompare = version_compare($currentVersion, $version); + if ($versionCompare == -1) { + $componentsToUpdate[$name] = array( + self::INDEX_CURRENT_VERSION => $currentVersion, + self::INDEX_NEW_VERSION => $version + ); + } else if ($versionCompare == 1) { + // the version in the DB is newest.. we choose to ignore (for the time being) + } + } + return $componentsToUpdate; + } + + /** + * Performs database update(s) + * + * @param string $file Update script filename + * @param array $sqlarray An array of SQL queries to be executed + * @throws Piwik_Updater_UpdateErrorException + */ + static function updateDatabase($file, $sqlarray) + { + foreach ($sqlarray as $update => $ignoreError) { + try { + Piwik_Exec($update); + } catch (Exception $e) { + 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); + } + } + } + } } /** @@ -334,4 +306,6 @@ class Piwik_Updater * @package Piwik * @subpackage Piwik_Updater */ -class Piwik_Updater_UpdateErrorException extends Exception {} +class Piwik_Updater_UpdateErrorException extends Exception +{ +} diff --git a/core/Updates.php b/core/Updates.php index 386f0abc89..91bdca17e7 100644 --- a/core/Updates.php +++ b/core/Updates.php @@ -17,102 +17,100 @@ */ abstract class Piwik_Updates { - /** - * Return SQL to be executed in this update - * - * @param string $schema Schema name - * @return array( - * 'ALTER .... ' => '1234', // if the query fails, it will be ignored if the error code is 1234 - * 'ALTER .... ' => false, // if an error occurs, the update will stop and fail - * // and user will have to manually run the query - * ) - */ - static function getSql($schema = 'Myisam') - { - return array(); - } - - /** - * Incremental version update - */ - static function update() - { - } - - /** - * Tell the updater that this is a major update. - * Leads to a more visible notice. - */ - static function isMajorUpdate() - { - return false; - } - - /** - * Helper method to enable maintenance mode during large updates - */ - static function enableMaintenanceMode() - { - $config = Piwik_Config::getInstance(); - $config->init(); - - $tracker = $config->Tracker; - $tracker['record_statistics'] = 0; - $config->Tracker = $tracker; - - $general = $config->General; - $general['maintenance_mode'] = 1; - $config->General = $general; - - $config->forceSave(); - } - - /** - * Helper method to disable maintenance mode after large updates - */ - static function disableMaintenanceMode() - { - $config = Piwik_Config::getInstance(); - $config->init(); - - $tracker = $config->Tracker; - $tracker['record_statistics'] = 1; - $config->Tracker = $tracker; - - $general = $config->General; - $general['maintenance_mode'] = 0; - $config->General = $general; - - $config->forceSave(); - } - - - - public static function deletePluginFromConfigFile($pluginToDelete) - { - $config = Piwik_Config::getInstance(); - $config->init(); - if (isset($config->Plugins['Plugins'])) - { - $plugins = $config->Plugins['Plugins']; - if (($key = array_search($pluginToDelete, $plugins)) !== false) { - unset($plugins[$key]); - } - $config->Plugins['Plugins'] = $plugins; - - $pluginsInstalled = $config->PluginsInstalled['PluginsInstalled']; - if (($key = array_search($pluginToDelete, $pluginsInstalled)) !== false) { - unset($pluginsInstalled[$key]); - } - $config->PluginsInstalled = $pluginsInstalled; - - $config->forceSave(); - } - } - - public static function deletePluginFromFilesystem($plugin) - { - Piwik::unlinkRecursive( PIWIK_INCLUDE_PATH . '/plugins/' . $plugin, $deleteRootToo = true); - } + /** + * Return SQL to be executed in this update + * + * @param string $schema Schema name + * @return array( + * 'ALTER .... ' => '1234', // if the query fails, it will be ignored if the error code is 1234 + * 'ALTER .... ' => false, // if an error occurs, the update will stop and fail + * // and user will have to manually run the query + * ) + */ + static function getSql($schema = 'Myisam') + { + return array(); + } + + /** + * Incremental version update + */ + static function update() + { + } + + /** + * Tell the updater that this is a major update. + * Leads to a more visible notice. + */ + static function isMajorUpdate() + { + return false; + } + + /** + * Helper method to enable maintenance mode during large updates + */ + static function enableMaintenanceMode() + { + $config = Piwik_Config::getInstance(); + $config->init(); + + $tracker = $config->Tracker; + $tracker['record_statistics'] = 0; + $config->Tracker = $tracker; + + $general = $config->General; + $general['maintenance_mode'] = 1; + $config->General = $general; + + $config->forceSave(); + } + + /** + * Helper method to disable maintenance mode after large updates + */ + static function disableMaintenanceMode() + { + $config = Piwik_Config::getInstance(); + $config->init(); + + $tracker = $config->Tracker; + $tracker['record_statistics'] = 1; + $config->Tracker = $tracker; + + $general = $config->General; + $general['maintenance_mode'] = 0; + $config->General = $general; + + $config->forceSave(); + } + + + public static function deletePluginFromConfigFile($pluginToDelete) + { + $config = Piwik_Config::getInstance(); + $config->init(); + if (isset($config->Plugins['Plugins'])) { + $plugins = $config->Plugins['Plugins']; + if (($key = array_search($pluginToDelete, $plugins)) !== false) { + unset($plugins[$key]); + } + $config->Plugins['Plugins'] = $plugins; + + $pluginsInstalled = $config->PluginsInstalled['PluginsInstalled']; + if (($key = array_search($pluginToDelete, $pluginsInstalled)) !== false) { + unset($pluginsInstalled[$key]); + } + $config->PluginsInstalled = $pluginsInstalled; + + $config->forceSave(); + } + } + + public static function deletePluginFromFilesystem($plugin) + { + Piwik::unlinkRecursive(PIWIK_INCLUDE_PATH . '/plugins/' . $plugin, $deleteRootToo = true); + } } diff --git a/core/Updates/0.2.10.php b/core/Updates/0.2.10.php index a411c2ad4e..aece273630 100644 --- a/core/Updates/0.2.10.php +++ b/core/Updates/0.2.10.php @@ -14,59 +14,56 @@ */ class Piwik_Updates_0_2_10 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'CREATE TABLE `'. Piwik_Common::prefixTable('option') .'` ( + static function getSql($schema = 'Myisam') + { + return array( + 'CREATE TABLE `' . Piwik_Common::prefixTable('option') . '` ( idoption BIGINT NOT NULL AUTO_INCREMENT , option_name VARCHAR( 64 ) NOT NULL , option_value LONGTEXT NOT NULL , PRIMARY KEY ( idoption , option_name ) - )' => false, + )' => false, - // 0.1.7 [463] - 'ALTER IGNORE TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + // 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') .'` + // 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, + 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', - ); - } + // 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()); + 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); - } + $obsoleteFile = '/plugins/ExamplePlugin/API.php'; + if (file_exists(PIWIK_INCLUDE_PATH . $obsoleteFile)) { + @unlink(PIWIK_INCLUDE_PATH . $obsoleteFile); + } - $obsoleteDirectories = array( - '/plugins/AdminHome', - '/plugins/Home', - '/plugins/PluginsAdmin', - ); - foreach($obsoleteDirectories as $dir) - { - if(file_exists(PIWIK_INCLUDE_PATH . $dir)) - { - Piwik::unlinkRecursive(PIWIK_INCLUDE_PATH . $dir, true); - } - } - } + $obsoleteDirectories = array( + '/plugins/AdminHome', + '/plugins/Home', + '/plugins/PluginsAdmin', + ); + foreach ($obsoleteDirectories as $dir) { + if (file_exists(PIWIK_INCLUDE_PATH . $dir)) { + Piwik::unlinkRecursive(PIWIK_INCLUDE_PATH . $dir, true); + } + } + } } diff --git a/core/Updates/0.2.12.php b/core/Updates/0.2.12.php index c4cc194449..f6ac41e839 100644 --- a/core/Updates/0.2.12.php +++ b/core/Updates/0.2.12.php @@ -14,22 +14,22 @@ */ class Piwik_Updates_0_2_12 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('site') .'` - CHANGE `ts_created` `ts_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('site') . '` + CHANGE `ts_created` `ts_created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL' => false, + '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', - ); - } + // 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()); - } + 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 a6b4293344..1572cd73bd 100644 --- a/core/Updates/0.2.13.php +++ b/core/Updates/0.2.13.php @@ -14,22 +14,22 @@ */ class Piwik_Updates_0_2_13 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'DROP TABLE IF EXISTS `'. Piwik_Common::prefixTable('option') .'`' => false, + static function getSql($schema = 'Myisam') + { + return array( + 'DROP TABLE IF EXISTS `' . Piwik_Common::prefixTable('option') . '`' => false, - 'CREATE TABLE `'. Piwik_Common::prefixTable('option') ."` ( + 'CREATE TABLE `' . Piwik_Common::prefixTable('option') . "` ( option_name VARCHAR( 64 ) NOT NULL , option_value LONGTEXT NOT NULL , autoload TINYINT NOT NULL DEFAULT '1', PRIMARY KEY ( option_name ) )" => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + 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 c6dbfdcb7a..594a461805 100644 --- a/core/Updates/0.2.24.php +++ b/core/Updates/0.2.24.php @@ -14,20 +14,20 @@ */ class Piwik_Updates_0_2_24 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'CREATE INDEX index_type_name - ON '. Piwik_Common::prefixTable('log_action') .' (type, name(15))' => false, - 'CREATE INDEX index_idsite_date - 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 getSql($schema = 'Myisam') + { + return array( + 'CREATE INDEX index_type_name + ON ' . Piwik_Common::prefixTable('log_action') . ' (type, name(15))' => false, + 'CREATE INDEX index_idsite_date + 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()); - } + 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 766cde3014..052033e9bf 100644 --- a/core/Updates/0.2.27.php +++ b/core/Updates/0.2.27.php @@ -14,16 +14,16 @@ */ class Piwik_Updates_0_2_27 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - $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') .'` + static function getSql($schema = 'Myisam') + { + $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, - 'CREATE TABLE `'. Piwik_Common::prefixTable('goal') ."` ( + 'CREATE TABLE `' . Piwik_Common::prefixTable('goal') . "` ( `idsite` int(11) NOT NULL, `idgoal` int(11) NOT NULL, `name` varchar(50) NOT NULL, @@ -34,9 +34,9 @@ class Piwik_Updates_0_2_27 extends Piwik_Updates `revenue` float NOT NULL, `deleted` tinyint(4) NOT NULL default '0', PRIMARY KEY (`idsite`,`idgoal`) - )" => false, + )" => false, - 'CREATE TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` ( + 'CREATE TABLE `' . Piwik_Common::prefixTable('log_conversion') . '` ( `idvisit` int(10) unsigned NOT NULL, `idsite` int(10) unsigned NOT NULL, `visitor_idcookie` char(32) NOT NULL, @@ -57,23 +57,21 @@ class Piwik_Updates_0_2_27 extends Piwik_Updates `revenue` float default NULL, PRIMARY KEY (`idvisit`,`idgoal`), KEY `index_idsite_date` (`idsite`,`visit_server_date`) - )' => false, - ); + )' => false, + ); - $tables = Piwik::getTablesInstalled(); - foreach($tables as $tableName) - { - if(preg_match('/archive_/', $tableName) == 1) - { - $sqlarray[ 'CREATE INDEX index_all ON '. $tableName .' (`idsite`,`date1`,`date2`,`name`,`ts_archived`)' ] = false; - } - } + $tables = Piwik::getTablesInstalled(); + foreach ($tables as $tableName) { + if (preg_match('/archive_/', $tableName) == 1) { + $sqlarray['CREATE INDEX index_all ON ' . $tableName . ' (`idsite`,`date1`,`date2`,`name`,`ts_archived`)'] = false; + } + } - return $sqlarray; - } + return $sqlarray; + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + 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 index d471b336e3..7a3bbfac42 100644 --- a/core/Updates/0.2.32.php +++ b/core/Updates/0.2.32.php @@ -14,23 +14,23 @@ */ class Piwik_Updates_0_2_32 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - 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') .'` + static function getSql($schema = 'Myisam') + { + 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', - ); - } + 'ALTER TABLE `' . Piwik_Common::prefixTable('user_language') . '` + CHANGE `login` `login` VARCHAR( 100 ) NOT NULL' => '1146', + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + 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 1b0f798957..0ad0b04b1b 100644 --- a/core/Updates/0.2.33.php +++ b/core/Updates/0.2.33.php @@ -14,28 +14,28 @@ */ class Piwik_Updates_0_2_33 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - $sqlarray = array( - // 0.2.33 [1020] - 'ALTER TABLE `'. Piwik_Common::prefixTable('user_dashboard') .'` + static function getSql($schema = 'Myisam') + { + $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 `'. 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) { - $sqlarray[ 'ALTER TABLE `'. $table .'` - CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci ' ] = false; - } + // alter table to set the utf8 collation + $tablesToAlter = Piwik::getTablesInstalled(true); + foreach ($tablesToAlter as $table) { + $sqlarray['ALTER TABLE `' . $table . '` + CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci '] = false; + } - return $sqlarray; - } + return $sqlarray; + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + 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 3da37d68d6..cf52c90ff6 100644 --- a/core/Updates/0.2.34.php +++ b/core/Updates/0.2.34.php @@ -14,11 +14,11 @@ */ class Piwik_Updates_0_2_34 extends Piwik_Updates { - static function update($schema = 'Myisam') - { - // force regeneration of cache files following #648 - Piwik::setUserIsSuperUser(); - $allSiteIds = Piwik_SitesManager_API::getInstance()->getAllSitesId(); - Piwik_Tracker_Cache::regenerateCacheWebsiteAttributes($allSiteIds); - } + static function update($schema = 'Myisam') + { + // force regeneration of cache files following #648 + Piwik::setUserIsSuperUser(); + $allSiteIds = Piwik_SitesManager_API::getInstance()->getAllSitesId(); + Piwik_Tracker_Cache::regenerateCacheWebsiteAttributes($allSiteIds); + } } diff --git a/core/Updates/0.2.35.php b/core/Updates/0.2.35.php index 442ef141f9..9f0f80abe7 100644 --- a/core/Updates/0.2.35.php +++ b/core/Updates/0.2.35.php @@ -14,16 +14,16 @@ */ class Piwik_Updates_0_2_35 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('user_dashboard') .'` + static function getSql($schema = 'Myisam') + { + 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()); - } + 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 eab56fb2bf..ca859c5f94 100644 --- a/core/Updates/0.2.37.php +++ b/core/Updates/0.2.37.php @@ -14,17 +14,17 @@ */ class Piwik_Updates_0_2_37 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'DELETE FROM `'. Piwik_Common::prefixTable('user_dashboard') ."` + static function getSql($schema = 'Myisam') + { + 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()); - } + 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 64f7694d27..60223f845e 100644 --- a/core/Updates/0.4.1.php +++ b/core/Updates/0.4.1.php @@ -14,18 +14,18 @@ */ class Piwik_Updates_0_4_1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` - CHANGE `idlink_va` `idlink_va` INT(11) DEFAULT NULL' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_conversion') . '` + CHANGE `idlink_va` `idlink_va` 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()); - } + 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 7ca2c5720f..e21135068b 100644 --- a/core/Updates/0.4.2.php +++ b/core/Updates/0.4.2.php @@ -14,22 +14,22 @@ */ class Piwik_Updates_0_4_2 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` - ADD `config_java` TINYINT(1) NOT NULL AFTER `config_flash`' => '1060', - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` + ADD `config_java` TINYINT(1) NOT NULL AFTER `config_flash`' => '1060', + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` ADD `config_quicktime` TINYINT(1) NOT NULL AFTER `config_director`' => '1060', - 'ALTER TABLE `'. Piwik_Common::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, - ); - } + 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()); - } + // 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.4.php b/core/Updates/0.4.4.php index d21fccefa7..b63731c5e7 100644 --- a/core/Updates/0.4.4.php +++ b/core/Updates/0.4.4.php @@ -14,16 +14,14 @@ */ class Piwik_Updates_0_4_4 extends Piwik_Updates { - static function update() - { - $obsoleteFile = PIWIK_DOCUMENT_ROOT . '/libs/open-flash-chart/php-ofc-library/ofc_upload_image.php'; - if(file_exists($obsoleteFile)) - { - $rc = @unlink($obsoleteFile); - if(!$rc) - { - throw new Exception(Piwik_TranslateException('General_ExceptionUndeletableFile', array($obsoleteFile))); - } - } - } + static function update() + { + $obsoleteFile = PIWIK_DOCUMENT_ROOT . '/libs/open-flash-chart/php-ofc-library/ofc_upload_image.php'; + if (file_exists($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 ce8ecd6643..dcbfffd578 100644 --- a/core/Updates/0.4.php +++ b/core/Updates/0.4.php @@ -14,23 +14,23 @@ */ class Piwik_Updates_0_4 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - 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_Common::prefixTable('log_visit') .'` - CHANGE `location_ip` `location_ip` BIGINT UNSIGNED NOT NULL' => false, - 'UPDATE `'. Piwik_Common::prefixTable('logger_api_call') .'` + static function getSql($schema = 'Myisam') + { + 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_Common::prefixTable('log_visit') . '` + CHANGE `location_ip` `location_ip` BIGINT UNSIGNED NOT NULL' => false, + '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_Common::prefixTable('logger_api_call') .'` - CHANGE `caller_ip` `caller_ip` BIGINT UNSIGNED' => false, - ); - } + '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()); - } + 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 index 8ca779dc49..733fccfd3d 100644 --- a/core/Updates/0.5.4.php +++ b/core/Updates/0.5.4.php @@ -14,56 +14,48 @@ */ class Piwik_Updates_0_5_4 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_action') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_action') . '` CHANGE `name` `name` TEXT' => false, - ); - } + ); + } - static function update() - { - $salt = Piwik_Common::generateUniqId(); + static function update() + { + $salt = Piwik_Common::generateUniqId(); $config = Piwik_Config::getInstance(); $superuser = $config->superuser; - if(!isset($superuser['salt'])) - { - try { - if(is_writable( Piwik_Config::getLocalConfigPath() )) - { - $superuser['salt'] = $salt; + if (!isset($superuser['salt'])) { + try { + if (is_writable(Piwik_Config::getLocalConfigPath())) { + $superuser['salt'] = $salt; $config->superuser = $superuser; - $config->forceSave(); - } - 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->forceSave(); + } 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>"); + } + } - $plugins = $config->Plugins; - if(!in_array('MultiSites', $plugins)) - { - try { - if(is_writable( Piwik_Config::getLocalConfigPath() )) - { - $plugins[] = 'MultiSites'; + $plugins = $config->Plugins; + if (!in_array('MultiSites', $plugins)) { + try { + if (is_writable(Piwik_Config::getLocalConfigPath())) { + $plugins[] = 'MultiSites'; $config->Plugins = $plugins; - $config->forceSave(); - } - 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()); - } + $config->forceSave(); + } 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 index d0d4225ae5..a2db73c613 100644 --- a/core/Updates/0.5.5.php +++ b/core/Updates/0.5.5.php @@ -14,32 +14,29 @@ */ class Piwik_Updates_0_5_5 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - $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', - ); + static function getSql($schema = 'Myisam') + { + $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'; - } - } + $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; - } + return $sqlarray; + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + + } } diff --git a/core/Updates/0.5.php b/core/Updates/0.5.php index 1040b22729..7877fba142 100644 --- a/core/Updates/0.5.php +++ b/core/Updates/0.5.php @@ -14,24 +14,24 @@ */ class Piwik_Updates_0_5 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - 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 getSql($schema = 'Myisam') + { + 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__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/0.6-rc1.php b/core/Updates/0.6-rc1.php index 23e896ccb5..e4dc34d522 100644 --- a/core/Updates/0.6-rc1.php +++ b/core/Updates/0.6-rc1.php @@ -14,54 +14,51 @@ */ class Piwik_Updates_0_6_rc1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - $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 getSql($schema = 'Myisam') + { + $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='?module=Proxy&action=redirect&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='?module=Proxy&action=redirect&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>"); - } - } + 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='?module=Proxy&action=redirect&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='?module=Proxy&action=redirect&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 index 0e0e88c27a..0273fe82c2 100644 --- a/core/Updates/0.6.2.php +++ b/core/Updates/0.6.2.php @@ -14,33 +14,29 @@ */ 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); - } - } + 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); - } - } + $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_Tracker_Cache::regenerateCacheWebsiteAttributes($allSiteIds); - } + // force regeneration of cache files + Piwik::setUserIsSuperUser(); + $allSiteIds = Piwik_SitesManager_API::getInstance()->getAllSitesId(); + Piwik_Tracker_Cache::regenerateCacheWebsiteAttributes($allSiteIds); + } } diff --git a/core/Updates/0.6.3.php b/core/Updates/0.6.3.php index 283f3f327d..21bb715b44 100644 --- a/core/Updates/0.6.3.php +++ b/core/Updates/0.6.3.php @@ -14,38 +14,34 @@ */ class Piwik_Updates_0_6_3 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - 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') .'` + static function getSql($schema = 'Myisam') + { + 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 = Piwik_Config::getInstance(); - $dbInfos = $config->database; - if(!isset($dbInfos['schema'])) - { - try { - if(is_writable( Piwik_Config::getLocalConfigPath() )) - { - $dbInfos['schema'] = 'Myisam'; - $config->database = $dbInfos; - $config->forceSave(); - } - 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>"); - } - } + static function update() + { + $config = Piwik_Config::getInstance(); + $dbInfos = $config->database; + if (!isset($dbInfos['schema'])) { + try { + if (is_writable(Piwik_Config::getLocalConfigPath())) { + $dbInfos['schema'] = 'Myisam'; + $config->database = $dbInfos; + $config->forceSave(); + } 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()); - } + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/0.7.php b/core/Updates/0.7.php index 8f68edba23..685fe7c962 100644 --- a/core/Updates/0.7.php +++ b/core/Updates/0.7.php @@ -14,16 +14,16 @@ */ class Piwik_Updates_0_7 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('option') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('option') . '` CHANGE `option_name` `option_name` VARCHAR(255) NOT NULL' => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/0.9.1.php b/core/Updates/0.9.1.php index 2ad423b8d3..24e6c27c60 100644 --- a/core/Updates/0.9.1.php +++ b/core/Updates/0.9.1.php @@ -14,42 +14,40 @@ */ class Piwik_Updates_0_9_1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - if(!Piwik::isTimezoneSupportEnabled()) - { - return array(); - } - // @see http://bugs.php.net/46111 - $timezones = timezone_identifiers_list(); - $brokenTZ = array(); + static function getSql($schema = 'Myisam') + { + if (!Piwik::isTimezoneSupportEnabled()) { + return array(); + } + // @see http://bugs.php.net/46111 + $timezones = timezone_identifiers_list(); + $brokenTZ = array(); - foreach ($timezones as $timezone) { - $testDate = "2008-08-19 13:00:00 " . $timezone; - - if (!strtotime($testDate)) { - $brokenTZ[] = $timezone; - } - } - $timezoneList = '"'. implode('","', $brokenTZ) . '"'; + foreach ($timezones as $timezone) { + $testDate = "2008-08-19 13:00:00 " . $timezone; - return array( - 'UPDATE '. Piwik_Common::prefixTable('site') .' + if (!strtotime($testDate)) { + $brokenTZ[] = $timezone; + } + } + $timezoneList = '"' . implode('","', $brokenTZ) . '"'; + + return array( + 'UPDATE ' . Piwik_Common::prefixTable('site') . ' SET timezone = "UTC" - WHERE timezone IN ('. $timezoneList .')' => false, + WHERE timezone IN (' . $timezoneList . ')' => false, - 'UPDATE `'. Piwik_Common::prefixTable('option') .'` + 'UPDATE `' . Piwik_Common::prefixTable('option') . '` SET option_value = "UTC" WHERE option_name = "SitesManager_DefaultTimezone" - AND option_value IN ('. $timezoneList .')' => false, - ); - } + AND option_value IN (' . $timezoneList . ')' => false, + ); + } - static function update() - { - if(Piwik::isTimezoneSupportEnabled()) - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } - } + static function update() + { + if (Piwik::isTimezoneSupportEnabled()) { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } + } } diff --git a/core/Updates/1.1.php b/core/Updates/1.1.php index d5ad681e1a..a98e4dc2f8 100644 --- a/core/Updates/1.1.php +++ b/core/Updates/1.1.php @@ -14,18 +14,18 @@ */ class Piwik_Updates_1_1 extends Piwik_Updates { - static function update($schema = 'Myisam') - { - $config = Piwik_Config::getInstance(); + static function update($schema = 'Myisam') + { + $config = Piwik_Config::getInstance(); - $rootLogin = $config->superuser['login']; - try { - // throws an exception if invalid - Piwik::checkValidLoginString($rootLogin); - } catch(Exception $e) { - throw new Exception('Superuser login name "' . $rootLogin . '" is no longer a valid format. ' - . $e->getMessage() - . ' Edit your config/config.ini.php to change it.'); - } - } + $rootLogin = $config->superuser['login']; + try { + // throws an exception if invalid + Piwik::checkValidLoginString($rootLogin); + } catch (Exception $e) { + throw new Exception('Superuser login name "' . $rootLogin . '" is no longer a valid format. ' + . $e->getMessage() + . ' Edit your config/config.ini.php to change it.'); + } + } } diff --git a/core/Updates/1.10-b4.php b/core/Updates/1.10-b4.php index eed44302d2..b34c3c8b5f 100755 --- a/core/Updates/1.10-b4.php +++ b/core/Updates/1.10-b4.php @@ -14,20 +14,17 @@ */ class Piwik_Updates_1_10_b4 extends Piwik_Updates { - static function isMajorUpdate() - { - return false; - } - - static function update() - { - try - { - Piwik_PluginsManager::getInstance()->activatePlugin('MobileMessaging'); - } - catch(Exception $e) - { - // pass - } - } + static function isMajorUpdate() + { + return false; + } + + static function update() + { + try { + Piwik_PluginsManager::getInstance()->activatePlugin('MobileMessaging'); + } catch (Exception $e) { + // pass + } + } }
\ No newline at end of file diff --git a/core/Updates/1.10.1.php b/core/Updates/1.10.1.php index ee416d0edd..ac00818c2f 100755 --- a/core/Updates/1.10.1.php +++ b/core/Updates/1.10.1.php @@ -14,20 +14,17 @@ */ class Piwik_Updates_1_10_1 extends Piwik_Updates { - static function isMajorUpdate() - { - return false; - } - - static function update() - { - try - { - Piwik_PluginsManager::getInstance()->activatePlugin('Overlay'); - } - catch(Exception $e) - { - // pass - } - } + static function isMajorUpdate() + { + return false; + } + + static function update() + { + try { + Piwik_PluginsManager::getInstance()->activatePlugin('Overlay'); + } catch (Exception $e) { + // pass + } + } }
\ No newline at end of file diff --git a/core/Updates/1.10.2-b1.php b/core/Updates/1.10.2-b1.php index c4482e383e..1796863297 100755 --- a/core/Updates/1.10.2-b1.php +++ b/core/Updates/1.10.2-b1.php @@ -14,17 +14,17 @@ */ class Piwik_Updates_1_10_2_b1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - // ignore existing column name error (1060) - 'ALTER TABLE '.Piwik_Common::prefixTable('report') - . " ADD COLUMN hour tinyint NOT NULL default 0 AFTER period" => 1060, - ); - } - - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function getSql($schema = 'Myisam') + { + return array( + // ignore existing column name error (1060) + 'ALTER TABLE ' . Piwik_Common::prefixTable('report') + . " ADD COLUMN hour tinyint NOT NULL default 0 AFTER period" => 1060, + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.10.2-b2.php b/core/Updates/1.10.2-b2.php index c18bfd677d..26cfd3e004 100644 --- a/core/Updates/1.10.2-b2.php +++ b/core/Updates/1.10.2-b2.php @@ -14,17 +14,17 @@ */ class Piwik_Updates_1_10_2_b2 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - // ignore existing column name error (1060) - 'ALTER TABLE '.Piwik_Common::prefixTable('site') - . " ADD COLUMN `keep_url_fragment` TINYINT NOT NULL DEFAULT 0 AFTER `group`" => 1060, - ); - } - - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function getSql($schema = 'Myisam') + { + return array( + // ignore existing column name error (1060) + 'ALTER TABLE ' . Piwik_Common::prefixTable('site') + . " ADD COLUMN `keep_url_fragment` TINYINT NOT NULL DEFAULT 0 AFTER `group`" => 1060, + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.11-b1.php b/core/Updates/1.11-b1.php index d38b123605..80bce9ce22 100644 --- a/core/Updates/1.11-b1.php +++ b/core/Updates/1.11-b1.php @@ -14,20 +14,17 @@ */ class Piwik_Updates_1_11_b1 extends Piwik_Updates { - static function isMajorUpdate() - { - return false; - } - - static function update() - { - try - { - Piwik_PluginsManager::getInstance()->activatePlugin('UserCountryMap'); - } - catch(Exception $e) - { - // pass - } - } + static function isMajorUpdate() + { + return false; + } + + static function update() + { + try { + Piwik_PluginsManager::getInstance()->activatePlugin('UserCountryMap'); + } catch (Exception $e) { + // pass + } + } }
\ No newline at end of file diff --git a/core/Updates/1.12-b1.php b/core/Updates/1.12-b1.php index 5f15ac0439..21412a24c0 100644 --- a/core/Updates/1.12-b1.php +++ b/core/Updates/1.12-b1.php @@ -14,22 +14,22 @@ */ class Piwik_Updates_1_12_b1 extends Piwik_Updates { - static function isMajorUpdate() - { - return true; - } - - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_link_visit_action') .'` + static function isMajorUpdate() + { + return true; + } + + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_link_visit_action') . '` ADD `custom_float_1` FLOAT NULL DEFAULT NULL' => false - ); - } - - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } - + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } + }
\ No newline at end of file diff --git a/core/Updates/1.2-rc1.php b/core/Updates/1.2-rc1.php index a6c8d15eeb..ac6bbcb0e3 100644 --- a/core/Updates/1.2-rc1.php +++ b/core/Updates/1.2-rc1.php @@ -14,11 +14,11 @@ */ class Piwik_Updates_1_2_rc1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - // Various performance improvements schema updates - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + static function getSql($schema = 'Myisam') + { + return array( + // Various performance improvements schema updates + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` DROP `visit_server_date`, DROP INDEX `index_idsite_date_config`, DROP INDEX `index_idsite_datetime_config`, @@ -42,16 +42,16 @@ class Piwik_Updates_1_2_rc1 extends Piwik_Updates ADD custom_var_v4 VARCHAR(100) DEFAULT NULL, ADD custom_var_k5 VARCHAR(100) DEFAULT NULL, ADD custom_var_v5 VARCHAR(100) DEFAULT NULL - ' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_link_visit_action') .'` + ' => false, + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_link_visit_action') . '` ADD `idsite` INT( 10 ) UNSIGNED NOT NULL AFTER `idlink_va` , ADD `server_time` DATETIME AFTER `idsite`, ADD `idvisitor` BINARY(8) NOT NULL AFTER `idsite`, ADD `idaction_name_ref` INT UNSIGNED NOT NULL AFTER `idaction_name`, ADD INDEX `index_idsite_servertime` ( `idsite` , `server_time` ) - ' => false, + ' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_conversion') . '` DROP `referer_idvisit`, ADD `idvisitor` BINARY(8) NOT NULL AFTER `idsite`, ADD visitor_count_visits SMALLINT(5) UNSIGNED NOT NULL, @@ -66,81 +66,78 @@ class Piwik_Updates_1_2_rc1 extends Piwik_Updates ADD custom_var_v4 VARCHAR(100) DEFAULT NULL, ADD custom_var_k5 VARCHAR(100) DEFAULT NULL, ADD custom_var_v5 VARCHAR(100) DEFAULT NULL - ' => false, - - // Migrate 128bits IDs inefficiently stored as 8bytes (256 bits) into 64bits - 'UPDATE '.Piwik_Common::prefixTable('log_visit') .' + ' => false, + + // Migrate 128bits IDs inefficiently stored as 8bytes (256 bits) into 64bits + 'UPDATE ' . Piwik_Common::prefixTable('log_visit') . ' SET idvisitor = binary(unhex(substring(visitor_idcookie,1,16))), config_id = binary(unhex(substring(config_md5config,1,16))) - ' => false, - 'UPDATE '.Piwik_Common::prefixTable('log_conversion') .' + ' => false, + 'UPDATE ' . Piwik_Common::prefixTable('log_conversion') . ' SET idvisitor = binary(unhex(substring(visitor_idcookie,1,16))) - ' => false, - - // Drop migrated fields - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + ' => false, + + // Drop migrated fields + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` DROP visitor_idcookie, DROP config_md5config - ' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` + ' => false, + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_conversion') . '` DROP visitor_idcookie - ' => false, - - // Recreate INDEX on new field - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + ' => false, + + // Recreate INDEX on new field + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` ADD INDEX `index_idsite_datetime_config` (idsite, visit_last_action_time, config_id) - ' => false, - - // Backfill action logs as best as we can - 'UPDATE '.Piwik_Common::prefixTable('log_link_visit_action') .' as action, - '.Piwik_Common::prefixTable('log_visit') .' as visit + ' => false, + + // Backfill action logs as best as we can + 'UPDATE ' . Piwik_Common::prefixTable('log_link_visit_action') . ' as action, + ' . Piwik_Common::prefixTable('log_visit') . ' as visit SET action.idsite = visit.idsite, action.server_time = visit.visit_last_action_time, action.idvisitor = visit.idvisitor WHERE action.idvisit=visit.idvisit - ' => false, - - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_link_visit_action') .'` + ' => false, + + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_link_visit_action') . '` CHANGE `server_time` `server_time` DATETIME NOT NULL - ' => false, + ' => false, + + // New index used max once per request, in case this table grows significantly in the future + 'ALTER TABLE `' . Piwik_Common::prefixTable('option') . '` ADD INDEX ( `autoload` ) ' => false, + + // new field for websites + 'ALTER TABLE `' . Piwik_Common::prefixTable('site') . '` ADD `group` VARCHAR( 250 ) NOT NULL' => false, + ); + } + + static function update() + { + // first we disable the plugins and keep an array of warnings messages + $pluginsToDisableMessage = array( + 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 1.2. \nYou can download the latest version of the plugin, compatible with Piwik 1.2.\n<a target='_blank' href='?module=Proxy&action=redirect&url=http://dev.piwik.org/trac/ticket/45'>Click here.</a>", + 'EntryPage' => "EntryPage plugin is not compatible with this version of Piwik, it was disabled.", + ); + $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()); - // New index used max once per request, in case this table grows significantly in the future - 'ALTER TABLE `'. Piwik_Common::prefixTable('option') .'` ADD INDEX ( `autoload` ) ' => false, - - // new field for websites - 'ALTER TABLE `'. Piwik_Common::prefixTable('site') .'` ADD `group` VARCHAR( 250 ) NOT NULL' => false, - ); - } + // 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>"); + } - static function update() - { - // first we disable the plugins and keep an array of warnings messages - $pluginsToDisableMessage = array( - 'GeoIP' => "GeoIP plugin was disabled, because it is not compatible with the new Piwik 1.2. \nYou can download the latest version of the plugin, compatible with Piwik 1.2.\n<a target='_blank' href='?module=Proxy&action=redirect&url=http://dev.piwik.org/trac/ticket/45'>Click here.</a>", - 'EntryPage' => "EntryPage plugin is not compatible with this version of Piwik, it was disabled.", - ); - $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/1.2-rc2.php b/core/Updates/1.2-rc2.php index 352a9ba174..b3afa13cbe 100644 --- a/core/Updates/1.2-rc2.php +++ b/core/Updates/1.2-rc2.php @@ -14,12 +14,12 @@ */ class Piwik_Updates_1_2_rc2 extends Piwik_Updates { - static function update() - { - try { - Piwik_PluginsManager::getInstance()->activatePlugin('CustomVariables'); - } catch(Exception $e) { - } - } + static function update() + { + try { + Piwik_PluginsManager::getInstance()->activatePlugin('CustomVariables'); + } catch (Exception $e) { + } + } } diff --git a/core/Updates/1.2.3.php b/core/Updates/1.2.3.php index b2b73ed9c1..f4a1104e9f 100644 --- a/core/Updates/1.2.3.php +++ b/core/Updates/1.2.3.php @@ -14,24 +14,24 @@ */ class Piwik_Updates_1_2_3 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - // LOAD DATA INFILE uses the database's charset - 'ALTER DATABASE `'. Piwik_Config::getInstance()->database['dbname'] .'` DEFAULT CHARACTER SET utf8' => false, + static function getSql($schema = 'Myisam') + { + return array( + // LOAD DATA INFILE uses the database's charset + 'ALTER DATABASE `' . Piwik_Config::getInstance()->database['dbname'] . '` DEFAULT CHARACTER SET utf8' => false, - // Various performance improvements schema updates - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + // Various performance improvements schema updates + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` DROP INDEX index_idsite_datetime_config, DROP INDEX index_idsite_idvisit, ADD INDEX index_idsite_config_datetime (idsite, config_id, visit_last_action_time), ADD INDEX index_idsite_datetime (idsite, visit_last_action_time)' => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.2.5-rc1.php b/core/Updates/1.2.5-rc1.php index f666c5bff3..90b0399589 100644 --- a/core/Updates/1.2.5-rc1.php +++ b/core/Updates/1.2.5-rc1.php @@ -14,21 +14,21 @@ */ class Piwik_Updates_1_2_5_rc1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('goal') .'` - ADD `allow_multiple` tinyint(4) NOT NULL AFTER case_sensitive' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('goal') . '` + ADD `allow_multiple` tinyint(4) NOT NULL AFTER case_sensitive' => false, + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_conversion') . '` ADD buster int unsigned NOT NULL AFTER revenue, DROP PRIMARY KEY, ADD PRIMARY KEY (idvisit, idgoal, buster)' => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.2.5-rc7.php b/core/Updates/1.2.5-rc7.php index c79e7f44ee..aafc70c7a9 100644 --- a/core/Updates/1.2.5-rc7.php +++ b/core/Updates/1.2.5-rc7.php @@ -14,18 +14,18 @@ */ class Piwik_Updates_1_2_5_rc7 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` ADD INDEX index_idsite_idvisitor (idsite, idvisitor)' => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.4-rc1.php b/core/Updates/1.4-rc1.php index b293d7e425..ce99c40497 100644 --- a/core/Updates/1.4-rc1.php +++ b/core/Updates/1.4-rc1.php @@ -14,21 +14,21 @@ */ class Piwik_Updates_1_4_rc1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('pdf') .'` - ADD COLUMN `format` VARCHAR(10)' => false, - 'UPDATE `'. Piwik_Common::prefixTable('pdf') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('pdf') . '` + ADD COLUMN `format` VARCHAR(10)' => false, + 'UPDATE `' . Piwik_Common::prefixTable('pdf') . '` SET format = "pdf"' => false, - ); - } + ); + } - static function update() - { - try { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } - catch(Exception $e){} - } + static function update() + { + try { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } catch (Exception $e) { + } + } } diff --git a/core/Updates/1.4-rc2.php b/core/Updates/1.4-rc2.php index f91541b42a..b5273b0505 100644 --- a/core/Updates/1.4-rc2.php +++ b/core/Updates/1.4-rc2.php @@ -14,28 +14,28 @@ */ class Piwik_Updates_1_4_rc2 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - "SET sql_mode=''" => false, - // this converts the 32-bit UNSIGNED INT column to a 16 byte VARBINARY; - // _but_ MySQL does string conversion! (e.g., integer 1 is converted to 49 -- the ASCII code for "1") - 'ALTER TABLE '. Piwik_Common::prefixTable('log_visit') .' - MODIFY location_ip VARBINARY(16) NOT NULL' => false, - 'ALTER TABLE '. Piwik_Common::prefixTable('logger_api_call') .' - MODIFY caller_ip VARBINARY(16) NOT NULL' => false, + static function getSql($schema = 'Myisam') + { + return array( + "SET sql_mode=''" => false, + // this converts the 32-bit UNSIGNED INT column to a 16 byte VARBINARY; + // _but_ MySQL does string conversion! (e.g., integer 1 is converted to 49 -- the ASCII code for "1") + 'ALTER TABLE ' . Piwik_Common::prefixTable('log_visit') . ' + MODIFY location_ip VARBINARY(16) NOT NULL' => false, + 'ALTER TABLE ' . Piwik_Common::prefixTable('logger_api_call') . ' + MODIFY caller_ip VARBINARY(16) NOT NULL' => false, - // fortunately, 2^32 is 10 digits long and fits in the VARBINARY(16) without truncation; - // to fix this, we cast to an integer, convert to hex, pad out leading zeros, and unhex it - 'UPDATE '. Piwik_Common::prefixTable('log_visit') ." - SET location_ip = UNHEX(LPAD(HEX(CONVERT(location_ip, UNSIGNED)), 8, '0'))" => false, - 'UPDATE '. Piwik_Common::prefixTable('logger_api_call') ." + // fortunately, 2^32 is 10 digits long and fits in the VARBINARY(16) without truncation; + // to fix this, we cast to an integer, convert to hex, pad out leading zeros, and unhex it + 'UPDATE ' . Piwik_Common::prefixTable('log_visit') . " + SET location_ip = UNHEX(LPAD(HEX(CONVERT(location_ip, UNSIGNED)), 8, '0'))" => false, + 'UPDATE ' . Piwik_Common::prefixTable('logger_api_call') . " SET caller_ip = UNHEX(LPAD(HEX(CONVERT(caller_ip, UNSIGNED)), 8, '0'))" => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.5-b1.php b/core/Updates/1.5-b1.php index 3290d4fa38..7156719e3d 100644 --- a/core/Updates/1.5-b1.php +++ b/core/Updates/1.5-b1.php @@ -14,10 +14,10 @@ */ class Piwik_Updates_1_5_b1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'CREATE TABLE `'. Piwik_Common::prefixTable('log_conversion_item') .'` ( + static function getSql($schema = 'Myisam') + { + return array( + 'CREATE TABLE `' . Piwik_Common::prefixTable('log_conversion_item') . '` ( idsite int(10) UNSIGNED NOT NULL, idvisitor BINARY(8) NOT NULL, server_time DATETIME NOT NULL, @@ -33,13 +33,13 @@ class Piwik_Updates_1_5_b1 extends Piwik_Updates PRIMARY KEY(idvisit, idorder, idaction_sku), INDEX index_idsite_servertime ( idsite, server_time ) - ) DEFAULT CHARSET=utf8 '=> false, + ) DEFAULT CHARSET=utf8 ' => false, - 'ALTER IGNORE TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + 'ALTER IGNORE TABLE `' . Piwik_Common::prefixTable('log_visit') . '` ADD visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL AFTER visitor_days_since_last, ADD visit_goal_buyer TINYINT(1) NOT NULL AFTER visit_goal_converted' => false, - 'ALTER IGNORE TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` + 'ALTER IGNORE TABLE `' . Piwik_Common::prefixTable('log_conversion') . '` ADD visitor_days_since_order SMALLINT(5) UNSIGNED NOT NULL AFTER visitor_days_since_first, ADD idorder varchar(100) default NULL AFTER buster, ADD items SMALLINT UNSIGNED DEFAULT NULL, @@ -48,12 +48,12 @@ class Piwik_Updates_1_5_b1 extends Piwik_Updates ADD revenue_shipping float default NULL, ADD revenue_discount float default NULL, ADD UNIQUE KEY unique_idsite_idorder (idsite, idorder), - MODIFY idgoal int(10) NOT NULL' => false, - ); - } + MODIFY idgoal int(10) NOT NULL' => false, + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.5-b2.php b/core/Updates/1.5-b2.php index 10524c7dd4..fee8f86e9a 100644 --- a/core/Updates/1.5-b2.php +++ b/core/Updates/1.5-b2.php @@ -14,10 +14,10 @@ */ class Piwik_Updates_1_5_b2 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_link_visit_action') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_link_visit_action') . '` ADD custom_var_k1 VARCHAR(100) DEFAULT NULL AFTER time_spent_ref_action, ADD custom_var_v1 VARCHAR(100) DEFAULT NULL, ADD custom_var_k2 VARCHAR(100) DEFAULT NULL, @@ -28,11 +28,11 @@ class Piwik_Updates_1_5_b2 extends Piwik_Updates ADD custom_var_v4 VARCHAR(100) DEFAULT NULL, ADD custom_var_k5 VARCHAR(100) DEFAULT NULL, ADD custom_var_v5 VARCHAR(100) DEFAULT NULL' => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.5-b3.php b/core/Updates/1.5-b3.php index 470b4806c9..e7d76bd2e5 100644 --- a/core/Updates/1.5-b3.php +++ b/core/Updates/1.5-b3.php @@ -14,10 +14,10 @@ */ class Piwik_Updates_1_5_b3 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` CHANGE custom_var_k1 custom_var_k1 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_v1 custom_var_v1 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_k2 custom_var_k2 VARCHAR(100) DEFAULT NULL, @@ -27,8 +27,8 @@ class Piwik_Updates_1_5_b3 extends Piwik_Updates CHANGE custom_var_k4 custom_var_k4 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_v4 custom_var_v4 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_k5 custom_var_k5 VARCHAR(100) DEFAULT NULL, - CHANGE custom_var_v5 custom_var_v5 VARCHAR(100) DEFAULT NULL' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` + CHANGE custom_var_v5 custom_var_v5 VARCHAR(100) DEFAULT NULL' => false, + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_conversion') . '` CHANGE custom_var_k1 custom_var_k1 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_v1 custom_var_v1 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_k2 custom_var_k2 VARCHAR(100) DEFAULT NULL, @@ -38,8 +38,8 @@ class Piwik_Updates_1_5_b3 extends Piwik_Updates CHANGE custom_var_k4 custom_var_k4 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_v4 custom_var_v4 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_k5 custom_var_k5 VARCHAR(100) DEFAULT NULL, - CHANGE custom_var_v5 custom_var_v5 VARCHAR(100) DEFAULT NULL' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_link_visit_action') .'` + CHANGE custom_var_v5 custom_var_v5 VARCHAR(100) DEFAULT NULL' => false, + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_link_visit_action') . '` CHANGE custom_var_k1 custom_var_k1 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_v1 custom_var_v1 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_k2 custom_var_k2 VARCHAR(100) DEFAULT NULL, @@ -50,11 +50,11 @@ class Piwik_Updates_1_5_b3 extends Piwik_Updates CHANGE custom_var_v4 custom_var_v4 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_k5 custom_var_k5 VARCHAR(100) DEFAULT NULL, CHANGE custom_var_v5 custom_var_v5 VARCHAR(100) DEFAULT NULL' => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.5-b4.php b/core/Updates/1.5-b4.php index ff889ea278..15479fb029 100644 --- a/core/Updates/1.5-b4.php +++ b/core/Updates/1.5-b4.php @@ -14,16 +14,16 @@ */ class Piwik_Updates_1_5_b4 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('site') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('site') . '` ADD ecommerce TINYINT DEFAULT 0' => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.5-b5.php b/core/Updates/1.5-b5.php index 65e95ca70f..c00eac3dce 100644 --- a/core/Updates/1.5-b5.php +++ b/core/Updates/1.5-b5.php @@ -14,21 +14,21 @@ */ class Piwik_Updates_1_5_b5 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'CREATE TABLE `'. Piwik_Common::prefixTable('session') .'` ( + static function getSql($schema = 'Myisam') + { + return array( + 'CREATE TABLE `' . Piwik_Common::prefixTable('session') . '` ( id CHAR(32) NOT NULL, modified INTEGER, lifetime INTEGER, data TEXT, PRIMARY KEY ( id ) ) DEFAULT CHARSET=utf8' => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.5-rc6.php b/core/Updates/1.5-rc6.php index f7bd1136db..af99ebe80f 100644 --- a/core/Updates/1.5-rc6.php +++ b/core/Updates/1.5-rc6.php @@ -14,12 +14,12 @@ */ class Piwik_Updates_1_5_rc6 extends Piwik_Updates { - static function update() - { - try { - Piwik_PluginsManager::getInstance()->activatePlugin('PrivacyManager'); - } catch(Exception $e) { - } - } + static function update() + { + try { + Piwik_PluginsManager::getInstance()->activatePlugin('PrivacyManager'); + } catch (Exception $e) { + } + } } diff --git a/core/Updates/1.6-b1.php b/core/Updates/1.6-b1.php index edaadabfa3..57e1deb889 100644 --- a/core/Updates/1.6-b1.php +++ b/core/Updates/1.6-b1.php @@ -14,15 +14,15 @@ */ class Piwik_Updates_1_6_b1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_conversion_item') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_conversion_item') . '` ADD idaction_category2 INTEGER(10) UNSIGNED NOT NULL AFTER idaction_category, ADD idaction_category3 INTEGER(10) UNSIGNED NOT NULL, ADD idaction_category4 INTEGER(10) UNSIGNED NOT NULL, - ADD idaction_category5 INTEGER(10) UNSIGNED NOT NULL' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + ADD idaction_category5 INTEGER(10) UNSIGNED NOT NULL' => false, + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` CHANGE custom_var_k1 custom_var_k1 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_v1 custom_var_v1 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_k2 custom_var_k2 VARCHAR(200) DEFAULT NULL, @@ -32,8 +32,8 @@ class Piwik_Updates_1_6_b1 extends Piwik_Updates CHANGE custom_var_k4 custom_var_k4 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_v4 custom_var_v4 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_k5 custom_var_k5 VARCHAR(200) DEFAULT NULL, - CHANGE custom_var_v5 custom_var_v5 VARCHAR(200) DEFAULT NULL' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_conversion') .'` + CHANGE custom_var_v5 custom_var_v5 VARCHAR(200) DEFAULT NULL' => false, + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_conversion') . '` CHANGE custom_var_k1 custom_var_k1 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_v1 custom_var_v1 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_k2 custom_var_k2 VARCHAR(200) DEFAULT NULL, @@ -43,8 +43,8 @@ class Piwik_Updates_1_6_b1 extends Piwik_Updates CHANGE custom_var_k4 custom_var_k4 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_v4 custom_var_v4 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_k5 custom_var_k5 VARCHAR(200) DEFAULT NULL, - CHANGE custom_var_v5 custom_var_v5 VARCHAR(200) DEFAULT NULL' => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_link_visit_action') .'` + CHANGE custom_var_v5 custom_var_v5 VARCHAR(200) DEFAULT NULL' => false, + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_link_visit_action') . '` CHANGE custom_var_k1 custom_var_k1 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_v1 custom_var_v1 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_k2 custom_var_k2 VARCHAR(200) DEFAULT NULL, @@ -55,11 +55,11 @@ class Piwik_Updates_1_6_b1 extends Piwik_Updates CHANGE custom_var_v4 custom_var_v4 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_k5 custom_var_k5 VARCHAR(200) DEFAULT NULL, CHANGE custom_var_v5 custom_var_v5 VARCHAR(200) DEFAULT NULL' => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.6-rc1.php b/core/Updates/1.6-rc1.php index 4671d88d5f..e66edc8992 100644 --- a/core/Updates/1.6-rc1.php +++ b/core/Updates/1.6-rc1.php @@ -14,12 +14,12 @@ */ class Piwik_Updates_1_6_rc1 extends Piwik_Updates { - static function update() - { - try { - Piwik_PluginsManager::getInstance()->activatePlugin('ImageGraph'); - } catch(Exception $e) { - } - } + static function update() + { + try { + Piwik_PluginsManager::getInstance()->activatePlugin('ImageGraph'); + } catch (Exception $e) { + } + } } diff --git a/core/Updates/1.7-b1.php b/core/Updates/1.7-b1.php index cb5bb46432..f5f084f892 100644 --- a/core/Updates/1.7-b1.php +++ b/core/Updates/1.7-b1.php @@ -14,21 +14,21 @@ */ class Piwik_Updates_1_7_b1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('pdf') .'` - ADD COLUMN `aggregate_reports_format` TINYINT(1) NOT NULL AFTER `reports`' => false, - 'UPDATE `'. Piwik_Common::prefixTable('pdf') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('pdf') . '` + ADD COLUMN `aggregate_reports_format` TINYINT(1) NOT NULL AFTER `reports`' => false, + 'UPDATE `' . Piwik_Common::prefixTable('pdf') . '` SET `aggregate_reports_format` = 1' => false, - ); - } + ); + } - static function update() - { - try { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } - catch(Exception $e){} - } + static function update() + { + try { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } catch (Exception $e) { + } + } } diff --git a/core/Updates/1.7.2-rc5.php b/core/Updates/1.7.2-rc5.php index f6f400a3cf..6d9e88a8b2 100644 --- a/core/Updates/1.7.2-rc5.php +++ b/core/Updates/1.7.2-rc5.php @@ -14,19 +14,19 @@ */ class Piwik_Updates_1_7_2_rc5 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('pdf') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('pdf') . '` CHANGE `aggregate_reports_format` `display_format` TINYINT(1) NOT NULL' => false - ); - } + ); + } - static function update() - { - try { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } - catch(Exception $e){} - } + static function update() + { + try { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } catch (Exception $e) { + } + } } diff --git a/core/Updates/1.7.2-rc7.php b/core/Updates/1.7.2-rc7.php index 3e93192936..c901f37a7d 100755 --- a/core/Updates/1.7.2-rc7.php +++ b/core/Updates/1.7.2-rc7.php @@ -14,28 +14,28 @@ */ class Piwik_Updates_1_7_2_rc7 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('user_dashboard') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('user_dashboard') . '` ADD `name` VARCHAR( 100 ) NULL DEFAULT NULL AFTER `iddashboard`' => false, - ); - } + ); + } - static function update() - { - try { - $dashboards = Piwik_FetchAll('SELECT * FROM `'. Piwik_Common::prefixTable('user_dashboard') .'`'); - foreach($dashboards AS $dashboard) { - $idDashboard = $dashboard['iddashboard']; - $login = $dashboard['login']; - $layout = $dashboard['layout']; - $layout = html_entity_decode($layout); - $layout = str_replace("\\\"", "\"", $layout); - Piwik_Query('UPDATE `'. Piwik_Common::prefixTable('user_dashboard') .'` SET layout = ? WHERE iddashboard = ? AND login = ?', array($layout, $idDashboard, $login)); - } - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } - catch(Exception $e){} - } + static function update() + { + try { + $dashboards = Piwik_FetchAll('SELECT * FROM `' . Piwik_Common::prefixTable('user_dashboard') . '`'); + foreach ($dashboards AS $dashboard) { + $idDashboard = $dashboard['iddashboard']; + $login = $dashboard['login']; + $layout = $dashboard['layout']; + $layout = html_entity_decode($layout); + $layout = str_replace("\\\"", "\"", $layout); + Piwik_Query('UPDATE `' . Piwik_Common::prefixTable('user_dashboard') . '` SET layout = ? WHERE iddashboard = ? AND login = ?', array($layout, $idDashboard, $login)); + } + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } catch (Exception $e) { + } + } } diff --git a/core/Updates/1.8.3-b1.php b/core/Updates/1.8.3-b1.php index 2f8c1c4860..2dfe336559 100644 --- a/core/Updates/1.8.3-b1.php +++ b/core/Updates/1.8.3-b1.php @@ -15,13 +15,13 @@ class Piwik_Updates_1_8_3_b1 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('site') .'` - CHANGE `excluded_parameters` `excluded_parameters` TEXT NOT NULL' => false, - - 'CREATE TABLE `'.Piwik_Common::prefixTable('report').'` ( + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('site') . '` + CHANGE `excluded_parameters` `excluded_parameters` TEXT NOT NULL' => false, + + 'CREATE TABLE `' . Piwik_Common::prefixTable('report') . '` ( `idreport` INT(11) NOT NULL AUTO_INCREMENT, `idsite` INTEGER(11) NOT NULL, `login` VARCHAR(100) NOT NULL, @@ -36,77 +36,75 @@ class Piwik_Updates_1_8_3_b1 extends Piwik_Updates `deleted` tinyint(4) NOT NULL default 0, PRIMARY KEY (`idreport`) ) DEFAULT CHARSET=utf8' => false, - ); - } - - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - if(!Piwik_PluginsManager::getInstance()->isPluginLoaded('PDFReports')) - { - return; - } - - try { + ); + } + + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + if (!Piwik_PluginsManager::getInstance()->isPluginLoaded('PDFReports')) { + return; + } - // Piwik_Common::prefixTable('pdf') has been heavily refactored to be more generic - // The following actions are taken in this update script : - // - create the new generic report table Piwik_Common::prefixTable('report') - // - migrate previous reports, if any, from Piwik_Common::prefixTable('pdf') to Piwik_Common::prefixTable('report') - // - delete Piwik_Common::prefixTable('pdf') + try { - $reports = Piwik_FetchAll('SELECT * FROM `'. Piwik_Common::prefixTable('pdf') .'`'); - foreach($reports AS $report) { + // Piwik_Common::prefixTable('pdf') has been heavily refactored to be more generic + // The following actions are taken in this update script : + // - create the new generic report table Piwik_Common::prefixTable('report') + // - migrate previous reports, if any, from Piwik_Common::prefixTable('pdf') to Piwik_Common::prefixTable('report') + // - delete Piwik_Common::prefixTable('pdf') - $idreport = $report['idreport']; - $idsite = $report['idsite']; - $login = $report['login']; - $description = $report['description']; - $period = $report['period']; - $format = $report['format']; - $display_format = $report['display_format']; - $email_me = $report['email_me']; - $additional_emails = $report['additional_emails']; - $reports = $report['reports']; - $ts_created = $report['ts_created']; - $ts_last_sent = $report['ts_last_sent']; - $deleted = $report['deleted']; + $reports = Piwik_FetchAll('SELECT * FROM `' . Piwik_Common::prefixTable('pdf') . '`'); + foreach ($reports AS $report) { - $parameters = array(); + $idreport = $report['idreport']; + $idsite = $report['idsite']; + $login = $report['login']; + $description = $report['description']; + $period = $report['period']; + $format = $report['format']; + $display_format = $report['display_format']; + $email_me = $report['email_me']; + $additional_emails = $report['additional_emails']; + $reports = $report['reports']; + $ts_created = $report['ts_created']; + $ts_last_sent = $report['ts_last_sent']; + $deleted = $report['deleted']; - if(!is_null($additional_emails)) - { - $parameters[Piwik_PDFReports::ADDITIONAL_EMAILS_PARAMETER] = preg_split('/,/', $additional_emails); - } + $parameters = array(); - $parameters[Piwik_PDFReports::EMAIL_ME_PARAMETER] = is_null($email_me) ? Piwik_PDFReports::EMAIL_ME_PARAMETER_DEFAULT_VALUE : (bool)$email_me; - $parameters[Piwik_PDFReports::DISPLAY_FORMAT_PARAMETER] = $display_format; + if (!is_null($additional_emails)) { + $parameters[Piwik_PDFReports::ADDITIONAL_EMAILS_PARAMETER] = preg_split('/,/', $additional_emails); + } - Piwik_Query( - 'INSERT INTO `' . Piwik_Common::prefixTable('report') . '` SET + $parameters[Piwik_PDFReports::EMAIL_ME_PARAMETER] = is_null($email_me) ? Piwik_PDFReports::EMAIL_ME_PARAMETER_DEFAULT_VALUE : (bool)$email_me; + $parameters[Piwik_PDFReports::DISPLAY_FORMAT_PARAMETER] = $display_format; + + Piwik_Query( + 'INSERT INTO `' . Piwik_Common::prefixTable('report') . '` SET idreport = ?, idsite = ?, login = ?, description = ?, period = ?, type = ?, format = ?, reports = ?, parameters = ?, ts_created = ?, ts_last_sent = ?, deleted = ?', - array( - $idreport, - $idsite, - $login, - $description, - is_null($period) ? Piwik_PDFReports::DEFAULT_PERIOD : $period, - Piwik_PDFReports::EMAIL_TYPE, - is_null($format) ? Piwik_PDFReports::DEFAULT_REPORT_FORMAT : $format, - Piwik_Common::json_encode(preg_split('/,/', $reports)), - Piwik_Common::json_encode($parameters), - $ts_created, - $ts_last_sent, - $deleted - ) - ); - } + array( + $idreport, + $idsite, + $login, + $description, + is_null($period) ? Piwik_PDFReports::DEFAULT_PERIOD : $period, + Piwik_PDFReports::EMAIL_TYPE, + is_null($format) ? Piwik_PDFReports::DEFAULT_REPORT_FORMAT : $format, + Piwik_Common::json_encode(preg_split('/,/', $reports)), + Piwik_Common::json_encode($parameters), + $ts_created, + $ts_last_sent, + $deleted + ) + ); + } + + Piwik_Query('DROP TABLE `' . Piwik_Common::prefixTable('pdf') . '`'); + } catch (Exception $e) { + } - Piwik_Query('DROP TABLE `'. Piwik_Common::prefixTable('pdf') .'`'); - } - catch(Exception $e){} - - } + } } diff --git a/core/Updates/1.8.4-b1.php b/core/Updates/1.8.4-b1.php index 2ae6698f78..2c0b5138c7 100644 --- a/core/Updates/1.8.4-b1.php +++ b/core/Updates/1.8.4-b1.php @@ -14,29 +14,29 @@ */ class Piwik_Updates_1_8_4_b1 extends Piwik_Updates { - - static function isMajorUpdate() - { - return true; - } - - static function getSql($schema = 'Myisam') - { - $action = Piwik_Common::prefixTable('log_action'); - $duplicates = Piwik_Common::prefixTable('log_action_duplicates'); - $visitAction = Piwik_Common::prefixTable('log_link_visit_action'); - $conversion = Piwik_Common::prefixTable('log_conversion'); - $visit = Piwik_Common::prefixTable('log_visit'); - - return array( - - // add url_prefix column - " ALTER TABLE `$action` + + static function isMajorUpdate() + { + return true; + } + + static function getSql($schema = 'Myisam') + { + $action = Piwik_Common::prefixTable('log_action'); + $duplicates = Piwik_Common::prefixTable('log_action_duplicates'); + $visitAction = Piwik_Common::prefixTable('log_link_visit_action'); + $conversion = Piwik_Common::prefixTable('log_conversion'); + $visit = Piwik_Common::prefixTable('log_visit'); + + return array( + + // add url_prefix column + " ALTER TABLE `$action` ADD `url_prefix` TINYINT(2) NULL AFTER `type`; - " => 1060, // ignore error 1060 Duplicate column name 'url_prefix' - - // remove protocol and www and store information in url_prefix - " UPDATE `$action` + " => 1060, // ignore error 1060 Duplicate column name 'url_prefix' + + // remove protocol and www and store information in url_prefix + " UPDATE `$action` SET url_prefix = IF ( LEFT(name, 11) = 'http://www.', 1, IF ( @@ -60,21 +60,21 @@ class Piwik_Updates_1_8_4_b1 extends Piwik_Updates WHERE type = 1 AND url_prefix IS NULL; - " => false, - - // find duplicates - " DROP TABLE IF EXISTS `$duplicates`; - " => false, - " CREATE TABLE `$duplicates` ( + " => false, + + // find duplicates + " DROP TABLE IF EXISTS `$duplicates`; + " => false, + " CREATE TABLE `$duplicates` ( `before` int(10) unsigned NOT NULL, `after` int(10) unsigned NOT NULL, KEY `mainkey` (`before`) ) ENGINE=MyISAM; - " => false, + " => false, - // grouping by name only would be case-insensitive, so we GROUP BY name,hash - // ON (action.type = 1 AND canonical.hash = action.hash) will use index (type, hash) - " INSERT INTO `$duplicates` ( + // grouping by name only would be case-insensitive, so we GROUP BY name,hash + // ON (action.type = 1 AND canonical.hash = action.hash) will use index (type, hash) + " INSERT INTO `$duplicates` ( SELECT action.idaction AS `before`, canonical.idaction AS `after` @@ -100,9 +100,9 @@ class Piwik_Updates_1_8_4_b1 extends Piwik_Updates AND canonical.idaction != action.idaction ); " => false, - - // replace idaction in log_link_visit_action - " UPDATE + + // replace idaction in log_link_visit_action + " UPDATE `$visitAction` AS link LEFT JOIN `$duplicates` AS duplicates_idaction_url @@ -111,8 +111,8 @@ class Piwik_Updates_1_8_4_b1 extends Piwik_Updates link.idaction_url = duplicates_idaction_url.after WHERE duplicates_idaction_url.after IS NOT NULL; - " => false, - " UPDATE + " => false, + " UPDATE `$visitAction` AS link LEFT JOIN `$duplicates` AS duplicates_idaction_url_ref @@ -121,10 +121,10 @@ class Piwik_Updates_1_8_4_b1 extends Piwik_Updates link.idaction_url_ref = duplicates_idaction_url_ref.after WHERE duplicates_idaction_url_ref.after IS NOT NULL; - " => false, - - // replace idaction in log_conversion - " UPDATE + " => false, + + // replace idaction in log_conversion + " UPDATE `$conversion` AS conversion LEFT JOIN `$duplicates` AS duplicates @@ -133,10 +133,10 @@ class Piwik_Updates_1_8_4_b1 extends Piwik_Updates conversion.idaction_url = duplicates.after WHERE duplicates.after IS NOT NULL; - " => false, - - // replace idaction in log_visit - " UPDATE + " => false, + + // replace idaction in log_visit + " UPDATE `$visit` AS visit LEFT JOIN `$duplicates` AS duplicates_entry @@ -145,8 +145,8 @@ class Piwik_Updates_1_8_4_b1 extends Piwik_Updates visit.visit_entry_idaction_url = duplicates_entry.after WHERE duplicates_entry.after IS NOT NULL; - " => false, - " UPDATE + " => false, + " UPDATE `$visit` AS visit LEFT JOIN `$duplicates` AS duplicates_exit @@ -155,36 +155,33 @@ class Piwik_Updates_1_8_4_b1 extends Piwik_Updates visit.visit_exit_idaction_url = duplicates_exit.after WHERE duplicates_exit.after IS NOT NULL; - " => false, - - // remove duplicates from log_action - " DELETE action FROM + " => false, + + // remove duplicates from log_action + " DELETE action FROM `$action` AS action LEFT JOIN `$duplicates` AS duplicates ON action.idaction = duplicates.before WHERE duplicates.after IS NOT NULL; - " => false, - - // remove the duplicates table - " DROP TABLE `$duplicates`; - " => false - ); - } + " => false, + + // remove the duplicates table + " DROP TABLE `$duplicates`; + " => false + ); + } - static function update() - { - try - { - self::enableMaintenanceMode(); - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - self::disableMaintenanceMode(); - } - catch(Exception $e) - { - self::disableMaintenanceMode(); - throw $e; - } - } + static function update() + { + try { + self::enableMaintenanceMode(); + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + self::disableMaintenanceMode(); + } catch (Exception $e) { + self::disableMaintenanceMode(); + throw $e; + } + } } diff --git a/core/Updates/1.9-b16.php b/core/Updates/1.9-b16.php index ccac385e9e..b4ec364801 100755 --- a/core/Updates/1.9-b16.php +++ b/core/Updates/1.9-b16.php @@ -14,38 +14,38 @@ */ class Piwik_Updates_1_9_b16 extends Piwik_Updates { - static function isMajorUpdate() - { - return true; - } - - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_link_visit_action') .'` + static function isMajorUpdate() + { + return true; + } + + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_link_visit_action') . '` CHANGE `idaction_url` `idaction_url` INT( 10 ) UNSIGNED NULL DEFAULT NULL' - => false, + => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` ADD visit_total_searches SMALLINT(5) UNSIGNED NOT NULL AFTER `visit_total_actions`' - => 1060, + => 1060, - 'ALTER TABLE `'. Piwik_Common::prefixTable('site') .'` + 'ALTER TABLE `' . Piwik_Common::prefixTable('site') . '` ADD sitesearch TINYINT DEFAULT 1 AFTER `excluded_parameters`, ADD sitesearch_keyword_parameters TEXT NOT NULL AFTER `sitesearch`, ADD sitesearch_category_parameters TEXT NOT NULL AFTER `sitesearch_keyword_parameters`' - => 1060, + => 1060, - // enable Site Search for all websites, users can manually disable the setting - 'UPDATE `'. Piwik_Common::prefixTable('site') .'` + // enable Site Search for all websites, users can manually disable the setting + 'UPDATE `' . Piwik_Common::prefixTable('site') . '` SET `sitesearch` = 1' => false, - ); - } + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Updates/1.9-b19.php b/core/Updates/1.9-b19.php index 9701a9cbbf..e5fc17f1ba 100755 --- a/core/Updates/1.9-b19.php +++ b/core/Updates/1.9-b19.php @@ -14,27 +14,27 @@ */ class Piwik_Updates_1_9_b19 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_link_visit_action') .'` + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_link_visit_action') . '` CHANGE `idaction_url_ref` `idaction_url_ref` INT( 10 ) UNSIGNED NULL DEFAULT 0' - => false, - 'ALTER TABLE `'. Piwik_Common::prefixTable('log_visit') .'` + => false, + 'ALTER TABLE `' . Piwik_Common::prefixTable('log_visit') . '` CHANGE `visit_exit_idaction_url` `visit_exit_idaction_url` INT( 10 ) UNSIGNED NULL DEFAULT 0' - => false - ); - } + => false + ); + } - static function update() - { - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + static function update() + { + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - try { - Piwik_PluginsManager::getInstance()->activatePlugin('Transitions'); - } catch(Exception $e) { - } - } + try { + Piwik_PluginsManager::getInstance()->activatePlugin('Transitions'); + } catch (Exception $e) { + } + } } diff --git a/core/Updates/1.9-b9.php b/core/Updates/1.9-b9.php index 53e45e276a..f986381d97 100755 --- a/core/Updates/1.9-b9.php +++ b/core/Updates/1.9-b9.php @@ -14,44 +14,41 @@ */ class Piwik_Updates_1_9_b9 extends Piwik_Updates { - static function isMajorUpdate() - { - return true; - } - - static function getSql($schema = 'Myisam') - { - $logVisit = Piwik_Common::prefixTable('log_visit'); - $logConversion = Piwik_Common::prefixTable('log_conversion'); - - $addColumns = "DROP `location_continent`, + static function isMajorUpdate() + { + return true; + } + + static function getSql($schema = 'Myisam') + { + $logVisit = Piwik_Common::prefixTable('log_visit'); + $logConversion = Piwik_Common::prefixTable('log_conversion'); + + $addColumns = "DROP `location_continent`, ADD `location_region` CHAR(2) NULL AFTER `location_country`, ADD `location_city` VARCHAR(255) NULL AFTER `location_region`, ADD `location_latitude` FLOAT(10, 6) NULL AFTER `location_city`, ADD `location_longitude` FLOAT(10, 6) NULL AFTER `location_latitude`"; - - return array( - // add geoip columns to log_visit - "ALTER TABLE `$logVisit` $addColumns" => 1091, - - // add geoip columns to log_conversion - "ALTER TABLE `$logConversion` $addColumns" => 1091, - ); - } - static function update() - { - try - { - self::enableMaintenanceMode(); - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - self::disableMaintenanceMode(); - } - catch (Exception $e) - { - self::disableMaintenanceMode(); - throw $e; - } - } + return array( + // add geoip columns to log_visit + "ALTER TABLE `$logVisit` $addColumns" => 1091, + + // add geoip columns to log_conversion + "ALTER TABLE `$logConversion` $addColumns" => 1091, + ); + } + + static function update() + { + try { + self::enableMaintenanceMode(); + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + self::disableMaintenanceMode(); + } catch (Exception $e) { + self::disableMaintenanceMode(); + throw $e; + } + } } diff --git a/core/Updates/1.9.1-b2.php b/core/Updates/1.9.1-b2.php index 219f5cd357..af9954be9a 100644 --- a/core/Updates/1.9.1-b2.php +++ b/core/Updates/1.9.1-b2.php @@ -14,20 +14,20 @@ */ class Piwik_Updates_1_9_1_b2 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - 'ALTER TABLE '.Piwik_Common::prefixTable('site'). " DROP `feedburnerName`" => 1091 - ); - } - - static function update() - { - // manually remove ExampleFeedburner column - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + static function getSql($schema = 'Myisam') + { + return array( + 'ALTER TABLE ' . Piwik_Common::prefixTable('site') . " DROP `feedburnerName`" => 1091 + ); + } - // remove ExampleFeedburner plugin - $pluginToDelete = 'ExampleFeedburner'; - self::deletePluginFromConfigFile($pluginToDelete); - } + static function update() + { + // manually remove ExampleFeedburner column + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + + // remove ExampleFeedburner plugin + $pluginToDelete = 'ExampleFeedburner'; + self::deletePluginFromConfigFile($pluginToDelete); + } } diff --git a/core/Updates/1.9.3-b10.php b/core/Updates/1.9.3-b10.php index 974c359872..6ceecbb67c 100755 --- a/core/Updates/1.9.3-b10.php +++ b/core/Updates/1.9.3-b10.php @@ -14,20 +14,17 @@ */ class Piwik_Updates_1_9_3_b10 extends Piwik_Updates { - static function isMajorUpdate() - { - return false; - } - - static function update() - { - try - { - Piwik_PluginsManager::getInstance()->activatePlugin('Annotations'); - } - catch(Exception $e) - { - // pass - } - } + static function isMajorUpdate() + { + return false; + } + + static function update() + { + try { + Piwik_PluginsManager::getInstance()->activatePlugin('Annotations'); + } catch (Exception $e) { + // pass + } + } } diff --git a/core/Updates/1.9.3-b3.php b/core/Updates/1.9.3-b3.php index 2c4819b7a7..65338a44eb 100644 --- a/core/Updates/1.9.3-b3.php +++ b/core/Updates/1.9.3-b3.php @@ -14,14 +14,14 @@ */ class Piwik_Updates_1_9_3_b3 extends Piwik_Updates { - static function update() - { - // Insight was a temporary code name for Overlay - $pluginToDelete = 'Insight'; - self::deletePluginFromConfigFile($pluginToDelete); - self::deletePluginFromFilesystem($pluginToDelete); + static function update() + { + // Insight was a temporary code name for Overlay + $pluginToDelete = 'Insight'; + self::deletePluginFromConfigFile($pluginToDelete); + self::deletePluginFromFilesystem($pluginToDelete); - // We also clean up 1.9.1 and delete Feedburner plugin - self::deletePluginFromFilesystem('Feedburner'); - } + // We also clean up 1.9.1 and delete Feedburner plugin + self::deletePluginFromFilesystem('Feedburner'); + } } diff --git a/core/Updates/1.9.3-b8.php b/core/Updates/1.9.3-b8.php index fe223daf3d..e0d29e0acd 100755 --- a/core/Updates/1.9.3-b8.php +++ b/core/Updates/1.9.3-b8.php @@ -14,18 +14,18 @@ */ class Piwik_Updates_1_9_3_b8 extends Piwik_Updates { - static function getSql($schema = 'Myisam') - { - return array( - // ignore existing column name error (1060) - 'ALTER TABLE '.Piwik_Common::prefixTable('site') - . " ADD COLUMN excluded_user_agents TEXT NOT NULL AFTER excluded_parameters" => 1060, - ); - } - - static function update() - { - // add excluded_user_agents column to site table - Piwik_Updater::updateDatabase(__FILE__, self::getSql()); - } + static function getSql($schema = 'Myisam') + { + return array( + // ignore existing column name error (1060) + 'ALTER TABLE ' . Piwik_Common::prefixTable('site') + . " ADD COLUMN excluded_user_agents TEXT NOT NULL AFTER excluded_parameters" => 1060, + ); + } + + static function update() + { + // add excluded_user_agents column to site table + Piwik_Updater::updateDatabase(__FILE__, self::getSql()); + } } diff --git a/core/Url.php b/core/Url.php index 748d87f716..b0e74f0067 100644 --- a/core/Url.php +++ b/core/Url.php @@ -15,463 +15,429 @@ * * @package Piwik */ -class Piwik_Url +class Piwik_Url { - /** - * List of hosts that are never checked for validity. - */ - private static $alwaysTrustedHosts = array('localhost', '127.0.0.1', '::1', '[::1]'); - - /** - * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * will return "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * - * @return string - */ - static public function getCurrentUrl() - { - 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" - * - * @param bool $checkTrustedHost Whether to do trusted host check. Should ALWAYS be true, - * except in Piwik_Controller. - * @return string - */ - static public function getCurrentUrlWithoutQueryString( $checkTrustedHost = true ) - { - return self::getCurrentScheme() . '://' - . self::getCurrentHost($default = 'unknown', $checkTrustedHost) - . self::getCurrentScriptName(); - } - - /** - * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * will return "http://example.org/dir1/dir2/" - * - * @return string with trailing slash - */ - static public function getCurrentUrlWithoutFileName() - { - return self::getCurrentScheme() . '://' - . self::getCurrentHost() - . self::getCurrentScriptPath(); - } - - /** - * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * will return "/dir1/dir2/" - * - * @return string with trailing slash - */ - static public function getCurrentScriptPath() - { - $queryString = self::getCurrentScriptName() ; - - //add a fake letter case /test/test2/ returns /test which is not expected - $urlDir = dirname ($queryString . 'x'); - $urlDir = str_replace('\\', '/', $urlDir); - // if we are in a subpath we add a trailing slash - if(strlen($urlDir) > 1) - { - $urlDir .= '/'; - } - return $urlDir; - } - - /** - * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * will return "/dir1/dir2/index.php" - * - * @return string - */ - static public function getCurrentScriptName() - { - $url = ''; - - if( !empty($_SERVER['REQUEST_URI']) ) - { - $url = $_SERVER['REQUEST_URI']; - - // strip http://host (Apache+Rails anomaly) - if(preg_match('~^https?://[^/]+($|/.*)~D', $url, $matches)) - { - $url = $matches[1]; - } - - // strip parameters - if(($pos = strpos($url, "?")) !== false) - { - $url = substr($url, 0, $pos); - } - - // strip path_info - if(isset($_SERVER['PATH_INFO'])) - { - $url = substr($url, 0, -strlen($_SERVER['PATH_INFO'])); - } - } - - /** - * SCRIPT_NAME is our fallback, though it may not be set correctly - * - * @see http://php.net/manual/en/reserved.variables.php - */ - if(empty($url)) - { - if(isset($_SERVER['SCRIPT_NAME'])) - { - $url = $_SERVER['SCRIPT_NAME']; - } - elseif(isset($_SERVER['SCRIPT_FILENAME'])) - { - $url = $_SERVER['SCRIPT_FILENAME']; - } - elseif(isset($_SERVER['argv'])) - { - $url = $_SERVER['argv'][0]; - } - } - - if(!isset($url[0]) || $url[0] !== '/') - { - $url = '/' . $url; - } - return $url; - } - - /** - * If the current URL is 'http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * will return 'http' - * - * @return string 'https' or 'http' - */ - static public function getCurrentScheme() - { - try { - $assume_secure_protocol = @Piwik_Config::getInstance()->General['assume_secure_protocol']; - } catch(Exception $e) { - $assume_secure_protocol = false; - } - if($assume_secure_protocol - || (isset($_SERVER['HTTPS']) - && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true)) - || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') - ) - { - return 'https'; - } - return 'http'; - } - - /** - * Validate "Host" (untrusted user input) - * - * @param string|false $host Contents of Host: header from Request. If false, gets the - * value from the request. - * - * @return boolean True if valid; false otherwise - */ - static public function isValidHost($host = false) - { - // only do trusted host check if it's enabled - if (isset(Piwik_Config::getInstance()->General['enable_trusted_host_check']) - && Piwik_Config::getInstance()->General['enable_trusted_host_check'] == 0) - { - return true; - } - - if ($host === false) - { - $host = @$_SERVER['HTTP_HOST']; - if (empty($host)) // if no current host, assume valid - { - return true; - } - } - // if host is in hardcoded whitelist, assume it's valid - if (in_array($host, self::$alwaysTrustedHosts)) - { - return true; - } - - $trustedHosts = @Piwik_Config::getInstance()->General['trusted_hosts']; - // if no trusted hosts, just assume it's valid - if (empty($trustedHosts)) - { - self::saveTrustedHostnameInConfig($host); - return true; - } - - // Only punctuation we allow is '[', ']', ':', '.' and '-' - $hostLength = Piwik_Common::strlen($host); - if ($hostLength !== strcspn($host, '`~!@#$%^&*()_+={}\\|;"\'<>,?/ ')) - { - return false; - } - - foreach ($trustedHosts as &$trustedHost) - { - $trustedHost = preg_quote($trustedHost); - } - $untrustedHost = Piwik_Common::mb_strtolower($host); - $untrustedHost = rtrim($untrustedHost, '.'); - $hostRegex = Piwik_Common::mb_strtolower('/(^|.)' . implode('|', $trustedHosts) . '$/'); - $result = preg_match($hostRegex, $untrustedHost); - return 0 !== $result; - } - - /** - * Records one host, or an array of hosts in the config file, - * if user is super user - * - * @static - * @param $host string|array - */ - public static function saveTrustedHostnameInConfig($host) - { - if (Piwik::isUserIsSuperUser() - && file_exists(Piwik_Config::getLocalConfigPath())) - { - $general = Piwik_Config::getInstance()->General; - if(!is_array($host)) - { - $host = array($host); - } - $host = array_filter($host); - if(empty($host)) { - return false; - } - $general['trusted_hosts'] = $host; - Piwik_Config::getInstance()->General = $general; - Piwik_Config::getInstance()->forceSave(); - } - } - - /** - * Get host - * - * @param bool $checkIfTrusted Whether to do trusted host check. Should ALWAYS be true, - * except in Piwik_Controller. - * @return string|false - */ - static public function getHost( $checkIfTrusted = true ) - { - // HTTP/1.1 request - if (isset($_SERVER['HTTP_HOST']) - && strlen($host = $_SERVER['HTTP_HOST']) - && (!$checkIfTrusted - || self::isValidHost($host))) - { - return $host; - } - - // HTTP/1.0 request doesn't include Host: header - if (isset($_SERVER['SERVER_ADDR'])) - { - return $_SERVER['SERVER_ADDR']; - } - - return false; - } - - /** - * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * will return "example.org" - * - * @param string $default Default value to return if host unknown - * @param bool $checkTrustedHost Whether to do trusted host check. Should ALWAYS be true, - * except in Piwik_Controller. - * @return string - */ - static public function getCurrentHost($default = 'unknown', $checkTrustedHost = true) - { - $hostHeaders = @Piwik_Config::getInstance()->General['proxy_host_headers']; - if(!is_array($hostHeaders)) - { - $hostHeaders = array(); - } - - $host = self::getHost($checkTrustedHost); - $default = Piwik_Common::sanitizeInputValue($host ? $host : $default); - - return Piwik_IP::getNonProxyIpFromHeader($default, $hostHeaders); - } - - /** - * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * will return "?param1=value1¶m2=value2" - * - * @return string - */ - static public function getCurrentQueryString() - { - $url = ''; - if(isset($_SERVER['QUERY_STRING']) - && !empty($_SERVER['QUERY_STRING'])) - { - $url .= "?".$_SERVER['QUERY_STRING']; - } - return $url; - } - - /** - * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" - * will return - * array - * 'param1' => string 'value1' - * 'param2' => string 'value2' - * - * @return array - */ - static function getArrayFromCurrentQueryString() - { - $queryString = self::getCurrentQueryString(); - $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; - * If a parameter wasn't found in the current query string, the new key-value will be added to the returned query string. - * - * @param array $params array ( 'param3' => 'value3' ) - * @return string ?param2=value2¶m3=value3 - */ - static function getCurrentQueryStringWithParametersModified( $params ) - { - $urlValues = self::getArrayFromCurrentQueryString(); - foreach($params as $key => $value) - { - $urlValues[$key] = $value; - } - $query = self::getQueryStringFromParameters($urlValues); - if(strlen($query) > 0) - { - return '?'.$query; - } - 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. - * - * @param array $parameters eg. array( 'param1' => 10, 'param2' => array(1,2)) - * @return string eg. "param1=10¶m2[]=1¶m2[]=2" - */ - static public function getQueryStringFromParameters($parameters) - { - $query = ''; - foreach($parameters as $name => $value) - { - if(is_null($value) - || $value === false) - { - continue; - } - if(is_array($value)) - { - foreach($value as $theValue) - { - $query .= $name . "[]=" . $theValue . "&"; - } - } - else - { - $query .= $name . "=" . $value . "&"; - } - } - $query = substr($query, 0, -1); - return $query; - } - - /** - * Redirects the user to the referrer if found. - * If the user doesn't have a referrer set, it redirects to the current URL without query string. - */ - static public function redirectToReferer() - { - $referrer = self::getReferer(); - if($referrer !== false) - { - self::redirectToUrl($referrer); - } - self::redirectToUrl(self::getCurrentUrlWithoutQueryString()); - } - - /** - * Redirects the user to the specified URL - * - * @param string $url - */ - static public function redirectToUrl( $url ) - { - if(Piwik_Common::isLookLikeUrl($url) - || strpos($url, 'index.php') === 0) - { - @header("Location: $url"); - } - else - { - echo "Invalid URL to redirect to."; - } - exit; - } - - /** - * Returns the HTTP_REFERER header, false if not found. - * - * @return string|false - */ - static public function getReferer() - { - if(!empty($_SERVER['HTTP_REFERER'])) - { - return $_SERVER['HTTP_REFERER']; - } - return false; - } - - /** - * Is the URL on the same host? - * - * @param string $url - * @return bool True if local; false otherwise. - */ - static public function isLocalUrl($url) - { - if(empty($url)) - { - return true; - } - - // handle host name mangling - $requestUri = isset($_SERVER['SCRIPT_URI']) ? $_SERVER['SCRIPT_URI'] : ''; - $parseRequest = @parse_url($requestUri); - $hosts = array( self::getHost(), self::getCurrentHost() ); - if(!empty($parseRequest['host'])) - { - $hosts[] = $parseRequest['host']; - } - - // drop port numbers from hostnames and IP addresses - $hosts = array_map(array('Piwik_IP', 'sanitizeIp'), $hosts); - - $disableHostCheck = Piwik_Config::getInstance()->General['enable_trusted_host_check'] == 0; - // compare scheme and host - $parsedUrl = @parse_url($url); - $host = Piwik_IP::sanitizeIp(@$parsedUrl['host']); - return !empty($host) - && ($disableHostCheck || in_array($host, $hosts)) - && !empty($parsedUrl['scheme']) - && in_array($parsedUrl['scheme'], array('http', 'https')); - } + /** + * List of hosts that are never checked for validity. + */ + private static $alwaysTrustedHosts = array('localhost', '127.0.0.1', '::1', '[::1]'); + + /** + * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * will return "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * + * @return string + */ + static public function getCurrentUrl() + { + 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" + * + * @param bool $checkTrustedHost Whether to do trusted host check. Should ALWAYS be true, + * except in Piwik_Controller. + * @return string + */ + static public function getCurrentUrlWithoutQueryString($checkTrustedHost = true) + { + return self::getCurrentScheme() . '://' + . self::getCurrentHost($default = 'unknown', $checkTrustedHost) + . self::getCurrentScriptName(); + } + + /** + * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * will return "http://example.org/dir1/dir2/" + * + * @return string with trailing slash + */ + static public function getCurrentUrlWithoutFileName() + { + return self::getCurrentScheme() . '://' + . self::getCurrentHost() + . self::getCurrentScriptPath(); + } + + /** + * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * will return "/dir1/dir2/" + * + * @return string with trailing slash + */ + static public function getCurrentScriptPath() + { + $queryString = self::getCurrentScriptName(); + + //add a fake letter case /test/test2/ returns /test which is not expected + $urlDir = dirname($queryString . 'x'); + $urlDir = str_replace('\\', '/', $urlDir); + // if we are in a subpath we add a trailing slash + if (strlen($urlDir) > 1) { + $urlDir .= '/'; + } + return $urlDir; + } + + /** + * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * will return "/dir1/dir2/index.php" + * + * @return string + */ + static public function getCurrentScriptName() + { + $url = ''; + + if (!empty($_SERVER['REQUEST_URI'])) { + $url = $_SERVER['REQUEST_URI']; + + // strip http://host (Apache+Rails anomaly) + if (preg_match('~^https?://[^/]+($|/.*)~D', $url, $matches)) { + $url = $matches[1]; + } + + // strip parameters + if (($pos = strpos($url, "?")) !== false) { + $url = substr($url, 0, $pos); + } + + // strip path_info + if (isset($_SERVER['PATH_INFO'])) { + $url = substr($url, 0, -strlen($_SERVER['PATH_INFO'])); + } + } + + /** + * SCRIPT_NAME is our fallback, though it may not be set correctly + * + * @see http://php.net/manual/en/reserved.variables.php + */ + if (empty($url)) { + if (isset($_SERVER['SCRIPT_NAME'])) { + $url = $_SERVER['SCRIPT_NAME']; + } elseif (isset($_SERVER['SCRIPT_FILENAME'])) { + $url = $_SERVER['SCRIPT_FILENAME']; + } elseif (isset($_SERVER['argv'])) { + $url = $_SERVER['argv'][0]; + } + } + + if (!isset($url[0]) || $url[0] !== '/') { + $url = '/' . $url; + } + return $url; + } + + /** + * If the current URL is 'http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * will return 'http' + * + * @return string 'https' or 'http' + */ + static public function getCurrentScheme() + { + try { + $assume_secure_protocol = @Piwik_Config::getInstance()->General['assume_secure_protocol']; + } catch (Exception $e) { + $assume_secure_protocol = false; + } + if ($assume_secure_protocol + || (isset($_SERVER['HTTPS']) + && ($_SERVER['HTTPS'] == 'on' || $_SERVER['HTTPS'] === true)) + || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') + ) { + return 'https'; + } + return 'http'; + } + + /** + * Validate "Host" (untrusted user input) + * + * @param string|false $host Contents of Host: header from Request. If false, gets the + * value from the request. + * + * @return boolean True if valid; false otherwise + */ + static public function isValidHost($host = false) + { + // only do trusted host check if it's enabled + if (isset(Piwik_Config::getInstance()->General['enable_trusted_host_check']) + && Piwik_Config::getInstance()->General['enable_trusted_host_check'] == 0 + ) { + return true; + } + + if ($host === false) { + $host = @$_SERVER['HTTP_HOST']; + if (empty($host)) // if no current host, assume valid + { + return true; + } + } + // if host is in hardcoded whitelist, assume it's valid + if (in_array($host, self::$alwaysTrustedHosts)) { + return true; + } + + $trustedHosts = @Piwik_Config::getInstance()->General['trusted_hosts']; + // if no trusted hosts, just assume it's valid + if (empty($trustedHosts)) { + self::saveTrustedHostnameInConfig($host); + return true; + } + + // Only punctuation we allow is '[', ']', ':', '.' and '-' + $hostLength = Piwik_Common::strlen($host); + if ($hostLength !== strcspn($host, '`~!@#$%^&*()_+={}\\|;"\'<>,?/ ')) { + return false; + } + + foreach ($trustedHosts as &$trustedHost) { + $trustedHost = preg_quote($trustedHost); + } + $untrustedHost = Piwik_Common::mb_strtolower($host); + $untrustedHost = rtrim($untrustedHost, '.'); + $hostRegex = Piwik_Common::mb_strtolower('/(^|.)' . implode('|', $trustedHosts) . '$/'); + $result = preg_match($hostRegex, $untrustedHost); + return 0 !== $result; + } + + /** + * Records one host, or an array of hosts in the config file, + * if user is super user + * + * @static + * @param $host string|array + */ + public static function saveTrustedHostnameInConfig($host) + { + if (Piwik::isUserIsSuperUser() + && file_exists(Piwik_Config::getLocalConfigPath()) + ) { + $general = Piwik_Config::getInstance()->General; + if (!is_array($host)) { + $host = array($host); + } + $host = array_filter($host); + if (empty($host)) { + return false; + } + $general['trusted_hosts'] = $host; + Piwik_Config::getInstance()->General = $general; + Piwik_Config::getInstance()->forceSave(); + } + } + + /** + * Get host + * + * @param bool $checkIfTrusted Whether to do trusted host check. Should ALWAYS be true, + * except in Piwik_Controller. + * @return string|false + */ + static public function getHost($checkIfTrusted = true) + { + // HTTP/1.1 request + if (isset($_SERVER['HTTP_HOST']) + && strlen($host = $_SERVER['HTTP_HOST']) + && (!$checkIfTrusted + || self::isValidHost($host)) + ) { + return $host; + } + + // HTTP/1.0 request doesn't include Host: header + if (isset($_SERVER['SERVER_ADDR'])) { + return $_SERVER['SERVER_ADDR']; + } + + return false; + } + + /** + * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * will return "example.org" + * + * @param string $default Default value to return if host unknown + * @param bool $checkTrustedHost Whether to do trusted host check. Should ALWAYS be true, + * except in Piwik_Controller. + * @return string + */ + static public function getCurrentHost($default = 'unknown', $checkTrustedHost = true) + { + $hostHeaders = @Piwik_Config::getInstance()->General['proxy_host_headers']; + if (!is_array($hostHeaders)) { + $hostHeaders = array(); + } + + $host = self::getHost($checkTrustedHost); + $default = Piwik_Common::sanitizeInputValue($host ? $host : $default); + + return Piwik_IP::getNonProxyIpFromHeader($default, $hostHeaders); + } + + /** + * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * will return "?param1=value1¶m2=value2" + * + * @return string + */ + static public function getCurrentQueryString() + { + $url = ''; + if (isset($_SERVER['QUERY_STRING']) + && !empty($_SERVER['QUERY_STRING']) + ) { + $url .= "?" . $_SERVER['QUERY_STRING']; + } + return $url; + } + + /** + * If current URL is "http://example.org/dir1/dir2/index.php?param1=value1¶m2=value2" + * will return + * array + * 'param1' => string 'value1' + * 'param2' => string 'value2' + * + * @return array + */ + static function getArrayFromCurrentQueryString() + { + $queryString = self::getCurrentQueryString(); + $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; + * If a parameter wasn't found in the current query string, the new key-value will be added to the returned query string. + * + * @param array $params array ( 'param3' => 'value3' ) + * @return string ?param2=value2¶m3=value3 + */ + static function getCurrentQueryStringWithParametersModified($params) + { + $urlValues = self::getArrayFromCurrentQueryString(); + foreach ($params as $key => $value) { + $urlValues[$key] = $value; + } + $query = self::getQueryStringFromParameters($urlValues); + if (strlen($query) > 0) { + return '?' . $query; + } + 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. + * + * @param array $parameters eg. array( 'param1' => 10, 'param2' => array(1,2)) + * @return string eg. "param1=10¶m2[]=1¶m2[]=2" + */ + static public function getQueryStringFromParameters($parameters) + { + $query = ''; + foreach ($parameters as $name => $value) { + if (is_null($value) + || $value === false + ) { + continue; + } + if (is_array($value)) { + foreach ($value as $theValue) { + $query .= $name . "[]=" . $theValue . "&"; + } + } else { + $query .= $name . "=" . $value . "&"; + } + } + $query = substr($query, 0, -1); + return $query; + } + + /** + * Redirects the user to the referrer if found. + * If the user doesn't have a referrer set, it redirects to the current URL without query string. + */ + static public function redirectToReferer() + { + $referrer = self::getReferer(); + if ($referrer !== false) { + self::redirectToUrl($referrer); + } + self::redirectToUrl(self::getCurrentUrlWithoutQueryString()); + } + + /** + * Redirects the user to the specified URL + * + * @param string $url + */ + static public function redirectToUrl($url) + { + if (Piwik_Common::isLookLikeUrl($url) + || strpos($url, 'index.php') === 0 + ) { + @header("Location: $url"); + } else { + echo "Invalid URL to redirect to."; + } + exit; + } + + /** + * Returns the HTTP_REFERER header, false if not found. + * + * @return string|false + */ + static public function getReferer() + { + if (!empty($_SERVER['HTTP_REFERER'])) { + return $_SERVER['HTTP_REFERER']; + } + return false; + } + + /** + * Is the URL on the same host? + * + * @param string $url + * @return bool True if local; false otherwise. + */ + static public function isLocalUrl($url) + { + if (empty($url)) { + return true; + } + + // handle host name mangling + $requestUri = isset($_SERVER['SCRIPT_URI']) ? $_SERVER['SCRIPT_URI'] : ''; + $parseRequest = @parse_url($requestUri); + $hosts = array(self::getHost(), self::getCurrentHost()); + if (!empty($parseRequest['host'])) { + $hosts[] = $parseRequest['host']; + } + + // drop port numbers from hostnames and IP addresses + $hosts = array_map(array('Piwik_IP', 'sanitizeIp'), $hosts); + + $disableHostCheck = Piwik_Config::getInstance()->General['enable_trusted_host_check'] == 0; + // compare scheme and host + $parsedUrl = @parse_url($url); + $host = Piwik_IP::sanitizeIp(@$parsedUrl['host']); + return !empty($host) + && ($disableHostCheck || in_array($host, $hosts)) + && !empty($parsedUrl['scheme']) + && in_array($parsedUrl['scheme'], array('http', 'https')); + } } diff --git a/core/Version.php b/core/Version.php index 545823c3b6..d6e1bca6ba 100644 --- a/core/Version.php +++ b/core/Version.php @@ -16,9 +16,9 @@ */ final class Piwik_Version { - /** - * Current Piwik version - * @var string - */ - const VERSION = '1.12-b1'; + /** + * Current Piwik version + * @var string + */ + const VERSION = '1.12-b1'; } diff --git a/core/View.php b/core/View.php index 982e4d381c..cb26fff689 100644 --- a/core/View.php +++ b/core/View.php @@ -12,9 +12,8 @@ /** * Transition for pre-Piwik 0.4.4 */ -if(!defined('PIWIK_USER_PATH')) -{ - define('PIWIK_USER_PATH', PIWIK_INCLUDE_PATH); +if (!defined('PIWIK_USER_PATH')) { + define('PIWIK_USER_PATH', PIWIK_INCLUDE_PATH); } /** @@ -24,236 +23,220 @@ if(!defined('PIWIK_USER_PATH')) */ class Piwik_View implements Piwik_View_Interface { - const COREUPDATER_ONE_CLICK_DONE = 'update_one_click_done'; - - - private $template = ''; - private $smarty = false; - private $contentType = 'text/html; charset=utf-8'; - private $xFrameOptions = null; - - public function __construct( $templateFile, $smConf = array(), $filter = true ) - { - $this->template = $templateFile; - $this->smarty = new Piwik_Smarty($smConf, $filter); - - // global value accessible to all templates: the piwik base URL for the current request - $this->piwik_version = Piwik_Version::VERSION; - $this->cacheBuster = md5(Piwik_Common::getSalt() . PHP_VERSION . Piwik_Version::VERSION); - $this->piwikUrl = Piwik_Common::sanitizeInputValue(Piwik_Url::getCurrentUrlWithoutFileName()); - } - - /** - * Directly assigns a variable to the view script. - * VAR names may not be prefixed with '_'. - * - * @param string $key The variable name. - * @param mixed $val The variable value. - */ - public function __set($key, $val) - { - $this->smarty->assign($key, $val); - } - - /** - * Retrieves an assigned variable. - * VAR names may not be prefixed with '_'. - * - * @param string $key The variable name. - * @return mixed The variable value. - */ - public function __get($key) - { - return $this->smarty->get_template_vars($key); - } - - /** - * Renders the current view. - * - * @return string Generated template - */ - public function render() - { - try { - $this->currentModule = Piwik::getModule(); - $this->currentAction = Piwik::getAction(); - $userLogin = Piwik::getCurrentUserLogin(); - $this->userLogin = $userLogin; - - $count = Piwik::getWebsitesCountToDisplay(); - - $sites = Piwik_SitesManager_API::getInstance()->getSitesWithAtLeastViewAccess($count); - usort($sites, create_function('$site1, $site2', 'return strcasecmp($site1["name"], $site2["name"]);')); - $this->sites = $sites; - $this->url = Piwik_Common::sanitizeInputValue(Piwik_Url::getCurrentUrl()); - $this->token_auth = Piwik::getCurrentUserTokenAuth(); - $this->userHasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess(); - $this->userIsSuperUser = Piwik::isUserIsSuperUser(); - $this->latest_version_available = Piwik_UpdateCheck::isNewestVersionAvailable(); - $this->disableLink = Piwik_Common::getRequestVar('disableLink', 0, 'int'); - $this->isWidget = Piwik_Common::getRequestVar('widget', 0, 'int'); - if(Piwik_Config::getInstance()->General['autocomplete_min_sites'] <= count($sites)) - { - $this->show_autocompleter = true; - } - else - { - $this->show_autocompleter = false; - } - - $this->loginModule = Piwik::getLoginPluginName(); - - $user = Piwik_UsersManager_API::getInstance()->getUser($userLogin); - $this->userAlias = $user['alias']; - - } catch(Exception $e) { - // can fail, for example at installation (no plugin loaded yet) - } - - $this->totalTimeGeneration = Zend_Registry::get('timer')->getTime(); - try { - $this->totalNumberOfQueries = Piwik::getQueryCount(); - } - catch(Exception $e){ - $this->totalNumberOfQueries = 0; - } - - Piwik::overrideCacheControlHeaders('no-store'); - - @header('Content-Type: '.$this->contentType); - // always sending this header, sometimes empty, to ensure that Dashboard embed loads (which could call this header() multiple times, the last one will prevail) - @header('X-Frame-Options: '. (string)$this->xFrameOptions); - - return $this->smarty->fetch($this->template); - } - - /** - * Set Content-Type field in HTTP response. - * Since PHP 5.1.2, header() protects against header injection attacks. - * - * @param string $contentType - */ - public function setContentType( $contentType ) - { - $this->contentType = $contentType; - } - - /** - * Set X-Frame-Options field in the HTTP response. - * - * @param string $option ('deny' or 'sameorigin') - */ - public function setXFrameOptions( $option = 'deny' ) - { - if($option === 'deny' || $option === 'sameorigin') - { - $this->xFrameOptions = $option; - } - if($option == 'allow') - { - $this->xFrameOptions = null; - } - } - - /** - * Add form to view - * - * @param Piwik_QuickForm2 $form - */ - public function addForm( $form ) - { - if($form instanceof Piwik_QuickForm2) - { - // assign array with form data - $this->smarty->assign('form_data', $form->getFormData()); - $this->smarty->assign('element_list', $form->getElementList()); - } - } + const COREUPDATER_ONE_CLICK_DONE = 'update_one_click_done'; + + + private $template = ''; + private $smarty = false; + private $contentType = 'text/html; charset=utf-8'; + private $xFrameOptions = null; + + public function __construct($templateFile, $smConf = array(), $filter = true) + { + $this->template = $templateFile; + $this->smarty = new Piwik_Smarty($smConf, $filter); + + // global value accessible to all templates: the piwik base URL for the current request + $this->piwik_version = Piwik_Version::VERSION; + $this->cacheBuster = md5(Piwik_Common::getSalt() . PHP_VERSION . Piwik_Version::VERSION); + $this->piwikUrl = Piwik_Common::sanitizeInputValue(Piwik_Url::getCurrentUrlWithoutFileName()); + } + + /** + * Directly assigns a variable to the view script. + * VAR names may not be prefixed with '_'. + * + * @param string $key The variable name. + * @param mixed $val The variable value. + */ + public function __set($key, $val) + { + $this->smarty->assign($key, $val); + } + + /** + * Retrieves an assigned variable. + * VAR names may not be prefixed with '_'. + * + * @param string $key The variable name. + * @return mixed The variable value. + */ + public function __get($key) + { + return $this->smarty->get_template_vars($key); + } + + /** + * Renders the current view. + * + * @return string Generated template + */ + public function render() + { + try { + $this->currentModule = Piwik::getModule(); + $this->currentAction = Piwik::getAction(); + $userLogin = Piwik::getCurrentUserLogin(); + $this->userLogin = $userLogin; + + $count = Piwik::getWebsitesCountToDisplay(); + + $sites = Piwik_SitesManager_API::getInstance()->getSitesWithAtLeastViewAccess($count); + usort($sites, create_function('$site1, $site2', 'return strcasecmp($site1["name"], $site2["name"]);')); + $this->sites = $sites; + $this->url = Piwik_Common::sanitizeInputValue(Piwik_Url::getCurrentUrl()); + $this->token_auth = Piwik::getCurrentUserTokenAuth(); + $this->userHasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess(); + $this->userIsSuperUser = Piwik::isUserIsSuperUser(); + $this->latest_version_available = Piwik_UpdateCheck::isNewestVersionAvailable(); + $this->disableLink = Piwik_Common::getRequestVar('disableLink', 0, 'int'); + $this->isWidget = Piwik_Common::getRequestVar('widget', 0, 'int'); + if (Piwik_Config::getInstance()->General['autocomplete_min_sites'] <= count($sites)) { + $this->show_autocompleter = true; + } else { + $this->show_autocompleter = false; + } - /** - * 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)) - { - $this->smarty->assign($var, $value); - } - elseif (is_array($var)) - { - foreach ($var as $key => $value) - { - $this->smarty->assign($key, $value); - } - } - } + $this->loginModule = Piwik::getLoginPluginName(); - /** - * Clear compiled Smarty templates - */ - static public function clearCompiledTemplates() - { - $view = new Piwik_View(null); - $view->smarty->clear_compiled_tpl(); - } + $user = Piwik_UsersManager_API::getInstance()->getUser($userLogin); + $this->userAlias = $user['alias']; - /** - * Render the single report template - * - * @param string $title Report title - * @param string $reportHtml Report body - * @param bool $fetch If true, return report contents as a string; else echo to screen - * @return string Report contents if $fetch == true - */ - static public function singleReport($title, $reportHtml, $fetch = false) - { - $view = new Piwik_View('CoreHome/templates/single_report.tpl'); - $view->title = $title; - $view->report = $reportHtml; + } catch (Exception $e) { + // can fail, for example at installation (no plugin loaded yet) + } - if ($fetch) - { - return $view->render(); - } - echo $view->render(); - } + $this->totalTimeGeneration = Zend_Registry::get('timer')->getTime(); + try { + $this->totalNumberOfQueries = Piwik::getQueryCount(); + } catch (Exception $e) { + $this->totalNumberOfQueries = 0; + } - /** - * View factory method - * - * @param string $templateName Template name (e.g., 'index') - * @throws Exception - * @return Piwik_View|Piwik_View_OneClickDone - */ - static public function factory( $templateName = null ) - { - if ($templateName == self::COREUPDATER_ONE_CLICK_DONE) - { - return new Piwik_View_OneClickDone(Piwik::getCurrentUserTokenAuth()); - } + Piwik::overrideCacheControlHeaders('no-store'); + + @header('Content-Type: ' . $this->contentType); + // always sending this header, sometimes empty, to ensure that Dashboard embed loads (which could call this header() multiple times, the last one will prevail) + @header('X-Frame-Options: ' . (string)$this->xFrameOptions); + + return $this->smarty->fetch($this->template); + } + + /** + * Set Content-Type field in HTTP response. + * Since PHP 5.1.2, header() protects against header injection attacks. + * + * @param string $contentType + */ + public function setContentType($contentType) + { + $this->contentType = $contentType; + } + + /** + * Set X-Frame-Options field in the HTTP response. + * + * @param string $option ('deny' or 'sameorigin') + */ + public function setXFrameOptions($option = 'deny') + { + if ($option === 'deny' || $option === 'sameorigin') { + $this->xFrameOptions = $option; + } + if ($option == 'allow') { + $this->xFrameOptions = null; + } + } + + /** + * Add form to view + * + * @param Piwik_QuickForm2 $form + */ + public function addForm($form) + { + if ($form instanceof Piwik_QuickForm2) { + // assign array with form data + $this->smarty->assign('form_data', $form->getFormData()); + $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)) { + $this->smarty->assign($var, $value); + } elseif (is_array($var)) { + foreach ($var as $key => $value) { + $this->smarty->assign($key, $value); + } + } + } + + /** + * Clear compiled Smarty templates + */ + static public function clearCompiledTemplates() + { + $view = new Piwik_View(null); + $view->smarty->clear_compiled_tpl(); + } + + /** + * Render the single report template + * + * @param string $title Report title + * @param string $reportHtml Report body + * @param bool $fetch If true, return report contents as a string; else echo to screen + * @return string Report contents if $fetch == true + */ + static public function singleReport($title, $reportHtml, $fetch = false) + { + $view = new Piwik_View('CoreHome/templates/single_report.tpl'); + $view->title = $title; + $view->report = $reportHtml; + + if ($fetch) { + return $view->render(); + } + echo $view->render(); + } + + /** + * View factory method + * + * @param string $templateName Template name (e.g., 'index') + * @throws Exception + * @return Piwik_View|Piwik_View_OneClickDone + */ + static public function factory($templateName = null) + { + if ($templateName == self::COREUPDATER_ONE_CLICK_DONE) { + return new Piwik_View_OneClickDone(Piwik::getCurrentUserTokenAuth()); + } - Piwik_PostEvent('View.getViewType', $viewType); + Piwik_PostEvent('View.getViewType', $viewType); - // get caller - $bt = @debug_backtrace(); - if($bt === null || !isset($bt[0])) - { - throw new Exception("View factory cannot be invoked"); - } - $path = basename(dirname($bt[0]['file'])); + // get caller + $bt = @debug_backtrace(); + if ($bt === null || !isset($bt[0])) { + throw new Exception("View factory cannot be invoked"); + } + $path = basename(dirname($bt[0]['file'])); - if(Piwik_Common::isPhpCliMode()) - { - $templateFile = $path.'/templates/cli_'.$templateName.'.tpl'; - if(file_exists( PIWIK_INCLUDE_PATH . '/plugins/' . $templateFile)) - { - return new Piwik_View($templateFile, array(), false); + if (Piwik_Common::isPhpCliMode()) { + $templateFile = $path . '/templates/cli_' . $templateName . '.tpl'; + if (file_exists(PIWIK_INCLUDE_PATH . '/plugins/' . $templateFile)) { + return new Piwik_View($templateFile, array(), false); } } - $templateFile = $path.'/templates/'.$templateName.'.tpl'; + $templateFile = $path . '/templates/' . $templateName . '.tpl'; return new Piwik_View($templateFile); - } + } } diff --git a/core/View/Interface.php b/core/View/Interface.php index c1861a1216..1998ead861 100644 --- a/core/View/Interface.php +++ b/core/View/Interface.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -16,10 +16,10 @@ */ interface Piwik_View_Interface { - /** - * Outputs the data. - * - * @return mixed (image, array, html...) - */ - function render(); + /** + * Outputs the data. + * + * @return mixed (image, array, html...) + */ + function render(); } diff --git a/core/View/OneClickDone.php b/core/View/OneClickDone.php index 5ed1111718..bafebb7085 100644 --- a/core/View/OneClickDone.php +++ b/core/View/OneClickDone.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -23,46 +23,46 @@ */ class Piwik_View_OneClickDone { - /** - * @var string - */ - private $tokenAuth; + /** + * @var string + */ + private $tokenAuth; - /** - * @var string - */ - public $coreError; + /** + * @var string + */ + public $coreError; - /** - * @var array - */ - public $feedbackMessages; + /** + * @var array + */ + public $feedbackMessages; - public function __construct($tokenAuth) - { - $this->tokenAuth = $tokenAuth; - } + public function __construct($tokenAuth) + { + $this->tokenAuth = $tokenAuth; + } - /** - * Outputs the data. - * - * @return string html - */ - public function render() - { - // set response headers - @header('Content-Type: text/html; charset=UTF-8'); - @header('Pragma: '); - @header('Expires: '); - @header('Cache-Control: must-revalidate'); - @header('X-Frame-Options: deny'); + /** + * Outputs the data. + * + * @return string html + */ + public function render() + { + // set response headers + @header('Content-Type: text/html; charset=UTF-8'); + @header('Pragma: '); + @header('Expires: '); + @header('Cache-Control: must-revalidate'); + @header('X-Frame-Options: deny'); - $error = htmlspecialchars($this->coreError, ENT_QUOTES, 'UTF-8'); - $messages = htmlspecialchars(serialize($this->feedbackMessages), ENT_QUOTES, 'UTF-8'); - $tokenAuth = $this->tokenAuth; + $error = htmlspecialchars($this->coreError, ENT_QUOTES, 'UTF-8'); + $messages = htmlspecialchars(serialize($this->feedbackMessages), ENT_QUOTES, 'UTF-8'); + $tokenAuth = $this->tokenAuth; - // use a heredoc instead of an external file - echo <<<END_OF_TEMPLATE + // use a heredoc instead of an external file + echo <<<END_OF_TEMPLATE <!DOCTYPE html> <html> <head> @@ -85,5 +85,5 @@ class Piwik_View_OneClickDone </body> </html> END_OF_TEMPLATE; - } + } } diff --git a/core/View/ReportsByDimension.php b/core/View/ReportsByDimension.php index 3c6919fbb5..e02d428af6 100644 --- a/core/View/ReportsByDimension.php +++ b/core/View/ReportsByDimension.php @@ -1,107 +1,104 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package SmartyPlugins */ /** * A facade that makes it easier to use the 'reports_by_dimension.tpl' template. - * + * * This view will output HTML that displays a list of report names by category and * loads them by AJAX when clicked. The loaded report is displayed to the right * of the report listing. */ class Piwik_View_ReportsByDimension extends Piwik_View { - /** - * Constructor. - */ - public function __construct() - { - parent::__construct(PIWIK_INCLUDE_PATH.'/plugins/CoreHome/templates/reports_by_dimension.tpl'); - $this->dimensionCategories = array(); - } - - /** - * Adds a report to the list of reports to display. - * - * @param string $category The report's category. Can be a i18n token. - * @param string $title The report's title. Can be a i18n token. - * @param string $action The controller action used to load the report, ie, Referrers.getAll - * @param array $params The list of query parameters to use when loading the report. - * This list overrides query parameters currently in use. For example, - * array('idSite' => 2, 'viewDataTable' => 'goalsTable') - * would mean the goals report for site w/ ID=2 will always be loaded. - */ - public function addReport( $category, $title, $action, $params = array() ) - { - list($module, $action) = explode('.', $action); - $params = array('module' => $module, 'action' => $action) + $params; - - $categories = $this->dimensionCategories; - $categories[$category][] = array( - 'title' => $title, - 'params' => $params, - 'url' => Piwik_Url::getCurrentQueryStringWithParametersModified($params) - ); - $this->dimensionCategories = $categories; - } - - /** - * Adds a set of reports to the list of reports to display. - * - * @param array $reports An array containing report information. The array requires - * the 'category', 'title', 'action' and 'params' elements. - * For information on what they should contain, @see addReport. - */ - public function addReports( $reports ) - { - foreach ($reports as $report) - { - $this->addReport($report['category'], $report['title'], $report['action'], $report['params']); - } - } - - /** - * Renders this view. - * - * @return string The rendered view. - */ - public function render() - { - $this->firstReport = ""; - - // if there are reports & report categories added, render the first one so we can - // display it initially - $categories = $this->dimensionCategories; - if (!empty($categories)) - { - $firstCategory = reset($categories); - $firstReportInfo = reset($firstCategory); - - $oldGet = $_GET; - $oldPost = $_POST; - - foreach ($firstReportInfo['params'] as $key => $value) - { - $_GET[$key] = $value; - } - - $_POST = array(); - - $module = $firstReportInfo['params']['module']; - $action = $firstReportInfo['params']['action']; - $this->firstReport = Piwik_FrontController::getInstance()->fetchDispatch($module, $action); - - $_GET = $oldGet; - $_POST = $oldPost; - } - - return parent::render(); - } + /** + * Constructor. + */ + public function __construct() + { + parent::__construct(PIWIK_INCLUDE_PATH . '/plugins/CoreHome/templates/reports_by_dimension.tpl'); + $this->dimensionCategories = array(); + } + + /** + * Adds a report to the list of reports to display. + * + * @param string $category The report's category. Can be a i18n token. + * @param string $title The report's title. Can be a i18n token. + * @param string $action The controller action used to load the report, ie, Referrers.getAll + * @param array $params The list of query parameters to use when loading the report. + * This list overrides query parameters currently in use. For example, + * array('idSite' => 2, 'viewDataTable' => 'goalsTable') + * would mean the goals report for site w/ ID=2 will always be loaded. + */ + public function addReport($category, $title, $action, $params = array()) + { + list($module, $action) = explode('.', $action); + $params = array('module' => $module, 'action' => $action) + $params; + + $categories = $this->dimensionCategories; + $categories[$category][] = array( + 'title' => $title, + 'params' => $params, + 'url' => Piwik_Url::getCurrentQueryStringWithParametersModified($params) + ); + $this->dimensionCategories = $categories; + } + + /** + * Adds a set of reports to the list of reports to display. + * + * @param array $reports An array containing report information. The array requires + * the 'category', 'title', 'action' and 'params' elements. + * For information on what they should contain, @see addReport. + */ + public function addReports($reports) + { + foreach ($reports as $report) { + $this->addReport($report['category'], $report['title'], $report['action'], $report['params']); + } + } + + /** + * Renders this view. + * + * @return string The rendered view. + */ + public function render() + { + $this->firstReport = ""; + + // if there are reports & report categories added, render the first one so we can + // display it initially + $categories = $this->dimensionCategories; + if (!empty($categories)) { + $firstCategory = reset($categories); + $firstReportInfo = reset($firstCategory); + + $oldGet = $_GET; + $oldPost = $_POST; + + foreach ($firstReportInfo['params'] as $key => $value) { + $_GET[$key] = $value; + } + + $_POST = array(); + + $module = $firstReportInfo['params']['module']; + $action = $firstReportInfo['params']['action']; + $this->firstReport = Piwik_FrontController::getInstance()->fetchDispatch($module, $action); + + $_GET = $oldGet; + $_POST = $oldPost; + } + + return parent::render(); + } } diff --git a/core/ViewDataTable.php b/core/ViewDataTable.php index e433b6abf8..bbe27ded80 100644 --- a/core/ViewDataTable.php +++ b/core/ViewDataTable.php @@ -19,21 +19,21 @@ * the label in the HTML output. * - 'html_label_suffix': If this metadata is present on a row, it's contents will be appended * after the label in the HTML output. - * + * * Example: * In the Controller of the plugin VisitorInterest * <pre> - * function getNumberOfVisitsPerVisitDuration( $fetch = false) + * function getNumberOfVisitsPerVisitDuration( $fetch = false) * { - * $view = Piwik_ViewDataTable::factory( 'cloud' ); - * $view->init( $this->pluginName, __FUNCTION__, 'VisitorInterest.getNumberOfVisitsPerVisitDuration' ); - * $view->setColumnsToDisplay( array('label','nb_visits') ); - * $view->disableSort(); - * $view->disableExcludeLowPopulation(); - * $view->disableOffsetInformation(); + * $view = Piwik_ViewDataTable::factory( 'cloud' ); + * $view->init( $this->pluginName, __FUNCTION__, 'VisitorInterest.getNumberOfVisitsPerVisitDuration' ); + * $view->setColumnsToDisplay( array('label','nb_visits') ); + * $view->disableSort(); + * $view->disableExcludeLowPopulation(); + * $view->disableOffsetInformation(); * - * return $this->renderView($view, $fetch); - * } + * return $this->renderView($view, $fetch); + * } * </pre> * * @see factory() for all the available output (cloud tags, html table, pie chart, vertical bar chart) @@ -42,1486 +42,1405 @@ */ abstract class Piwik_ViewDataTable { - /** - * Template file that will be loaded for this view. - * Usually set in the Piwik_ViewDataTable_* - * - * @var string eg. 'CoreHome/templates/cloud.tpl' - */ - protected $dataTableTemplate = null; - - /** - * Flag used to make sure the main() is only executed once - * - * @var bool - */ - protected $mainAlreadyExecuted = false; - - /** - * Contains the values set for the parameters - * - * @see getJavascriptVariablesToSet() - * @var array - */ - protected $variablesDefault = array(); - - /** - * Array of properties that are available in the view (from smarty) - * Used to store UI properties, eg. "show_footer", "show_search", etc. - * - * @var array - */ - protected $viewProperties = array(); - - /** - * If the current dataTable refers to a subDataTable (eg. keywordsBySearchEngineId for id=X) this variable is set to the Id - * - * @var bool|int - */ - protected $idSubtable = false; - - /** - * DataTable loaded from the API for this ViewDataTable. - * - * @var Piwik_DataTable - */ - protected $dataTable = null; - - - /** - * List of filters to apply after the data has been loaded from the API - * - * @var array - */ - protected $queuedFilters = array(); - - /** - * List of filter to apply just before the 'Generic' filters - * These filters should delete rows from the table - * @var array - */ - protected $queuedFiltersPriority = array(); - - /** - * @see init() - * @var string - */ - protected $currentControllerAction; - - /** - * @see init() - * @var string - */ - protected $currentControllerName; - - /** - * @see init() - * @var string - */ - protected $controllerActionCalledWhenRequestSubTable = null; - - /** - * @see init() - * @var string - */ - protected $apiMethodToRequestDataTable; - - /** - * This view should be an implementation of the Interface Piwik_View_Interface - * The $view object should be created in the main() method. - * - * @var Piwik_View_Interface - */ - protected $view = null; - - /** - * Array of columns names translations - * - * @var array - */ - protected $columnsTranslations = array(); - - /** - * Documentation for the metrics used in the current report. - * Received from the Plugin API, used for inline help. - * - * @var array - */ - protected $metricsDocumentation = false; - - /** - * Documentation for the report. - * Received from the Plugin API, used for inline help. - * - * @var array - */ - protected $documentation = false; - - /** - * 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_*. - * This method should create and initialize a $this->view object @see Piwik_View_Interface - * - * @return mixed either prints the result or returns the output string - */ - abstract public function main(); - - /** - * Unique string ID that defines the format of the dataTable, eg. "pieChart", "table", etc. - * - * @return string - */ - abstract protected function getViewDataTableId(); - - /** - * Returns a Piwik_ViewDataTable_* object. - * By default it will return a ViewDataTable_Html - * If there is a viewDataTable parameter in the URL, a ViewDataTable of this 'viewDataTable' type will be returned. - * If defaultType is specified and if there is no 'viewDataTable' in the URL, a ViewDataTable of this $defaultType will be returned. - * If force is set to true, a ViewDataTable of the $defaultType will be returned in all cases. - * - * @param string $defaultType Any of these: table, cloud, graphPie, graphVerticalBar, graphEvolution, sparkline, generateDataChart* - * @param bool $force If set to true, returns a ViewDataTable of the $defaultType - * @return Piwik_ViewDataTable - */ - static public function factory( $defaultType = null, $force = false) - { - if(is_null($defaultType)) - { - $defaultType = 'table'; - } - - if($force === true) - { - $type = $defaultType; - } - else - { - $type = Piwik_Common::getRequestVar('viewDataTable', $defaultType, 'string'); - } - switch($type) - { - case 'cloud': - return new Piwik_ViewDataTable_Cloud(); - break; - - case 'graphPie': - return new Piwik_ViewDataTable_GenerateGraphHTML_ChartPie(); - break; - - case 'graphVerticalBar': - return new Piwik_ViewDataTable_GenerateGraphHTML_ChartVerticalBar(); - break; - - case 'graphEvolution': - return new Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution(); - break; - - case 'sparkline': - return new Piwik_ViewDataTable_Sparkline(); - break; - - case 'generateDataChartVerticalBar': - return new Piwik_ViewDataTable_GenerateGraphData_ChartVerticalBar(); - break; - - case 'generateDataChartPie': - return new Piwik_ViewDataTable_GenerateGraphData_ChartPie(); - break; - - case 'generateDataChartEvolution': - return new Piwik_ViewDataTable_GenerateGraphData_ChartEvolution(); - break; - - case 'tableAllColumns': - return new Piwik_ViewDataTable_HtmlTable_AllColumns(); - break; - - case 'tableGoals': - return new Piwik_ViewDataTable_HtmlTable_Goals(); - break; - - case 'table': - default: - return new Piwik_ViewDataTable_HtmlTable(); - break; - } - } - - /** - * Inits the object given the $currentControllerName, $currentControllerAction of - * the calling controller action, eg. 'Referers' 'getLongListOfKeywords'. - * The initialization also requires the $apiMethodToRequestDataTable of the API method - * to call in order to get the DataTable, eg. 'Referers.getKeywords'. - * The optional $controllerActionCalledWhenRequestSubTable defines the method name of the API to call when there is a idSubtable. - * This value would be used by the javascript code building the GET request to the API. - * - * Example: - * For the keywords listing, a click on the row loads the subTable of the Search Engines for this row. - * In this case $controllerActionCalledWhenRequestSubTable = 'getSearchEnginesFromKeywordId'. - * The GET request will hit 'Referers.getSearchEnginesFromKeywordId'. - * - * @param string $currentControllerName eg. 'Referers' - * @param string $currentControllerAction eg. 'getKeywords' - * @param string $apiMethodToRequestDataTable eg. 'Referers.getKeywords' - * @param string $controllerActionCalledWhenRequestSubTable eg. 'getSearchEnginesFromKeywordId' - */ - public function init( $currentControllerName, - $currentControllerAction, - $apiMethodToRequestDataTable, - $controllerActionCalledWhenRequestSubTable = null) - { - $this->currentControllerName = $currentControllerName; - $this->currentControllerAction = $currentControllerAction; - $this->apiMethodToRequestDataTable = $apiMethodToRequestDataTable; - $this->controllerActionCalledWhenRequestSubTable = $controllerActionCalledWhenRequestSubTable; - $this->idSubtable = Piwik_Common::getRequestVar('idSubtable', false, 'int'); - - $this->viewProperties['show_goals'] = false; - $this->viewProperties['show_ecommerce'] = false; - $this->viewProperties['show_search'] = Piwik_Common::getRequestVar('show_search', true); - $this->viewProperties['show_table'] = Piwik_Common::getRequestVar('show_table', true); - $this->viewProperties['show_table_all_columns'] = Piwik_Common::getRequestVar('show_table_all_columns', true); - $this->viewProperties['show_all_views_icons'] = Piwik_Common::getRequestVar('show_all_views_icons', true); - $this->viewProperties['hide_all_views_icons'] = Piwik_Common::getRequestVar('hide_all_views_icons', false); - $this->viewProperties['hide_annotations_view'] = Piwik_Common::getRequestVar('hide_annotations_view', true); - $this->viewProperties['show_bar_chart'] = Piwik_Common::getRequestVar('show_barchart', true); - $this->viewProperties['show_pie_chart'] = Piwik_Common::getRequestVar('show_piechart', true); - $this->viewProperties['show_tag_cloud'] = Piwik_Common::getRequestVar('show_tag_cloud', true); - $this->viewProperties['show_export_as_image_icon'] = Piwik_Common::getRequestVar('show_export_as_image_icon', false); - $this->viewProperties['show_export_as_rss_feed'] = Piwik_Common::getRequestVar('show_export_as_rss_feed', true); - $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_pagination_control'] = Piwik_Common::getRequestVar('show_pagination_control', true); - $this->viewProperties['show_limit_control'] = false; - $this->viewProperties['show_footer'] = Piwik_Common::getRequestVar('show_footer', true); - $this->viewProperties['show_footer_icons'] = ($this->idSubtable == false); - $this->viewProperties['show_related_reports'] = Piwik_Common::getRequestVar('show_related_reports', true); - $this->viewProperties['apiMethodToRequestDataTable'] = $this->apiMethodToRequestDataTable; - $this->viewProperties['uniqueId'] = $this->getUniqueIdViewDataTable(); - $this->viewProperties['exportLimit'] = Piwik_Config::getInstance()->General['API_datatable_default_limit']; - - $this->viewProperties['highlight_summary_row'] = false; - $this->viewProperties['metadata'] = array(); - - $this->viewProperties['relatedReports'] = array(); - $this->viewProperties['title'] = 'unknown'; - $this->viewProperties['self_url'] = $this->getBaseReportUrl($currentControllerName, $currentControllerAction); - $this->viewProperties['tooltip_metadata_name'] = false; - - $standardColumnNameToTranslation = array_merge( - Piwik_API_API::getInstance()->getDefaultMetrics(), - Piwik_API_API::getInstance()->getDefaultProcessedMetrics() - ); - $this->setColumnsTranslations($standardColumnNameToTranslation); - } - - /** - * Forces the View to use a given template. - * Usually the template to use is set in the specific ViewDataTable_* - * eg. 'CoreHome/templates/cloud.tpl' - * But some users may want to force this template to some other value - * - * @param string $tpl eg .'MyPlugin/templates/templateToUse.tpl' - */ - public function setTemplate( $tpl ) - { - $this->dataTableTemplate = $tpl; - } - - /** - * Returns the View_Interface. - * You can then call render() on this object. - * - * @return Piwik_View_Interface - * @throws exception if the view object was not created - */ - public function getView() - { - if(is_null($this->view)) - { - throw new Exception('The $this->view object has not been created. + /** + * Template file that will be loaded for this view. + * Usually set in the Piwik_ViewDataTable_* + * + * @var string eg. 'CoreHome/templates/cloud.tpl' + */ + protected $dataTableTemplate = null; + + /** + * Flag used to make sure the main() is only executed once + * + * @var bool + */ + protected $mainAlreadyExecuted = false; + + /** + * Contains the values set for the parameters + * + * @see getJavascriptVariablesToSet() + * @var array + */ + protected $variablesDefault = array(); + + /** + * Array of properties that are available in the view (from smarty) + * Used to store UI properties, eg. "show_footer", "show_search", etc. + * + * @var array + */ + protected $viewProperties = array(); + + /** + * If the current dataTable refers to a subDataTable (eg. keywordsBySearchEngineId for id=X) this variable is set to the Id + * + * @var bool|int + */ + protected $idSubtable = false; + + /** + * DataTable loaded from the API for this ViewDataTable. + * + * @var Piwik_DataTable + */ + protected $dataTable = null; + + + /** + * List of filters to apply after the data has been loaded from the API + * + * @var array + */ + protected $queuedFilters = array(); + + /** + * List of filter to apply just before the 'Generic' filters + * These filters should delete rows from the table + * @var array + */ + protected $queuedFiltersPriority = array(); + + /** + * @see init() + * @var string + */ + protected $currentControllerAction; + + /** + * @see init() + * @var string + */ + protected $currentControllerName; + + /** + * @see init() + * @var string + */ + protected $controllerActionCalledWhenRequestSubTable = null; + + /** + * @see init() + * @var string + */ + protected $apiMethodToRequestDataTable; + + /** + * This view should be an implementation of the Interface Piwik_View_Interface + * The $view object should be created in the main() method. + * + * @var Piwik_View_Interface + */ + protected $view = null; + + /** + * Array of columns names translations + * + * @var array + */ + protected $columnsTranslations = array(); + + /** + * Documentation for the metrics used in the current report. + * Received from the Plugin API, used for inline help. + * + * @var array + */ + protected $metricsDocumentation = false; + + /** + * Documentation for the report. + * Received from the Plugin API, used for inline help. + * + * @var array + */ + protected $documentation = false; + + /** + * 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_*. + * This method should create and initialize a $this->view object @see Piwik_View_Interface + * + * @return mixed either prints the result or returns the output string + */ + abstract public function main(); + + /** + * Unique string ID that defines the format of the dataTable, eg. "pieChart", "table", etc. + * + * @return string + */ + abstract protected function getViewDataTableId(); + + /** + * Returns a Piwik_ViewDataTable_* object. + * By default it will return a ViewDataTable_Html + * If there is a viewDataTable parameter in the URL, a ViewDataTable of this 'viewDataTable' type will be returned. + * If defaultType is specified and if there is no 'viewDataTable' in the URL, a ViewDataTable of this $defaultType will be returned. + * If force is set to true, a ViewDataTable of the $defaultType will be returned in all cases. + * + * @param string $defaultType Any of these: table, cloud, graphPie, graphVerticalBar, graphEvolution, sparkline, generateDataChart* + * @param bool $force If set to true, returns a ViewDataTable of the $defaultType + * @return Piwik_ViewDataTable + */ + static public function factory($defaultType = null, $force = false) + { + if (is_null($defaultType)) { + $defaultType = 'table'; + } + + if ($force === true) { + $type = $defaultType; + } else { + $type = Piwik_Common::getRequestVar('viewDataTable', $defaultType, 'string'); + } + switch ($type) { + case 'cloud': + return new Piwik_ViewDataTable_Cloud(); + break; + + case 'graphPie': + return new Piwik_ViewDataTable_GenerateGraphHTML_ChartPie(); + break; + + case 'graphVerticalBar': + return new Piwik_ViewDataTable_GenerateGraphHTML_ChartVerticalBar(); + break; + + case 'graphEvolution': + return new Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution(); + break; + + case 'sparkline': + return new Piwik_ViewDataTable_Sparkline(); + break; + + case 'generateDataChartVerticalBar': + return new Piwik_ViewDataTable_GenerateGraphData_ChartVerticalBar(); + break; + + case 'generateDataChartPie': + return new Piwik_ViewDataTable_GenerateGraphData_ChartPie(); + break; + + case 'generateDataChartEvolution': + return new Piwik_ViewDataTable_GenerateGraphData_ChartEvolution(); + break; + + case 'tableAllColumns': + return new Piwik_ViewDataTable_HtmlTable_AllColumns(); + break; + + case 'tableGoals': + return new Piwik_ViewDataTable_HtmlTable_Goals(); + break; + + case 'table': + default: + return new Piwik_ViewDataTable_HtmlTable(); + break; + } + } + + /** + * Inits the object given the $currentControllerName, $currentControllerAction of + * the calling controller action, eg. 'Referers' 'getLongListOfKeywords'. + * The initialization also requires the $apiMethodToRequestDataTable of the API method + * to call in order to get the DataTable, eg. 'Referers.getKeywords'. + * The optional $controllerActionCalledWhenRequestSubTable defines the method name of the API to call when there is a idSubtable. + * This value would be used by the javascript code building the GET request to the API. + * + * Example: + * For the keywords listing, a click on the row loads the subTable of the Search Engines for this row. + * In this case $controllerActionCalledWhenRequestSubTable = 'getSearchEnginesFromKeywordId'. + * The GET request will hit 'Referers.getSearchEnginesFromKeywordId'. + * + * @param string $currentControllerName eg. 'Referers' + * @param string $currentControllerAction eg. 'getKeywords' + * @param string $apiMethodToRequestDataTable eg. 'Referers.getKeywords' + * @param string $controllerActionCalledWhenRequestSubTable eg. 'getSearchEnginesFromKeywordId' + */ + public function init($currentControllerName, + $currentControllerAction, + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable = null) + { + $this->currentControllerName = $currentControllerName; + $this->currentControllerAction = $currentControllerAction; + $this->apiMethodToRequestDataTable = $apiMethodToRequestDataTable; + $this->controllerActionCalledWhenRequestSubTable = $controllerActionCalledWhenRequestSubTable; + $this->idSubtable = Piwik_Common::getRequestVar('idSubtable', false, 'int'); + + $this->viewProperties['show_goals'] = false; + $this->viewProperties['show_ecommerce'] = false; + $this->viewProperties['show_search'] = Piwik_Common::getRequestVar('show_search', true); + $this->viewProperties['show_table'] = Piwik_Common::getRequestVar('show_table', true); + $this->viewProperties['show_table_all_columns'] = Piwik_Common::getRequestVar('show_table_all_columns', true); + $this->viewProperties['show_all_views_icons'] = Piwik_Common::getRequestVar('show_all_views_icons', true); + $this->viewProperties['hide_all_views_icons'] = Piwik_Common::getRequestVar('hide_all_views_icons', false); + $this->viewProperties['hide_annotations_view'] = Piwik_Common::getRequestVar('hide_annotations_view', true); + $this->viewProperties['show_bar_chart'] = Piwik_Common::getRequestVar('show_barchart', true); + $this->viewProperties['show_pie_chart'] = Piwik_Common::getRequestVar('show_piechart', true); + $this->viewProperties['show_tag_cloud'] = Piwik_Common::getRequestVar('show_tag_cloud', true); + $this->viewProperties['show_export_as_image_icon'] = Piwik_Common::getRequestVar('show_export_as_image_icon', false); + $this->viewProperties['show_export_as_rss_feed'] = Piwik_Common::getRequestVar('show_export_as_rss_feed', true); + $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_pagination_control'] = Piwik_Common::getRequestVar('show_pagination_control', true); + $this->viewProperties['show_limit_control'] = false; + $this->viewProperties['show_footer'] = Piwik_Common::getRequestVar('show_footer', true); + $this->viewProperties['show_footer_icons'] = ($this->idSubtable == false); + $this->viewProperties['show_related_reports'] = Piwik_Common::getRequestVar('show_related_reports', true); + $this->viewProperties['apiMethodToRequestDataTable'] = $this->apiMethodToRequestDataTable; + $this->viewProperties['uniqueId'] = $this->getUniqueIdViewDataTable(); + $this->viewProperties['exportLimit'] = Piwik_Config::getInstance()->General['API_datatable_default_limit']; + + $this->viewProperties['highlight_summary_row'] = false; + $this->viewProperties['metadata'] = array(); + + $this->viewProperties['relatedReports'] = array(); + $this->viewProperties['title'] = 'unknown'; + $this->viewProperties['self_url'] = $this->getBaseReportUrl($currentControllerName, $currentControllerAction); + $this->viewProperties['tooltip_metadata_name'] = false; + + $standardColumnNameToTranslation = array_merge( + Piwik_API_API::getInstance()->getDefaultMetrics(), + Piwik_API_API::getInstance()->getDefaultProcessedMetrics() + ); + $this->setColumnsTranslations($standardColumnNameToTranslation); + } + + /** + * Forces the View to use a given template. + * Usually the template to use is set in the specific ViewDataTable_* + * eg. 'CoreHome/templates/cloud.tpl' + * But some users may want to force this template to some other value + * + * @param string $tpl eg .'MyPlugin/templates/templateToUse.tpl' + */ + public function setTemplate($tpl) + { + $this->dataTableTemplate = $tpl; + } + + /** + * Returns the View_Interface. + * You can then call render() on this object. + * + * @return Piwik_View_Interface + * @throws exception if the view object was not created + */ + public function getView() + { + if (is_null($this->view)) { + throw new Exception('The $this->view object has not been created. It should be created in the main() method of the Piwik_ViewDataTable_* subclass you are using.'); - } - return $this->view; - } - - public function getCurrentControllerAction() - { - return $this->currentControllerAction; - } - - public function getCurrentControllerName() - { - return $this->currentControllerName; - } - - public function getApiMethodToRequestDataTable() - { - return $this->apiMethodToRequestDataTable; - } - - public function getControllerActionCalledWhenRequestSubTable() - { - return $this->controllerActionCalledWhenRequestSubTable; - } - - /** - * Returns the DataTable loaded from the API - * - * @return Piwik_DataTable - * @throws exception if not yet defined - */ - public function getDataTable() - { - if(is_null($this->dataTable)) - { - throw new Exception("The DataTable object has not yet been created"); - } - return $this->dataTable; - } - - /** - * To prevent calling an API multiple times, the DataTable can be set directly. - * It won't be loaded again from the API in this case - * - * @param $dataTable - * @return void $dataTable Piwik_DataTable - */ - public function setDataTable($dataTable) - { - $this->dataTable = $dataTable; - } - - /** - * Function called by the ViewDataTable objects in order to fetch data from the API. - * The function init() must have been called before, so that the object knows which API module and action to call. - * It builds the API request string and uses Piwik_API_Request to call the API. - * The requested Piwik_DataTable object is stored in $this->dataTable. - */ - protected function loadDataTableFromAPI() - { - if(!is_null($this->dataTable)) - { - // data table is already there - // this happens when setDataTable has been used - return; - } - - // we build the request string (URL) to call the API - $requestString = $this->getRequestString(); - - // we make the request to the API - $request = new Piwik_API_Request($requestString); - - // and get the DataTable structure - $dataTable = $request->process(); - - $this->dataTable = $dataTable; - } - - /** - * Checks that the API returned a normal DataTable (as opposed to DataTable_Array) - * @throws Exception - * @return void - */ - protected function checkStandardDataTable() - { - Piwik::checkObjectTypeIs($this->dataTable, array('Piwik_DataTable')); - } - - /** - * Hook called after the dataTable has been loaded from the API - * Can be used to add, delete or modify the data freshly loaded - * - * @return bool - */ - protected function postDataTableLoadedFromAPI() - { - if(empty($this->dataTable)) - { - return false; - } - - // deal w/ table metadata - if ($this->dataTable instanceof Piwik_DataTable) - { - $this->viewProperties['metadata'] = $this->dataTable->getAllTableMetadata(); - - if (isset($this->viewProperties['metadata'][Piwik_DataTable::ARCHIVED_DATE_METADATA_NAME])) - { - $this->viewProperties['metadata'][Piwik_DataTable::ARCHIVED_DATE_METADATA_NAME] = - $this->makePrettyArchivedOnText(); - } - } - - // First, filters that delete rows - foreach($this->queuedFiltersPriority as $filter) - { - $filterName = $filter[0]; - $filterParameters = $filter[1]; - $this->dataTable->filter($filterName, $filterParameters); - } - - if (!$this->areGenericFiltersDisabled()) - { - // Second, generic filters (Sort, Limit, Replace Column Names, etc.) - $requestString = $this->getRequestString(); - $request = Piwik_API_Request::getRequestArrayFromString($requestString); - - if(!empty($this->variablesDefault['enable_sort']) - && $this->variablesDefault['enable_sort'] === 'false') - { - $request['filter_sort_column'] = $request['filter_sort_order'] = ''; - } - - $genericFilter = new Piwik_API_DataTableGenericFilter($request); - $genericFilter->filter($this->dataTable); - } - - if (!$this->areQueuedFiltersDisabled()) - { - // Finally, apply datatable filters that were queued (should be 'presentation' filters that - // do not affect the number of rows) - foreach($this->queuedFilters as $filter) - { - $filterName = $filter[0]; - $filterParameters = $filter[1]; - $this->dataTable->filter($filterName, $filterParameters); - } - } - return true; - } - - /** - * Returns true if generic filters have been disabled, false if otherwise. - * - * @return bool - */ - private function areGenericFiltersDisabled() - { - // if disable_generic_filters query param is set to '1', generic filters are disabled - if (Piwik_Common::getRequestVar('disable_generic_filters', '0', 'string') == 1) - { - return true; - } - - // if $this->disableGenericFilters() was called, generic filters are disabled - if (isset($this->variablesDefault['disable_generic_filters']) - && $this->variablesDefault['disable_generic_filters'] === true) - { - return true; - } - - return false; - } - - /** - * Returns true if queued filters have been disabled, false if otherwise. - * - * @return bool - */ - private function areQueuedFiltersDisabled() - { - return isset($this->variablesDefault['disable_queued_filters']) - && $this->variablesDefault['disable_queued_filters']; - } - - /** - * Returns prettified and translated text that describes when a report was last updated. - * - * @return string - */ - private function makePrettyArchivedOnText() - { - $dateText = $this->viewProperties['metadata'][Piwik_DataTable::ARCHIVED_DATE_METADATA_NAME]; - $date = Piwik_Date::factory($dateText); - $today = mktime(0,0,0); - if ($date->getTimestamp() > $today) - { - $elapsedSeconds = time() - $date->getTimestamp(); - $timeAgo = Piwik::getPrettyTimeFromSeconds( $elapsedSeconds ); - - return Piwik_Translate('CoreHome_ReportGeneratedXAgo', $timeAgo); - } - - $prettyDate = $date->getLocalized("%longYear%, %longMonth% %day%") . $date->toString('S'); - return Piwik_Translate('CoreHome_ReportGeneratedOn', $prettyDate); - } - - /** - * @return string URL to call the API, eg. "method=Referers.getKeywords&period=day&date=yesterday"... - */ - protected function getRequestString() - { - // we prepare the string to give to the API Request - // we setup the method and format variable - // - we request the method to call to get this specific DataTable - // - the format = original specifies that we want to get the original DataTable structure itself, not rendered - $requestString = 'method='.$this->apiMethodToRequestDataTable; - $requestString .= '&format=original'; - $requestString .= '&disable_generic_filters='.Piwik_Common::getRequestVar('disable_generic_filters', 1, 'int'); - - $toSetEventually = array( - 'filter_limit', - 'keep_summary_row', - 'filter_sort_column', - 'filter_sort_order', - 'filter_excludelowpop', - 'filter_excludelowpop_value', - 'filter_column', - 'filter_pattern', - 'disable_queued_filters', - ); - - foreach($toSetEventually as $varToSet) - { - $value = $this->getDefaultOrCurrent($varToSet); - if( false !== $value ) - { - if( is_array($value) ) - { - foreach($value as $v) - { - $requestString .= "&".$varToSet.'[]='.$v; - } - } - else - { - $requestString .= '&'.$varToSet.'='.$value; - } - } - } - return $requestString; - } - - /** - * For convenience, the client code can call methods that are defined in a specific children class - * without testing the children class type, which would trigger an error with a different children class. - * - * Example: - * ViewDataTable/Html.php defines a setColumnsToDisplay(). The client code calls this methods even if - * the ViewDataTable object is a ViewDataTable_Cloud instance (he doesn't know because of the factory()). - * But ViewDataTable_Cloud doesn't define the setColumnsToDisplay() method. - * Because we don't want to force users to test for the object type we simply catch these - * calls when they are not defined in the child and do nothing. - * - * @param string $function - * @param array $args - */ - public function __call($function, $args) - { - } - - /** - * Returns a unique ID for this ViewDataTable. - * This unique ID is used in the Javascript code: - * Any ajax loaded data is loaded within a DIV that has id=$unique_id - * The jquery code then replaces the existing html div id=$unique_id in the code with this data. - * - * @see datatable.js - * @return string - */ - 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 - if( $this->idSubtable != 0 // parent DIV has a idSubtable = 0 but the html DIV must have the name of the module.action - && $this->idSubtable !== false // case there is no idSubtable - ) - { - // see also datatable.js (the ID has to match with the html ID created to be replaced by the result of the ajax call) - $uniqIdTable = 'subDataTable_' . $this->idSubtable; - } - else - { - // the $uniqIdTable variable is used as the DIV ID in the rendered HTML - // we use the current Controller action name as it is supposed to be unique in the rendered page - $uniqIdTable = $this->currentControllerName . $this->currentControllerAction; - } - return $uniqIdTable; - } - - /** - * Sets the $uniqIdTable variable that is used as the DIV ID in the rendered HTML - * @param $uniqIdTable - */ - 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 - * @return null|string - */ - public function getUniqueIdViewDataTable() - { - if( $this->uniqIdTable == null ) - { - $this->uniqIdTable = $this->loadUniqueIdViewDataTable(); - } - return $this->uniqIdTable; - } - - /** - * Returns array of properties, eg. "show_footer", "show_search", etc. - * - * @return array of boolean - */ - protected function getViewProperties() - { - return $this->viewProperties; - } - - /** - * This functions reads the customization values for the DataTable and returns an array (name,value) to be printed in Javascript. - * This array defines things such as: - * - name of the module & action to call to request data for this table - * - optional filters information, eg. filter_limit and filter_offset - * - etc. - * - * The values are loaded: - * - from the generic filters that are applied by default @see Piwik_API_DataTableGenericFilter.php::getGenericFiltersInformation() - * - from the values already available in the GET array - * - from the values set using methods from this class (eg. setSearchPattern(), setLimit(), etc.) - * - * @return array eg. array('show_offset_information' => 0, 'show_... - */ - protected function getJavascriptVariablesToSet() - { - // build javascript variables to set - $javascriptVariablesToSet = array(); - - $genericFilters = Piwik_API_DataTableGenericFilter::getGenericFiltersInformation(); - foreach($genericFilters as $filter) - { - foreach($filter as $filterVariableName => $filterInfo) - { - // if there is a default value for this filter variable we set it - // so that it is propagated to the javascript - if(isset($filterInfo[1])) - { - $javascriptVariablesToSet[$filterVariableName] = $filterInfo[1]; - - // we set the default specified column and Order to sort by - // when this javascript variable is not set already - // for example during an AJAX call this variable will be set in the URL - // so this will not be executed (and the default sorted not be used as the sorted column might have changed in the meanwhile) - if( false !== ($defaultValue = $this->getDefault($filterVariableName))) - { - $javascriptVariablesToSet[$filterVariableName] = $defaultValue; - } - } - } - } - - foreach($_GET as $name => $value) - { - try { - $requestValue = Piwik_Common::getRequestVar($name); - } - catch(Exception $e) { - $requestValue = ''; - } - $javascriptVariablesToSet[$name] = $requestValue; - } - - // at this point there are some filters values we may have not set, - // case of the filter without default values and parameters set directly in this class - // for example setExcludeLowPopulation - // we go through all the $this->variablesDefault array and set the variables not set yet - foreach($this->variablesDefault as $name => $value) - { - if(!isset($javascriptVariablesToSet[$name] )) - { - $javascriptVariablesToSet[$name] = $value; - } - } - - if($this->dataTable instanceof Piwik_DataTable) - { - // we override the filter_sort_column with the column used for sorting, - // which can be different from the one specified (eg. if the column doesn't exist) - $javascriptVariablesToSet['filter_sort_column'] = $this->dataTable->getSortedByColumnName(); - // datatable can return "2" but we want to write "nb_visits" in the js - if(isset(Piwik_Archive::$mappingFromIdToName[$javascriptVariablesToSet['filter_sort_column']])) - { - $javascriptVariablesToSet['filter_sort_column'] = Piwik_Archive::$mappingFromIdToName[$javascriptVariablesToSet['filter_sort_column']]; - } - } - - $javascriptVariablesToSet['module'] = $this->currentControllerName; - $javascriptVariablesToSet['action'] = $this->currentControllerAction; - $javascriptVariablesToSet['viewDataTable'] = $this->getViewDataTableId(); - $javascriptVariablesToSet['controllerActionCalledWhenRequestSubTable'] = $this->controllerActionCalledWhenRequestSubTable; - - if($this->dataTable && - // Piwik_DataTable_Array doesn't have the method - !($this->dataTable instanceof Piwik_DataTable_Array) - && empty($javascriptVariablesToSet['totalRows']) - ) - { - $javascriptVariablesToSet['totalRows'] = $this->dataTable->getRowsCountBeforeLimitFilter(); - } - - // we escape the values that will be displayed in the javascript footer of each datatable - // to make sure there is no malicious code injected (the value are already htmlspecialchar'ed as they - // are loaded with Piwik_Common::getRequestVar() - foreach($javascriptVariablesToSet as &$value) - { - if(is_array($value)) - { - $value = array_map('addslashes',$value); - } - else - { - $value = addslashes($value); - } - } - - $deleteFromJavascriptVariables = array( - 'filter_excludelowpop', - 'filter_excludelowpop_value', - ); - foreach($deleteFromJavascriptVariables as $name) - { - if(isset($javascriptVariablesToSet[$name])) - { - unset($javascriptVariablesToSet[$name]); - } - } - return $javascriptVariablesToSet; - } - - /** - * Returns, for a given parameter, the value of this parameter in the REQUEST array. - * If not set, returns the default value for this parameter @see getDefault() - * - * @param string $nameVar - * @return string|mixed Value of this parameter - */ - protected function getDefaultOrCurrent( $nameVar ) - { - if(isset($_GET[$nameVar])) - { - return Piwik_Common::sanitizeInputValue($_GET[$nameVar]); - } - $default = $this->getDefault($nameVar); - return $default; - } - - /** - * Returns the default value for a given parameter. - * For example, these default values can be set using the disable* methods. - * - * @param string $nameVar - * @return mixed - */ - protected function getDefault($nameVar) - { - if(!isset($this->variablesDefault[$nameVar])) - { - return false; - } - return $this->variablesDefault[$nameVar]; - } - - /** - * The generic filters (limit, offset, sort by visit desc) will not be applied to this datatable. - */ - public function disableGenericFilters() - { - $this->variablesDefault['disable_generic_filters'] = true; - } - - /** - * The queued filters (replace column names, enhance column with percentage signs, add logo metadata information, etc.) - * will not be applied to this datatable. They can be manually applied by calling applyQueuedFilters on the datatable. - */ - public function disableQueuedFilters() - { - $this->variablesDefault['disable_queued_filters'] = true; - } - - /** - * The "X-Y of Z" and the "< Previous / Next >"-Buttons won't be displayed under this table - */ - public function disableOffsetInformationAndPaginationControls() - { - $this->viewProperties['show_offset_information'] = false; - $this->viewProperties['show_pagination_control'] = false; - } - - /** - * The "< Previous / Next >"-Buttons won't be displayed under this table - */ - public function disableShowPaginationControl() - { - $this->viewProperties['show_pagination_control'] = false; - } - - /** - * Ensures the limit dropdown will always be shown, even if pagination is disabled. - */ - public function alwaysShowLimitDropdown() - { - $this->viewProperties['show_limit_control'] = true; - } - - /** - * The "X-Y of Z" won't be displayed under this table - */ - public function disableOffsetInformation() - { - $this->viewProperties['show_offset_information'] = false; - } - - /** - * The search box won't be displayed under this table - */ - public function disableSearchBox() - { - $this->viewProperties['show_search'] = false; - } - - /** - * Do not sort this table, leave it as it comes out of the API - */ - public function disableSort() - { - $this->variablesDefault['enable_sort'] = 'false'; - } - - /** - * Do not show the footer icons (show all columns icon, "plus" icon) - */ - public function disableFooterIcons() - { - $this->viewProperties['show_footer_icons'] = false; - } - - /** - * When this method is called, the output will not contain the template datatable_footer.tpl - */ - public function disableFooter() - { - $this->viewProperties['show_footer'] = false; - } - - /** - * The "Include low population" link won't be displayed under this table - */ - public function disableExcludeLowPopulation() - { - $this->viewProperties['show_exclude_low_population'] = false; - } - - /** - * Whether or not to show the "View table" icon - */ - public function disableShowTable() - { - $this->viewProperties['show_table'] = false; - } - - /** - * Whether or not to show the "View more data" icon - */ - public function disableShowAllColumns() - { - $this->viewProperties['show_table_all_columns'] = false; - } - - /** - * Whether or not to show the tag cloud, pie charts, bar chart icons - */ - public function disableShowAllViewsIcons() - { - $this->viewProperties['show_all_views_icons'] = false; - } - - /** - * Whether or not to hide view icons altogether. - * The difference to disableShowAllViewsIcons is that not even the single icon - * will be shown. This icon might cause trouble because it reloads the graph on click. - */ - public function hideAllViewsIcons() - { - $this->viewProperties['show_all_views_icons'] = false; - $this->viewProperties['hide_all_views_icons'] = true; - } - - /** - * Whether or not to show the annotations view. This method has no effect if - * the Annotations plugin is not loaded. - */ - public function showAnnotationsView() - { - if (!Piwik_PluginsManager::getInstance()->isPluginLoaded('Annotations')) - { - return; - } - - $this->viewProperties['hide_annotations_view'] = false; - } - - /** - * Whether or not to show the bar chart icon. - */ - public function disableShowBarChart() - { - $this->viewProperties['show_bar_chart'] = false; - } - - /** - * Whether or not to show the pie chart icon. - */ - public function disableShowPieChart() - { - $this->viewProperties['show_pie_chart'] = false; - } - - /** - * Whether or not to show the tag cloud icon. - */ - public function disableTagCloud() - { - $this->viewProperties['show_tag_cloud'] = false; - } - - /** - * Whether or not to show related reports in the footer - */ - public function disableShowRelatedReports() - { - $this->viewProperties['show_related_reports'] = false; - } - - /** - * Whether or not to show the export to RSS feed icon - */ - public function disableShowExportAsRssFeed() - { - $this->viewProperties['show_export_as_rss_feed'] = false; - } - - /** - * Whether or not to show the "goal" icon - */ - public function enableShowGoals() - { - if(Piwik_PluginsManager::getInstance()->isPluginActivated('Goals')) - { - $this->viewProperties['show_goals'] = true; - } - } - - /** - * Whether or not to show the "Ecommerce orders/cart" icons - */ - public function enableShowEcommerce() - { - $this->viewProperties['show_ecommerce'] = true; - } - - /** - * Whether or not to show the summary row on every page of results. The default behavior - * is to treat the summary row like any other row. - */ - public function alwaysShowSummaryRow() - { - $this->variablesDefault['keep_summary_row'] = true; - } - - /** - * Sets the value to use for the Exclude low population filter. - * - * @param int|float If a row value is less than this value, it will be removed from the dataTable - * @param string The name of the column for which we compare the value to $minValue - */ - public function setExcludeLowPopulation( $columnName = null, $minValue = null ) - { - if(is_null($columnName)) - { - $columnName = 'nb_visits'; - } - $this->variablesDefault['filter_excludelowpop'] = $columnName; - $this->variablesDefault['filter_excludelowpop_value'] = $minValue; - } - - /** - * Sets the pattern to look for in the table (only rows matching the pattern will be kept) - * - * @param string $pattern to look for - * @param string $column to compare the pattern to - */ - public function setSearchPattern($pattern, $column) - { - $this->variablesDefault['filter_pattern'] = $pattern; - $this->variablesDefault['filter_column'] = $column; - } - - /** - * Sets the maximum number of rows of the table - * - * @param int $limit - */ - public function setLimit( $limit ) - { - if($limit !== 0) - { - $this->variablesDefault['filter_limit'] = $limit; - } - } - - /** - * Will display a message in the DataTable footer. - * - * @param string $message Message - */ - public function setFooterMessage( $message ) - { - $this->viewProperties['show_footer_message'] = $message; - } - - /** - * Sets the dataTable column to sort by. This sorting will be applied before applying the (offset, limit) filter. - * - * @param int|string $columnId eg. 'nb_visits' for some tables, or Piwik_Archive::INDEX_NB_VISITS for others - * @param string $order desc or asc - */ - public function setSortedColumn( $columnId, $order = 'desc') - { + } + return $this->view; + } + + public function getCurrentControllerAction() + { + return $this->currentControllerAction; + } + + public function getCurrentControllerName() + { + return $this->currentControllerName; + } + + public function getApiMethodToRequestDataTable() + { + return $this->apiMethodToRequestDataTable; + } + + public function getControllerActionCalledWhenRequestSubTable() + { + return $this->controllerActionCalledWhenRequestSubTable; + } + + /** + * Returns the DataTable loaded from the API + * + * @return Piwik_DataTable + * @throws exception if not yet defined + */ + public function getDataTable() + { + if (is_null($this->dataTable)) { + throw new Exception("The DataTable object has not yet been created"); + } + return $this->dataTable; + } + + /** + * To prevent calling an API multiple times, the DataTable can be set directly. + * It won't be loaded again from the API in this case + * + * @param $dataTable + * @return void $dataTable Piwik_DataTable + */ + public function setDataTable($dataTable) + { + $this->dataTable = $dataTable; + } + + /** + * Function called by the ViewDataTable objects in order to fetch data from the API. + * The function init() must have been called before, so that the object knows which API module and action to call. + * It builds the API request string and uses Piwik_API_Request to call the API. + * The requested Piwik_DataTable object is stored in $this->dataTable. + */ + protected function loadDataTableFromAPI() + { + if (!is_null($this->dataTable)) { + // data table is already there + // this happens when setDataTable has been used + return; + } + + // we build the request string (URL) to call the API + $requestString = $this->getRequestString(); + + // we make the request to the API + $request = new Piwik_API_Request($requestString); + + // and get the DataTable structure + $dataTable = $request->process(); + + $this->dataTable = $dataTable; + } + + /** + * Checks that the API returned a normal DataTable (as opposed to DataTable_Array) + * @throws Exception + * @return void + */ + protected function checkStandardDataTable() + { + Piwik::checkObjectTypeIs($this->dataTable, array('Piwik_DataTable')); + } + + /** + * Hook called after the dataTable has been loaded from the API + * Can be used to add, delete or modify the data freshly loaded + * + * @return bool + */ + protected function postDataTableLoadedFromAPI() + { + if (empty($this->dataTable)) { + return false; + } + + // deal w/ table metadata + if ($this->dataTable instanceof Piwik_DataTable) { + $this->viewProperties['metadata'] = $this->dataTable->getAllTableMetadata(); + + if (isset($this->viewProperties['metadata'][Piwik_DataTable::ARCHIVED_DATE_METADATA_NAME])) { + $this->viewProperties['metadata'][Piwik_DataTable::ARCHIVED_DATE_METADATA_NAME] = + $this->makePrettyArchivedOnText(); + } + } + + // First, filters that delete rows + foreach ($this->queuedFiltersPriority as $filter) { + $filterName = $filter[0]; + $filterParameters = $filter[1]; + $this->dataTable->filter($filterName, $filterParameters); + } + + if (!$this->areGenericFiltersDisabled()) { + // Second, generic filters (Sort, Limit, Replace Column Names, etc.) + $requestString = $this->getRequestString(); + $request = Piwik_API_Request::getRequestArrayFromString($requestString); + + if (!empty($this->variablesDefault['enable_sort']) + && $this->variablesDefault['enable_sort'] === 'false' + ) { + $request['filter_sort_column'] = $request['filter_sort_order'] = ''; + } + + $genericFilter = new Piwik_API_DataTableGenericFilter($request); + $genericFilter->filter($this->dataTable); + } + + if (!$this->areQueuedFiltersDisabled()) { + // Finally, apply datatable filters that were queued (should be 'presentation' filters that + // do not affect the number of rows) + foreach ($this->queuedFilters as $filter) { + $filterName = $filter[0]; + $filterParameters = $filter[1]; + $this->dataTable->filter($filterName, $filterParameters); + } + } + return true; + } + + /** + * Returns true if generic filters have been disabled, false if otherwise. + * + * @return bool + */ + private function areGenericFiltersDisabled() + { + // if disable_generic_filters query param is set to '1', generic filters are disabled + if (Piwik_Common::getRequestVar('disable_generic_filters', '0', 'string') == 1) { + return true; + } + + // if $this->disableGenericFilters() was called, generic filters are disabled + if (isset($this->variablesDefault['disable_generic_filters']) + && $this->variablesDefault['disable_generic_filters'] === true + ) { + return true; + } + + return false; + } + + /** + * Returns true if queued filters have been disabled, false if otherwise. + * + * @return bool + */ + private function areQueuedFiltersDisabled() + { + return isset($this->variablesDefault['disable_queued_filters']) + && $this->variablesDefault['disable_queued_filters']; + } + + /** + * Returns prettified and translated text that describes when a report was last updated. + * + * @return string + */ + private function makePrettyArchivedOnText() + { + $dateText = $this->viewProperties['metadata'][Piwik_DataTable::ARCHIVED_DATE_METADATA_NAME]; + $date = Piwik_Date::factory($dateText); + $today = mktime(0, 0, 0); + if ($date->getTimestamp() > $today) { + $elapsedSeconds = time() - $date->getTimestamp(); + $timeAgo = Piwik::getPrettyTimeFromSeconds($elapsedSeconds); + + return Piwik_Translate('CoreHome_ReportGeneratedXAgo', $timeAgo); + } + + $prettyDate = $date->getLocalized("%longYear%, %longMonth% %day%") . $date->toString('S'); + return Piwik_Translate('CoreHome_ReportGeneratedOn', $prettyDate); + } + + /** + * @return string URL to call the API, eg. "method=Referers.getKeywords&period=day&date=yesterday"... + */ + protected function getRequestString() + { + // we prepare the string to give to the API Request + // we setup the method and format variable + // - we request the method to call to get this specific DataTable + // - the format = original specifies that we want to get the original DataTable structure itself, not rendered + $requestString = 'method=' . $this->apiMethodToRequestDataTable; + $requestString .= '&format=original'; + $requestString .= '&disable_generic_filters=' . Piwik_Common::getRequestVar('disable_generic_filters', 1, 'int'); + + $toSetEventually = array( + 'filter_limit', + 'keep_summary_row', + 'filter_sort_column', + 'filter_sort_order', + 'filter_excludelowpop', + 'filter_excludelowpop_value', + 'filter_column', + 'filter_pattern', + 'disable_queued_filters', + ); + + foreach ($toSetEventually as $varToSet) { + $value = $this->getDefaultOrCurrent($varToSet); + if (false !== $value) { + if (is_array($value)) { + foreach ($value as $v) { + $requestString .= "&" . $varToSet . '[]=' . $v; + } + } else { + $requestString .= '&' . $varToSet . '=' . $value; + } + } + } + return $requestString; + } + + /** + * For convenience, the client code can call methods that are defined in a specific children class + * without testing the children class type, which would trigger an error with a different children class. + * + * Example: + * ViewDataTable/Html.php defines a setColumnsToDisplay(). The client code calls this methods even if + * the ViewDataTable object is a ViewDataTable_Cloud instance (he doesn't know because of the factory()). + * But ViewDataTable_Cloud doesn't define the setColumnsToDisplay() method. + * Because we don't want to force users to test for the object type we simply catch these + * calls when they are not defined in the child and do nothing. + * + * @param string $function + * @param array $args + */ + public function __call($function, $args) + { + } + + /** + * Returns a unique ID for this ViewDataTable. + * This unique ID is used in the Javascript code: + * Any ajax loaded data is loaded within a DIV that has id=$unique_id + * The jquery code then replaces the existing html div id=$unique_id in the code with this data. + * + * @see datatable.js + * @return string + */ + 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 + if ($this->idSubtable != 0 // parent DIV has a idSubtable = 0 but the html DIV must have the name of the module.action + && $this->idSubtable !== false // case there is no idSubtable + ) { + // see also datatable.js (the ID has to match with the html ID created to be replaced by the result of the ajax call) + $uniqIdTable = 'subDataTable_' . $this->idSubtable; + } else { + // the $uniqIdTable variable is used as the DIV ID in the rendered HTML + // we use the current Controller action name as it is supposed to be unique in the rendered page + $uniqIdTable = $this->currentControllerName . $this->currentControllerAction; + } + return $uniqIdTable; + } + + /** + * Sets the $uniqIdTable variable that is used as the DIV ID in the rendered HTML + * @param $uniqIdTable + */ + 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 + * @return null|string + */ + public function getUniqueIdViewDataTable() + { + if ($this->uniqIdTable == null) { + $this->uniqIdTable = $this->loadUniqueIdViewDataTable(); + } + return $this->uniqIdTable; + } + + /** + * Returns array of properties, eg. "show_footer", "show_search", etc. + * + * @return array of boolean + */ + protected function getViewProperties() + { + return $this->viewProperties; + } + + /** + * This functions reads the customization values for the DataTable and returns an array (name,value) to be printed in Javascript. + * This array defines things such as: + * - name of the module & action to call to request data for this table + * - optional filters information, eg. filter_limit and filter_offset + * - etc. + * + * The values are loaded: + * - from the generic filters that are applied by default @see Piwik_API_DataTableGenericFilter.php::getGenericFiltersInformation() + * - from the values already available in the GET array + * - from the values set using methods from this class (eg. setSearchPattern(), setLimit(), etc.) + * + * @return array eg. array('show_offset_information' => 0, 'show_... + */ + protected function getJavascriptVariablesToSet() + { + // build javascript variables to set + $javascriptVariablesToSet = array(); + + $genericFilters = Piwik_API_DataTableGenericFilter::getGenericFiltersInformation(); + foreach ($genericFilters as $filter) { + foreach ($filter as $filterVariableName => $filterInfo) { + // if there is a default value for this filter variable we set it + // so that it is propagated to the javascript + if (isset($filterInfo[1])) { + $javascriptVariablesToSet[$filterVariableName] = $filterInfo[1]; + + // we set the default specified column and Order to sort by + // when this javascript variable is not set already + // for example during an AJAX call this variable will be set in the URL + // so this will not be executed (and the default sorted not be used as the sorted column might have changed in the meanwhile) + if (false !== ($defaultValue = $this->getDefault($filterVariableName))) { + $javascriptVariablesToSet[$filterVariableName] = $defaultValue; + } + } + } + } + + foreach ($_GET as $name => $value) { + try { + $requestValue = Piwik_Common::getRequestVar($name); + } catch (Exception $e) { + $requestValue = ''; + } + $javascriptVariablesToSet[$name] = $requestValue; + } + + // at this point there are some filters values we may have not set, + // case of the filter without default values and parameters set directly in this class + // for example setExcludeLowPopulation + // we go through all the $this->variablesDefault array and set the variables not set yet + foreach ($this->variablesDefault as $name => $value) { + if (!isset($javascriptVariablesToSet[$name])) { + $javascriptVariablesToSet[$name] = $value; + } + } + + if ($this->dataTable instanceof Piwik_DataTable) { + // we override the filter_sort_column with the column used for sorting, + // which can be different from the one specified (eg. if the column doesn't exist) + $javascriptVariablesToSet['filter_sort_column'] = $this->dataTable->getSortedByColumnName(); + // datatable can return "2" but we want to write "nb_visits" in the js + if (isset(Piwik_Archive::$mappingFromIdToName[$javascriptVariablesToSet['filter_sort_column']])) { + $javascriptVariablesToSet['filter_sort_column'] = Piwik_Archive::$mappingFromIdToName[$javascriptVariablesToSet['filter_sort_column']]; + } + } + + $javascriptVariablesToSet['module'] = $this->currentControllerName; + $javascriptVariablesToSet['action'] = $this->currentControllerAction; + $javascriptVariablesToSet['viewDataTable'] = $this->getViewDataTableId(); + $javascriptVariablesToSet['controllerActionCalledWhenRequestSubTable'] = $this->controllerActionCalledWhenRequestSubTable; + + if ($this->dataTable && + // Piwik_DataTable_Array doesn't have the method + !($this->dataTable instanceof Piwik_DataTable_Array) + && empty($javascriptVariablesToSet['totalRows']) + ) { + $javascriptVariablesToSet['totalRows'] = $this->dataTable->getRowsCountBeforeLimitFilter(); + } + + // we escape the values that will be displayed in the javascript footer of each datatable + // to make sure there is no malicious code injected (the value are already htmlspecialchar'ed as they + // are loaded with Piwik_Common::getRequestVar() + foreach ($javascriptVariablesToSet as &$value) { + if (is_array($value)) { + $value = array_map('addslashes', $value); + } else { + $value = addslashes($value); + } + } + + $deleteFromJavascriptVariables = array( + 'filter_excludelowpop', + 'filter_excludelowpop_value', + ); + foreach ($deleteFromJavascriptVariables as $name) { + if (isset($javascriptVariablesToSet[$name])) { + unset($javascriptVariablesToSet[$name]); + } + } + return $javascriptVariablesToSet; + } + + /** + * Returns, for a given parameter, the value of this parameter in the REQUEST array. + * If not set, returns the default value for this parameter @see getDefault() + * + * @param string $nameVar + * @return string|mixed Value of this parameter + */ + protected function getDefaultOrCurrent($nameVar) + { + if (isset($_GET[$nameVar])) { + return Piwik_Common::sanitizeInputValue($_GET[$nameVar]); + } + $default = $this->getDefault($nameVar); + return $default; + } + + /** + * Returns the default value for a given parameter. + * For example, these default values can be set using the disable* methods. + * + * @param string $nameVar + * @return mixed + */ + protected function getDefault($nameVar) + { + if (!isset($this->variablesDefault[$nameVar])) { + return false; + } + return $this->variablesDefault[$nameVar]; + } + + /** + * The generic filters (limit, offset, sort by visit desc) will not be applied to this datatable. + */ + public function disableGenericFilters() + { + $this->variablesDefault['disable_generic_filters'] = true; + } + + /** + * The queued filters (replace column names, enhance column with percentage signs, add logo metadata information, etc.) + * will not be applied to this datatable. They can be manually applied by calling applyQueuedFilters on the datatable. + */ + public function disableQueuedFilters() + { + $this->variablesDefault['disable_queued_filters'] = true; + } + + /** + * The "X-Y of Z" and the "< Previous / Next >"-Buttons won't be displayed under this table + */ + public function disableOffsetInformationAndPaginationControls() + { + $this->viewProperties['show_offset_information'] = false; + $this->viewProperties['show_pagination_control'] = false; + } + + /** + * The "< Previous / Next >"-Buttons won't be displayed under this table + */ + public function disableShowPaginationControl() + { + $this->viewProperties['show_pagination_control'] = false; + } + + /** + * Ensures the limit dropdown will always be shown, even if pagination is disabled. + */ + public function alwaysShowLimitDropdown() + { + $this->viewProperties['show_limit_control'] = true; + } + + /** + * The "X-Y of Z" won't be displayed under this table + */ + public function disableOffsetInformation() + { + $this->viewProperties['show_offset_information'] = false; + } + + /** + * The search box won't be displayed under this table + */ + public function disableSearchBox() + { + $this->viewProperties['show_search'] = false; + } + + /** + * Do not sort this table, leave it as it comes out of the API + */ + public function disableSort() + { + $this->variablesDefault['enable_sort'] = 'false'; + } + + /** + * Do not show the footer icons (show all columns icon, "plus" icon) + */ + public function disableFooterIcons() + { + $this->viewProperties['show_footer_icons'] = false; + } + + /** + * When this method is called, the output will not contain the template datatable_footer.tpl + */ + public function disableFooter() + { + $this->viewProperties['show_footer'] = false; + } + + /** + * The "Include low population" link won't be displayed under this table + */ + public function disableExcludeLowPopulation() + { + $this->viewProperties['show_exclude_low_population'] = false; + } + + /** + * Whether or not to show the "View table" icon + */ + public function disableShowTable() + { + $this->viewProperties['show_table'] = false; + } + + /** + * Whether or not to show the "View more data" icon + */ + public function disableShowAllColumns() + { + $this->viewProperties['show_table_all_columns'] = false; + } + + /** + * Whether or not to show the tag cloud, pie charts, bar chart icons + */ + public function disableShowAllViewsIcons() + { + $this->viewProperties['show_all_views_icons'] = false; + } + + /** + * Whether or not to hide view icons altogether. + * The difference to disableShowAllViewsIcons is that not even the single icon + * will be shown. This icon might cause trouble because it reloads the graph on click. + */ + public function hideAllViewsIcons() + { + $this->viewProperties['show_all_views_icons'] = false; + $this->viewProperties['hide_all_views_icons'] = true; + } + + /** + * Whether or not to show the annotations view. This method has no effect if + * the Annotations plugin is not loaded. + */ + public function showAnnotationsView() + { + if (!Piwik_PluginsManager::getInstance()->isPluginLoaded('Annotations')) { + return; + } + + $this->viewProperties['hide_annotations_view'] = false; + } + + /** + * Whether or not to show the bar chart icon. + */ + public function disableShowBarChart() + { + $this->viewProperties['show_bar_chart'] = false; + } + + /** + * Whether or not to show the pie chart icon. + */ + public function disableShowPieChart() + { + $this->viewProperties['show_pie_chart'] = false; + } + + /** + * Whether or not to show the tag cloud icon. + */ + public function disableTagCloud() + { + $this->viewProperties['show_tag_cloud'] = false; + } + + /** + * Whether or not to show related reports in the footer + */ + public function disableShowRelatedReports() + { + $this->viewProperties['show_related_reports'] = false; + } + + /** + * Whether or not to show the export to RSS feed icon + */ + public function disableShowExportAsRssFeed() + { + $this->viewProperties['show_export_as_rss_feed'] = false; + } + + /** + * Whether or not to show the "goal" icon + */ + public function enableShowGoals() + { + if (Piwik_PluginsManager::getInstance()->isPluginActivated('Goals')) { + $this->viewProperties['show_goals'] = true; + } + } + + /** + * Whether or not to show the "Ecommerce orders/cart" icons + */ + public function enableShowEcommerce() + { + $this->viewProperties['show_ecommerce'] = true; + } + + /** + * Whether or not to show the summary row on every page of results. The default behavior + * is to treat the summary row like any other row. + */ + public function alwaysShowSummaryRow() + { + $this->variablesDefault['keep_summary_row'] = true; + } + + /** + * Sets the value to use for the Exclude low population filter. + * + * @param int|float If a row value is less than this value, it will be removed from the dataTable + * @param string The name of the column for which we compare the value to $minValue + */ + public function setExcludeLowPopulation($columnName = null, $minValue = null) + { + if (is_null($columnName)) { + $columnName = 'nb_visits'; + } + $this->variablesDefault['filter_excludelowpop'] = $columnName; + $this->variablesDefault['filter_excludelowpop_value'] = $minValue; + } + + /** + * Sets the pattern to look for in the table (only rows matching the pattern will be kept) + * + * @param string $pattern to look for + * @param string $column to compare the pattern to + */ + public function setSearchPattern($pattern, $column) + { + $this->variablesDefault['filter_pattern'] = $pattern; + $this->variablesDefault['filter_column'] = $column; + } + + /** + * Sets the maximum number of rows of the table + * + * @param int $limit + */ + public function setLimit($limit) + { + if ($limit !== 0) { + $this->variablesDefault['filter_limit'] = $limit; + } + } + + /** + * Will display a message in the DataTable footer. + * + * @param string $message Message + */ + public function setFooterMessage($message) + { + $this->viewProperties['show_footer_message'] = $message; + } + + /** + * Sets the dataTable column to sort by. This sorting will be applied before applying the (offset, limit) filter. + * + * @param int|string $columnId eg. 'nb_visits' for some tables, or Piwik_Archive::INDEX_NB_VISITS for others + * @param string $order desc or asc + */ + public function setSortedColumn($columnId, $order = 'desc') + { // debug_print_backtrace(); - $this->variablesDefault['filter_sort_column'] = $columnId; - $this->variablesDefault['filter_sort_order'] = $order; - } - - /** - * Returns the column name on which the table will be sorted - * - * @return string - */ - public function getSortedColumn() - { - return isset($this->variablesDefault['filter_sort_column']) ? $this->variablesDefault['filter_sort_column'] : false; - } - - /** - * Sets translation string for given column - * - * @param string $columnName column name - * @param string $columnTranslation column name translation - * @throws Exception - */ - public function setColumnTranslation( $columnName, $columnTranslation ) - { - if(empty($columnTranslation)) - { - throw new Exception('Unknown column: '.$columnName); - } - - $this->columnsTranslations[$columnName] = $columnTranslation; - } - - /** - * Returns column translation if available, in other case given column name - * - * @param string $columnName column name - * @return string - */ - public function getColumnTranslation( $columnName ) - { - if( isset($this->columnsTranslations[$columnName]) ) - { - return $this->columnsTranslations[$columnName]; - } - return $columnName; - } - - /** - * Set the documentation of a metric used in the report. - * Please note, that the default way of doing this is by using - * getReportMetadata. Only use this method, if you have a good - * reason to do so. - * - * @param string $metricIdentifier The idenentifier string of - * the metric - * @param string $documentation The metric documentation as a - * translated string - */ - public function setMetricDocumentation($metricIdentifier, $documentation) { - $this->metricsDocumentation[$metricIdentifier] = $documentation; - } - - /** - * Returns metric documentation, or false - * - * @param string $columnName column name - * @return bool - */ - public function getMetricDocumentation($columnName) - { - if ($this->metricsDocumentation === false) - { - $this->loadDocumentation(); - } - - if (!empty($this->metricsDocumentation[$columnName])) - { - return $this->metricsDocumentation[$columnName]; - } - - return false; - } - - /** - * Set the documentation of the report. - * Please note, that the default way of doing this is by using - * getReportMetadata. Only use this method, if you have a good - * reason to do so. - * - * @param string $documentation The report documentation as a - * translated string - */ - public function setReportDocumentation($documentation) { - $this->documentation = $documentation; - } - - /** - * Returns report documentation, or false - * @return array|bool - */ - public function getReportDocumentation() - { - if ($this->metricsDocumentation === false) - { - $this->loadDocumentation(); - } - - return $this->documentation; - } - - /** Load documentation from the API */ - private function loadDocumentation() - { - $this->metricsDocumentation = array(); - - $report = Piwik_API_API::getInstance()->getMetadata(0, $this->currentControllerName, $this->currentControllerAction); - $report = $report[0]; - - if (isset($report['metricsDocumentation'])) - { - $this->metricsDocumentation = $report['metricsDocumentation']; - } - - if (isset($report['documentation'])) - { - $this->documentation = $report['documentation']; - } - } - - /** - * Sets the columns that will be displayed in the HTML output - * By default all columns are displayed ($columnsNames = array() will display all columns) - * - * @param array $columnsNames Array of column names eg. array('nb_visits','nb_hits') - */ - public function setColumnsToDisplay( $columnsNames ) - { - if(!is_array($columnsNames)) - { - if (strpos($columnsNames, ',') !== false) - { - // array values are comma separated - $columnsNames = explode(',', $columnsNames); - } - else - { - $columnsNames = array($columnsNames); - } - } - $this->columnsToDisplay = array_filter($columnsNames); - } - - /** - * Returns columns names to display, in order. - * If no columns were specified to be displayed, return all columns found in the first row. - * If the data table has empty_columns meta data set, those columns will be removed. - * @param array PHP array conversion of the data table - * @return array - */ - public function getColumnsToDisplay() - { - if(empty($this->columnsToDisplay)) - { - $row = $this->dataTable->getFirstRow(); - if(empty($row)) - { - return array(); - } - - return array_keys($row->getColumns()); - } - - $this->columnsToDisplay = array_filter($this->columnsToDisplay); - - if ($this->dataTable instanceof Piwik_DataTable_Array) { - $emptyColumns = $this->dataTable->getMetadataIntersectArray(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME); - } else { - $emptyColumns = $this->dataTable->getMetadata(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME); - } - if (is_array($emptyColumns)) - { - foreach ($emptyColumns as $emptyColumn) - { - $key = array_search($emptyColumn, $this->columnsToDisplay); - if ($key !== false) - { - unset($this->columnsToDisplay[$key]); - } - } - $this->columnsToDisplay = array_values($this->columnsToDisplay); - } - - return $this->columnsToDisplay; - } - - /** - * Set whether to highlight the summary row or not. If not highlighted, it will - * look like every other row. - */ - public function setHighlightSummaryRow( $highlightSummaryRow ) - { - $this->viewProperties['highlight_summary_row'] = $highlightSummaryRow; - } - - /** - * Sets the name of the metadata to use for a custom tooltip. - */ - public function setTooltipMetadataName( $metadataName ) - { - $this->viewProperties['tooltip_metadata_name'] = $metadataName; - } - - /** - * Sets columns translations array. - * - * @param array $columnsTranslations An associative array indexed by column names, eg. array('nb_visit'=>"Numer of visits") - */ - public function setColumnsTranslations( $columnsTranslations ) - { - $this->columnsTranslations += $columnsTranslations; - } - - /** - * Sets a custom parameter, that will be printed in the javascript array associated with each datatable - * - * @param string $parameter name - * @param mixed $value - * @throws Exception - */ - public function setCustomParameter($parameter, $value) - { - if(isset($this->variablesDefault[$parameter])) - { - throw new Exception("$parameter is already defined for this DataTable."); - } - $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 string $filterName - * @param mixed $parameters - * @param bool $runBeforeGenericFilters Set to true if the filter will delete rows from the table, - * and should therefore be ran before Sort, Limit, etc. - * @return void - */ - public function queueFilter($filterName, $parameters, $runBeforeGenericFilters = false) - { - if($runBeforeGenericFilters) - { - $this->queuedFiltersPriority[] = array($filterName, $parameters); - } - else - { - $this->queuedFilters[] = array($filterName, $parameters); - } - } - - /** - * Adds one report to the set of reports that are related to this one. Related reports - * are displayed in the footer as links. When they are clicked, the report will change to - * the related report. - * - * Make sure to call setReportTitle so this report will be displayed correctly. - * - * @param string $module The report's controller name, ie, 'UserSettings'. - * @param string $action The report's controller action, ie, 'getBrowser'. - * @param string $title The text used to describe the related report. - * @param array $queryParams Any specific query params to use when loading the report. - * This can be used to, for example, make a goal report a related - * report (by adding an idGoal parameter). - */ - public function addRelatedReport( $module, $action, $title, $queryParams = array() ) - { - // don't add the related report if it references this report - if ($this->currentControllerName == $module && $this->currentControllerAction == $action) - { - return; - } - - $url = $this->getBaseReportUrl($module, $action, $queryParams); - $this->viewProperties['relatedReports'][$url] = $title; - } - - /** - * Adds a set of reports that are related to this one. Related reports are displayed in - * the footer as links. When they are clicked, the report will change to the related report. - * - * If you need to associate specific query params with a report, use the addRelatedReport - * method instead of this one. - * - * @param string $thisReportTitle The title of this report. - * @param array $relatedReports An array mapping report IDs ('Controller.methodName') with - * display text. - */ - public function addRelatedReports( $thisReportTitle, $relatedReports ) - { - $this->setReportTitle($thisReportTitle); - foreach ($relatedReports as $report => $title) - { - list($module, $action) = explode('.', $report); - $this->addRelatedReport($module, $action, $title); - } - } - - /** - * Sets the title of this report. - * - * @param string $title - */ - public function setReportTitle( $title ) - { - $this->viewProperties['title'] = $title; - } - - /** - * Sets a custom URL to use to reference this report. - * - * @param string $url - */ - public function setReportUrl( $module, $action, $queryParams = array() ) - { - $this->viewProperties['self_url'] = $this->getBaseReportUrl($module, $action, $queryParams); - } - - /** - * Returns true if it is likely that the data for this report has been purged and if the - * user should be told about that. - * - * In order for this function to return true, the following must also be true: - * - The data table for this report must either be empty or not have been fetched. - * - The period of this report is not a multiple period. - * - The date of this report must be older than the delete_reports_older_than config option. - * @return bool - */ - public function hasReportBeenPurged() - { - $strPeriod = Piwik_Common::getRequestVar('period', false); - $strDate = Piwik_Common::getRequestVar('date', false); - - if ($strPeriod !== false - && $strDate !== false - && (is_null($this->dataTable) || $this->dataTable->getRowsCount() == 0)) - { - // if range, only look at the first date - if ($strPeriod == 'range') - { - $idSite = Piwik_Common::getRequestVar('idSite', ''); - if (intval($idSite) != 0) - { - $site = new Piwik_Site($idSite); - $timezone = $site->getTimezone(); - } - else - { - $timezone = 'UTC'; - } - - $period = new Piwik_Period_Range('range', $strDate, $timezone); - $reportDate = $period->getDateStart(); - } - // if a multiple period, this function is irrelevant - else if (Piwik_Archive::isMultiplePeriod($strDate, $strPeriod)) - { - return false; - } - // otherwise, use the date as given - else - { - $reportDate = Piwik_Date::factory($strDate); - } - - $reportYear = $reportDate->toString('Y'); - $reportMonth = $reportDate->toString('m'); - - if (class_exists('Piwik_PrivacyManager') - && Piwik_PrivacyManager::shouldReportBePurged($reportYear, $reportMonth)) - { - return true; - } - } - - return false; - } - - /** - * Returns URL for this report w/o any filter parameters. - * - * @param string $module - * @param string $action - * @param array $queryParams - */ - private function getBaseReportUrl( $module, $action, $queryParams = array() ) - { - $params = array_merge($queryParams, array('module' => $module, 'action' => $action)); - - // unset all filter query params so the related report will show up in its default state, - // unless the filter param was in $queryParams - $genericFiltersInfo = Piwik_API_DataTableGenericFilter::getGenericFiltersInformation(); - foreach ($genericFiltersInfo as $filter) - { - foreach ($filter as $queryParamName => $queryParamInfo) - { - if (!isset($params[$queryParamName])) - { - $params[$queryParamName] = null; - } - } - } - - // add the related report - $url = Piwik_Url::getCurrentQueryStringWithParametersModified($params); - return $url; - } + $this->variablesDefault['filter_sort_column'] = $columnId; + $this->variablesDefault['filter_sort_order'] = $order; + } + + /** + * Returns the column name on which the table will be sorted + * + * @return string + */ + public function getSortedColumn() + { + return isset($this->variablesDefault['filter_sort_column']) ? $this->variablesDefault['filter_sort_column'] : false; + } + + /** + * Sets translation string for given column + * + * @param string $columnName column name + * @param string $columnTranslation column name translation + * @throws Exception + */ + public function setColumnTranslation($columnName, $columnTranslation) + { + if (empty($columnTranslation)) { + throw new Exception('Unknown column: ' . $columnName); + } + + $this->columnsTranslations[$columnName] = $columnTranslation; + } + + /** + * Returns column translation if available, in other case given column name + * + * @param string $columnName column name + * @return string + */ + public function getColumnTranslation($columnName) + { + if (isset($this->columnsTranslations[$columnName])) { + return $this->columnsTranslations[$columnName]; + } + return $columnName; + } + + /** + * Set the documentation of a metric used in the report. + * Please note, that the default way of doing this is by using + * getReportMetadata. Only use this method, if you have a good + * reason to do so. + * + * @param string $metricIdentifier The idenentifier string of + * the metric + * @param string $documentation The metric documentation as a + * translated string + */ + public function setMetricDocumentation($metricIdentifier, $documentation) + { + $this->metricsDocumentation[$metricIdentifier] = $documentation; + } + + /** + * Returns metric documentation, or false + * + * @param string $columnName column name + * @return bool + */ + public function getMetricDocumentation($columnName) + { + if ($this->metricsDocumentation === false) { + $this->loadDocumentation(); + } + + if (!empty($this->metricsDocumentation[$columnName])) { + return $this->metricsDocumentation[$columnName]; + } + + return false; + } + + /** + * Set the documentation of the report. + * Please note, that the default way of doing this is by using + * getReportMetadata. Only use this method, if you have a good + * reason to do so. + * + * @param string $documentation The report documentation as a + * translated string + */ + public function setReportDocumentation($documentation) + { + $this->documentation = $documentation; + } + + /** + * Returns report documentation, or false + * @return array|bool + */ + public function getReportDocumentation() + { + if ($this->metricsDocumentation === false) { + $this->loadDocumentation(); + } + + return $this->documentation; + } + + /** Load documentation from the API */ + private function loadDocumentation() + { + $this->metricsDocumentation = array(); + + $report = Piwik_API_API::getInstance()->getMetadata(0, $this->currentControllerName, $this->currentControllerAction); + $report = $report[0]; + + if (isset($report['metricsDocumentation'])) { + $this->metricsDocumentation = $report['metricsDocumentation']; + } + + if (isset($report['documentation'])) { + $this->documentation = $report['documentation']; + } + } + + /** + * Sets the columns that will be displayed in the HTML output + * By default all columns are displayed ($columnsNames = array() will display all columns) + * + * @param array $columnsNames Array of column names eg. array('nb_visits','nb_hits') + */ + public function setColumnsToDisplay($columnsNames) + { + if (!is_array($columnsNames)) { + if (strpos($columnsNames, ',') !== false) { + // array values are comma separated + $columnsNames = explode(',', $columnsNames); + } else { + $columnsNames = array($columnsNames); + } + } + $this->columnsToDisplay = array_filter($columnsNames); + } + + /** + * Returns columns names to display, in order. + * If no columns were specified to be displayed, return all columns found in the first row. + * If the data table has empty_columns meta data set, those columns will be removed. + * @param array PHP array conversion of the data table + * @return array + */ + public function getColumnsToDisplay() + { + if (empty($this->columnsToDisplay)) { + $row = $this->dataTable->getFirstRow(); + if (empty($row)) { + return array(); + } + + return array_keys($row->getColumns()); + } + + $this->columnsToDisplay = array_filter($this->columnsToDisplay); + + if ($this->dataTable instanceof Piwik_DataTable_Array) { + $emptyColumns = $this->dataTable->getMetadataIntersectArray(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME); + } else { + $emptyColumns = $this->dataTable->getMetadata(Piwik_DataTable::EMPTY_COLUMNS_METADATA_NAME); + } + if (is_array($emptyColumns)) { + foreach ($emptyColumns as $emptyColumn) { + $key = array_search($emptyColumn, $this->columnsToDisplay); + if ($key !== false) { + unset($this->columnsToDisplay[$key]); + } + } + $this->columnsToDisplay = array_values($this->columnsToDisplay); + } + + return $this->columnsToDisplay; + } + + /** + * Set whether to highlight the summary row or not. If not highlighted, it will + * look like every other row. + */ + public function setHighlightSummaryRow($highlightSummaryRow) + { + $this->viewProperties['highlight_summary_row'] = $highlightSummaryRow; + } + + /** + * Sets the name of the metadata to use for a custom tooltip. + */ + public function setTooltipMetadataName($metadataName) + { + $this->viewProperties['tooltip_metadata_name'] = $metadataName; + } + + /** + * Sets columns translations array. + * + * @param array $columnsTranslations An associative array indexed by column names, eg. array('nb_visit'=>"Numer of visits") + */ + public function setColumnsTranslations($columnsTranslations) + { + $this->columnsTranslations += $columnsTranslations; + } + + /** + * Sets a custom parameter, that will be printed in the javascript array associated with each datatable + * + * @param string $parameter name + * @param mixed $value + * @throws Exception + */ + public function setCustomParameter($parameter, $value) + { + if (isset($this->variablesDefault[$parameter])) { + throw new Exception("$parameter is already defined for this DataTable."); + } + $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 string $filterName + * @param mixed $parameters + * @param bool $runBeforeGenericFilters Set to true if the filter will delete rows from the table, + * and should therefore be ran before Sort, Limit, etc. + * @return void + */ + public function queueFilter($filterName, $parameters, $runBeforeGenericFilters = false) + { + if ($runBeforeGenericFilters) { + $this->queuedFiltersPriority[] = array($filterName, $parameters); + } else { + $this->queuedFilters[] = array($filterName, $parameters); + } + } + + /** + * Adds one report to the set of reports that are related to this one. Related reports + * are displayed in the footer as links. When they are clicked, the report will change to + * the related report. + * + * Make sure to call setReportTitle so this report will be displayed correctly. + * + * @param string $module The report's controller name, ie, 'UserSettings'. + * @param string $action The report's controller action, ie, 'getBrowser'. + * @param string $title The text used to describe the related report. + * @param array $queryParams Any specific query params to use when loading the report. + * This can be used to, for example, make a goal report a related + * report (by adding an idGoal parameter). + */ + public function addRelatedReport($module, $action, $title, $queryParams = array()) + { + // don't add the related report if it references this report + if ($this->currentControllerName == $module && $this->currentControllerAction == $action) { + return; + } + + $url = $this->getBaseReportUrl($module, $action, $queryParams); + $this->viewProperties['relatedReports'][$url] = $title; + } + + /** + * Adds a set of reports that are related to this one. Related reports are displayed in + * the footer as links. When they are clicked, the report will change to the related report. + * + * If you need to associate specific query params with a report, use the addRelatedReport + * method instead of this one. + * + * @param string $thisReportTitle The title of this report. + * @param array $relatedReports An array mapping report IDs ('Controller.methodName') with + * display text. + */ + public function addRelatedReports($thisReportTitle, $relatedReports) + { + $this->setReportTitle($thisReportTitle); + foreach ($relatedReports as $report => $title) { + list($module, $action) = explode('.', $report); + $this->addRelatedReport($module, $action, $title); + } + } + + /** + * Sets the title of this report. + * + * @param string $title + */ + public function setReportTitle($title) + { + $this->viewProperties['title'] = $title; + } + + /** + * Sets a custom URL to use to reference this report. + * + * @param string $url + */ + public function setReportUrl($module, $action, $queryParams = array()) + { + $this->viewProperties['self_url'] = $this->getBaseReportUrl($module, $action, $queryParams); + } + + /** + * Returns true if it is likely that the data for this report has been purged and if the + * user should be told about that. + * + * In order for this function to return true, the following must also be true: + * - The data table for this report must either be empty or not have been fetched. + * - The period of this report is not a multiple period. + * - The date of this report must be older than the delete_reports_older_than config option. + * @return bool + */ + public function hasReportBeenPurged() + { + $strPeriod = Piwik_Common::getRequestVar('period', false); + $strDate = Piwik_Common::getRequestVar('date', false); + + if ($strPeriod !== false + && $strDate !== false + && (is_null($this->dataTable) || $this->dataTable->getRowsCount() == 0) + ) { + // if range, only look at the first date + if ($strPeriod == 'range') { + $idSite = Piwik_Common::getRequestVar('idSite', ''); + if (intval($idSite) != 0) { + $site = new Piwik_Site($idSite); + $timezone = $site->getTimezone(); + } else { + $timezone = 'UTC'; + } + + $period = new Piwik_Period_Range('range', $strDate, $timezone); + $reportDate = $period->getDateStart(); + } // if a multiple period, this function is irrelevant + else if (Piwik_Archive::isMultiplePeriod($strDate, $strPeriod)) { + return false; + } // otherwise, use the date as given + else { + $reportDate = Piwik_Date::factory($strDate); + } + + $reportYear = $reportDate->toString('Y'); + $reportMonth = $reportDate->toString('m'); + + if (class_exists('Piwik_PrivacyManager') + && Piwik_PrivacyManager::shouldReportBePurged($reportYear, $reportMonth) + ) { + return true; + } + } + + return false; + } + + /** + * Returns URL for this report w/o any filter parameters. + * + * @param string $module + * @param string $action + * @param array $queryParams + */ + private function getBaseReportUrl($module, $action, $queryParams = array()) + { + $params = array_merge($queryParams, array('module' => $module, 'action' => $action)); + + // unset all filter query params so the related report will show up in its default state, + // unless the filter param was in $queryParams + $genericFiltersInfo = Piwik_API_DataTableGenericFilter::getGenericFiltersInformation(); + foreach ($genericFiltersInfo as $filter) { + foreach ($filter as $queryParamName => $queryParamInfo) { + if (!isset($params[$queryParamName])) { + $params[$queryParamName] = null; + } + } + } + + // add the related report + $url = Piwik_Url::getCurrentQueryStringWithParametersModified($params); + return $url; + } } diff --git a/core/ViewDataTable/Cloud.php b/core/ViewDataTable/Cloud.php index ec83741dc8..7b106f3a24 100644 --- a/core/ViewDataTable/Cloud.php +++ b/core/ViewDataTable/Cloud.php @@ -18,61 +18,60 @@ */ class Piwik_ViewDataTable_Cloud extends Piwik_ViewDataTable { - protected $displayLogoInsteadOfLabel = false; + protected $displayLogoInsteadOfLabel = false; - public function setDisplayLogoInTagCloud($bool) - { - $this->displayLogoInsteadOfLabel = $bool; - } - - protected function getViewDataTableId() - { - return 'cloud'; - } + public function setDisplayLogoInTagCloud($bool) + { + $this->displayLogoInsteadOfLabel = $bool; + } - /** - * @see Piwik_ViewDataTable::init() - * @param string $currentControllerName - * @param string $currentControllerAction - * @param string $apiMethodToRequestDataTable - * @param null|string $controllerActionCalledWhenRequestSubTable - */ - function init($currentControllerName, - $currentControllerAction, - $apiMethodToRequestDataTable, - $controllerActionCalledWhenRequestSubTable = null) - { - parent::init($currentControllerName, - $currentControllerAction, - $apiMethodToRequestDataTable, - $controllerActionCalledWhenRequestSubTable); - $this->dataTableTemplate = 'CoreHome/templates/cloud.tpl'; - $this->disableOffsetInformation(); - $this->disableExcludeLowPopulation(); - } - - /** - * @see Piwik_ViewDataTable::main() - * - * @return null - */ - public function main() - { - if($this->mainAlreadyExecuted) - { - return; - } - $this->mainAlreadyExecuted = true; + protected function getViewDataTableId() + { + return 'cloud'; + } - $this->isDataAvailable = true; - try { - $this->loadDataTableFromAPI(); - } catch(Exception $e) { - $this->isDataAvailable = false; - } - $this->checkStandardDataTable(); - $this->view = $this->buildView(); - } + /** + * @see Piwik_ViewDataTable::init() + * @param string $currentControllerName + * @param string $currentControllerAction + * @param string $apiMethodToRequestDataTable + * @param null|string $controllerActionCalledWhenRequestSubTable + */ + function init($currentControllerName, + $currentControllerAction, + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable = null) + { + parent::init($currentControllerName, + $currentControllerAction, + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable); + $this->dataTableTemplate = 'CoreHome/templates/cloud.tpl'; + $this->disableOffsetInformation(); + $this->disableExcludeLowPopulation(); + } + + /** + * @see Piwik_ViewDataTable::main() + * + * @return null + */ + public function main() + { + if ($this->mainAlreadyExecuted) { + return; + } + $this->mainAlreadyExecuted = true; + + $this->isDataAvailable = true; + try { + $this->loadDataTableFromAPI(); + } catch (Exception $e) { + $this->isDataAvailable = false; + } + $this->checkStandardDataTable(); + $this->view = $this->buildView(); + } /** * Returns the name of the first numeric column to be displayed @@ -81,61 +80,54 @@ class Piwik_ViewDataTable_Cloud extends Piwik_ViewDataTable * @return string */ public function getColumnToDisplay() - { - $columns = parent::getColumnsToDisplay(); - // not label, but the first numeric column - return $columns[1]; - } - - protected function buildView() - { - $view = new Piwik_View($this->dataTableTemplate); - if(!$this->isDataAvailable) - { - $view->cloudValues = array(); - } - else - { - $columnToDisplay = $this->getColumnToDisplay(); - $columnTranslation = $this->getColumnTranslation($columnToDisplay); - $values = $this->dataTable->getColumn($columnToDisplay); - $labels = $this->dataTable->getColumn('label'); - $labelMetadata = array(); - foreach($this->dataTable->getRows() as $row) - { - $logo = false; - if($this->displayLogoInsteadOfLabel) - { - $logo = $row->getMetadata('logo'); - } - $labelMetadata[$row->getColumn('label')] = array( - 'logo' => $logo, - 'url' => $row->getMetadata('url'), - ); - } - $cloud = new Piwik_Visualization_Cloud(); - foreach($labels as $i => $label) - { - $cloud->addWord($label, $values[$i]); - } - $cloudValues = $cloud->render('array'); - foreach($cloudValues as &$value) - { - $value['logoWidth'] = round(max(16, $value['percent'])); - } - $view->columnTranslation = $columnTranslation; - $view->labelMetadata = $labelMetadata; - $view->cloudValues = $cloudValues; - } - $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); - $view->properties = $this->getViewProperties(); - $view->reportDocumentation = $this->getReportDocumentation(); - - // if it's likely that the report data for this data table has been purged, - // set whether we should display a message to that effect. - $view->showReportDataWasPurgedMessage = $this->hasReportBeenPurged(); - $view->deleteReportsOlderThan = Piwik_GetOption('delete_reports_older_than'); - - return $view; - } + { + $columns = parent::getColumnsToDisplay(); + // not label, but the first numeric column + return $columns[1]; + } + + protected function buildView() + { + $view = new Piwik_View($this->dataTableTemplate); + if (!$this->isDataAvailable) { + $view->cloudValues = array(); + } else { + $columnToDisplay = $this->getColumnToDisplay(); + $columnTranslation = $this->getColumnTranslation($columnToDisplay); + $values = $this->dataTable->getColumn($columnToDisplay); + $labels = $this->dataTable->getColumn('label'); + $labelMetadata = array(); + foreach ($this->dataTable->getRows() as $row) { + $logo = false; + if ($this->displayLogoInsteadOfLabel) { + $logo = $row->getMetadata('logo'); + } + $labelMetadata[$row->getColumn('label')] = array( + 'logo' => $logo, + 'url' => $row->getMetadata('url'), + ); + } + $cloud = new Piwik_Visualization_Cloud(); + foreach ($labels as $i => $label) { + $cloud->addWord($label, $values[$i]); + } + $cloudValues = $cloud->render('array'); + foreach ($cloudValues as &$value) { + $value['logoWidth'] = round(max(16, $value['percent'])); + } + $view->columnTranslation = $columnTranslation; + $view->labelMetadata = $labelMetadata; + $view->cloudValues = $cloudValues; + } + $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); + $view->properties = $this->getViewProperties(); + $view->reportDocumentation = $this->getReportDocumentation(); + + // if it's likely that the report data for this data table has been purged, + // set whether we should display a message to that effect. + $view->showReportDataWasPurgedMessage = $this->hasReportBeenPurged(); + $view->deleteReportsOlderThan = Piwik_GetOption('delete_reports_older_than'); + + return $view; + } } diff --git a/core/ViewDataTable/GenerateGraphData.php b/core/ViewDataTable/GenerateGraphData.php index ad1779b64b..51b2956cfc 100644 --- a/core/ViewDataTable/GenerateGraphData.php +++ b/core/ViewDataTable/GenerateGraphData.php @@ -15,15 +15,15 @@ * You can set the number of elements to appear in the graph using: setGraphLimit(); * Example: * <pre> - * function getWebsites( $fetch = false) - * { - * $view = Piwik_ViewDataTable::factory(); - * $view->init( $this->pluginName, 'getWebsites', 'Referers.getWebsites', 'getUrlsFromWebsiteId' ); - * $view->setColumnsToDisplay( array('label','nb_visits') ); - * $view->setLimit(10); - * $view->setGraphLimit(12); - * return $this->renderView($view, $fetch); - * } + * function getWebsites( $fetch = false) + * { + * $view = Piwik_ViewDataTable::factory(); + * $view->init( $this->pluginName, 'getWebsites', 'Referers.getWebsites', 'getUrlsFromWebsiteId' ); + * $view->setColumnsToDisplay( array('label','nb_visits') ); + * $view->setLimit(10); + * $view->setGraphLimit(12); + * return $this->renderView($view, $fetch); + * } * </pre> * * @package Piwik @@ -31,223 +31,212 @@ */ abstract class Piwik_ViewDataTable_GenerateGraphData extends Piwik_ViewDataTable { - /** - * Number of elements to display in the graph. - * @var int - */ - protected $graphLimit = null; - protected $yAxisUnit = ''; - - // used for the series picker - protected $selectableColumns = array(); - - public function setAxisYUnit($unit) - { - $this->yAxisUnit = $unit; - } - - /** - * Sets the number max of elements to display (number of pie slice, vertical bars, etc.) - * If the data has more elements than $limit then the last part of the data will be the sum of all the remaining data. - * - * @param int $limit - */ - public function setGraphLimit( $limit ) - { - $this->graphLimit = $limit; - } - - /** - * Returns numbers of elemnts to display in the graph - * - * @return int - */ - public function getGraphLimit() - { - return $this->graphLimit; - } - - protected $displayPercentageInTooltip = true; - - /** - * The percentage in tooltips is computed based on the sum of all values for the plotted column. - * If the sum of the column in the data set is not the number of elements in the data set, - * for example when plotting visits that have a given plugin enabled: - * one visit can have several plugins, hence the sum is much greater than the number of visits. - * In this case displaying the percentage doesn't make sense. - */ - public function disallowPercentageInGraphTooltip() - { - $this->displayPercentageInTooltip = false; - } - - /** - * Sets the columns that can be added/removed by the user - * This is done on data level (not html level) because the columns might change after reloading via sparklines - * @param array $columnsNames Array of column names eg. array('nb_visits','nb_hits') - */ - public function setSelectableColumns($columnsNames) - { - // the array contains values if enableShowGoals() has been used - // add $columnsNames to the beginning of the array - $this->selectableColumns = array_merge($columnsNames, $this->selectableColumns); - } - - /** - * The implementation of this method in Piwik_ViewDataTable passes to the graph whether the - * goals icon should be displayed or not. Here, we use it to implicitly add the goal metrics - * to the metrics picker. - */ - public function enableShowGoals() - { - parent::enableShowGoals(); - - $goalMetrics = array('nb_conversions', 'revenue'); - $this->selectableColumns = array_merge($this->selectableColumns, $goalMetrics); - - $this->setColumnTranslation('nb_conversions', Piwik_Translate('Goals_ColumnConversions')); - $this->setColumnTranslation('revenue', Piwik_Translate('General_TotalRevenue')); - } - - /** - * Used in initChartObjectData to add the series picker config to the view object - * @param bool $multiSelect - */ - protected function addSeriesPickerToView($multiSelect=true) - { - if (count($this->selectableColumns) - && Piwik_Common::getRequestVar('showSeriesPicker', 1) == 1) - { - // build the final configuration for the series picker - $columnsToDisplay = $this->getColumnsToDisplay(); - $selectableColumns = array(); - - foreach ($this->selectableColumns as $column) - { - $selectableColumns[] = array( - 'column' => $column, - 'translation' => $this->getColumnTranslation($column), - 'displayed' => in_array($column, $columnsToDisplay) - ); - } - $this->view->setSelectableColumns($selectableColumns, $multiSelect); - } - } - - protected function getUnitsForColumnsToDisplay() - { - // derive units from column names - $idSite = Piwik_Common::getRequestVar('idSite', null, 'int'); - $units = $this->deriveUnitsFromRequestedColumnNames($this->getColumnsToDisplay(), $idSite); - if(!empty($this->yAxisUnit)) - { - // force unit to the value set via $this->setAxisYUnit() - foreach ($units as &$unit) - { - $unit = $this->yAxisUnit; - } - } - - return $units; - } - - protected function deriveUnitsFromRequestedColumnNames($requestedColumnNames, $idSite) - { - $units = array(); - foreach($requestedColumnNames as $columnName) - { - $derivedUnit = Piwik_API_API::getUnit($columnName, $idSite); - $units[$columnName] = empty($derivedUnit) ? false : $derivedUnit; - } - return $units; - } - - public function main() - { - if($this->mainAlreadyExecuted) - { - return; - } - $this->mainAlreadyExecuted = true; - - // 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(); - - // throws exception if no view access - $this->loadDataTableFromAPI(); - $this->checkStandardDataTable(); - $this->postDataTableLoadedFromAPI(); - - $graphLimit = $this->getGraphLimit(); - if(!empty($graphLimit)) - { - $offsetStartSummary = $this->getGraphLimit() - 1; - $this->dataTable->filter('AddSummaryRow', - array($offsetStartSummary, - Piwik_Translate('General_Others'), - - // Column to sort by, before truncation - $this->dataTable->getSortedByColumnName() - ? $this->dataTable->getSortedByColumnName() - : Piwik_Archive::INDEX_NB_VISITS - ) - ); - } - $this->isDataAvailable = $this->dataTable->getRowsCount() != 0; - - // if addTotalRow was called in GenerateGraphHTML, add a row containing totals of - // different metrics - if (Piwik_Common::getRequestVar('add_total_row', 0) == 1) - { - $this->dataTable->queueFilter('AddSummaryRow', array(0, Piwik_Translate('General_Total'), null, false)); - } - - if($this->isDataAvailable) - { - $this->initChartObjectData(); - } - $this->view->customizeChartProperties(); - } - - protected function initChartObjectData() - { - $this->dataTable->applyQueuedFilters(); - - // We apply a filter to the DataTable, decoding the label column (useful for keywords for example) - $this->dataTable->filter('ColumnCallbackReplace', array('label','urldecode')); - - $xLabels = $this->dataTable->getColumn('label'); - $columnNames = parent::getColumnsToDisplay(); - if(($labelColumnFound = array_search('label',$columnNames)) !== false) - { - unset($columnNames[$labelColumnFound]); - } - - $columnNameToTranslation = $columnNameToValue = array(); - foreach($columnNames as $columnName) - { - $columnNameToTranslation[$columnName] = $this->getColumnTranslation($columnName); - $columnNameToValue[$columnName] = $this->dataTable->getColumn($columnName); - } - $this->view->setAxisXLabels($xLabels); - $this->view->setAxisYValues($columnNameToValue); - $this->view->setAxisYLabels($columnNameToTranslation); - $this->view->setAxisYUnit($this->yAxisUnit); - $this->view->setDisplayPercentageInTooltip($this->displayPercentageInTooltip); - - // show_all_ticks is not real query param, it is set by GenerateGraphHTML. - if (Piwik_Common::getRequestVar('show_all_ticks', 0) == 1) - { - $this->view->showAllTicks(); - } - - $units = $this->getUnitsForColumnsToDisplay(); - $this->view->setAxisYUnits($units); - - $this->addSeriesPickerToView(); - } + /** + * Number of elements to display in the graph. + * @var int + */ + protected $graphLimit = null; + protected $yAxisUnit = ''; + + // used for the series picker + protected $selectableColumns = array(); + + public function setAxisYUnit($unit) + { + $this->yAxisUnit = $unit; + } + + /** + * Sets the number max of elements to display (number of pie slice, vertical bars, etc.) + * If the data has more elements than $limit then the last part of the data will be the sum of all the remaining data. + * + * @param int $limit + */ + public function setGraphLimit($limit) + { + $this->graphLimit = $limit; + } + + /** + * Returns numbers of elemnts to display in the graph + * + * @return int + */ + public function getGraphLimit() + { + return $this->graphLimit; + } + + protected $displayPercentageInTooltip = true; + + /** + * The percentage in tooltips is computed based on the sum of all values for the plotted column. + * If the sum of the column in the data set is not the number of elements in the data set, + * for example when plotting visits that have a given plugin enabled: + * one visit can have several plugins, hence the sum is much greater than the number of visits. + * In this case displaying the percentage doesn't make sense. + */ + public function disallowPercentageInGraphTooltip() + { + $this->displayPercentageInTooltip = false; + } + + /** + * Sets the columns that can be added/removed by the user + * This is done on data level (not html level) because the columns might change after reloading via sparklines + * @param array $columnsNames Array of column names eg. array('nb_visits','nb_hits') + */ + public function setSelectableColumns($columnsNames) + { + // the array contains values if enableShowGoals() has been used + // add $columnsNames to the beginning of the array + $this->selectableColumns = array_merge($columnsNames, $this->selectableColumns); + } + + /** + * The implementation of this method in Piwik_ViewDataTable passes to the graph whether the + * goals icon should be displayed or not. Here, we use it to implicitly add the goal metrics + * to the metrics picker. + */ + public function enableShowGoals() + { + parent::enableShowGoals(); + + $goalMetrics = array('nb_conversions', 'revenue'); + $this->selectableColumns = array_merge($this->selectableColumns, $goalMetrics); + + $this->setColumnTranslation('nb_conversions', Piwik_Translate('Goals_ColumnConversions')); + $this->setColumnTranslation('revenue', Piwik_Translate('General_TotalRevenue')); + } + + /** + * Used in initChartObjectData to add the series picker config to the view object + * @param bool $multiSelect + */ + protected function addSeriesPickerToView($multiSelect = true) + { + if (count($this->selectableColumns) + && Piwik_Common::getRequestVar('showSeriesPicker', 1) == 1 + ) { + // build the final configuration for the series picker + $columnsToDisplay = $this->getColumnsToDisplay(); + $selectableColumns = array(); + + foreach ($this->selectableColumns as $column) { + $selectableColumns[] = array( + 'column' => $column, + 'translation' => $this->getColumnTranslation($column), + 'displayed' => in_array($column, $columnsToDisplay) + ); + } + $this->view->setSelectableColumns($selectableColumns, $multiSelect); + } + } + + protected function getUnitsForColumnsToDisplay() + { + // derive units from column names + $idSite = Piwik_Common::getRequestVar('idSite', null, 'int'); + $units = $this->deriveUnitsFromRequestedColumnNames($this->getColumnsToDisplay(), $idSite); + if (!empty($this->yAxisUnit)) { + // force unit to the value set via $this->setAxisYUnit() + foreach ($units as &$unit) { + $unit = $this->yAxisUnit; + } + } + + return $units; + } + + protected function deriveUnitsFromRequestedColumnNames($requestedColumnNames, $idSite) + { + $units = array(); + foreach ($requestedColumnNames as $columnName) { + $derivedUnit = Piwik_API_API::getUnit($columnName, $idSite); + $units[$columnName] = empty($derivedUnit) ? false : $derivedUnit; + } + return $units; + } + + public function main() + { + if ($this->mainAlreadyExecuted) { + return; + } + $this->mainAlreadyExecuted = true; + + // 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(); + + // throws exception if no view access + $this->loadDataTableFromAPI(); + $this->checkStandardDataTable(); + $this->postDataTableLoadedFromAPI(); + + $graphLimit = $this->getGraphLimit(); + if (!empty($graphLimit)) { + $offsetStartSummary = $this->getGraphLimit() - 1; + $this->dataTable->filter('AddSummaryRow', + array($offsetStartSummary, + Piwik_Translate('General_Others'), + + // Column to sort by, before truncation + $this->dataTable->getSortedByColumnName() + ? $this->dataTable->getSortedByColumnName() + : Piwik_Archive::INDEX_NB_VISITS + ) + ); + } + $this->isDataAvailable = $this->dataTable->getRowsCount() != 0; + + // if addTotalRow was called in GenerateGraphHTML, add a row containing totals of + // different metrics + if (Piwik_Common::getRequestVar('add_total_row', 0) == 1) { + $this->dataTable->queueFilter('AddSummaryRow', array(0, Piwik_Translate('General_Total'), null, false)); + } + + if ($this->isDataAvailable) { + $this->initChartObjectData(); + } + $this->view->customizeChartProperties(); + } + + protected function initChartObjectData() + { + $this->dataTable->applyQueuedFilters(); + + // We apply a filter to the DataTable, decoding the label column (useful for keywords for example) + $this->dataTable->filter('ColumnCallbackReplace', array('label', 'urldecode')); + + $xLabels = $this->dataTable->getColumn('label'); + $columnNames = parent::getColumnsToDisplay(); + if (($labelColumnFound = array_search('label', $columnNames)) !== false) { + unset($columnNames[$labelColumnFound]); + } + + $columnNameToTranslation = $columnNameToValue = array(); + foreach ($columnNames as $columnName) { + $columnNameToTranslation[$columnName] = $this->getColumnTranslation($columnName); + $columnNameToValue[$columnName] = $this->dataTable->getColumn($columnName); + } + $this->view->setAxisXLabels($xLabels); + $this->view->setAxisYValues($columnNameToValue); + $this->view->setAxisYLabels($columnNameToTranslation); + $this->view->setAxisYUnit($this->yAxisUnit); + $this->view->setDisplayPercentageInTooltip($this->displayPercentageInTooltip); + + // show_all_ticks is not real query param, it is set by GenerateGraphHTML. + if (Piwik_Common::getRequestVar('show_all_ticks', 0) == 1) { + $this->view->showAllTicks(); + } + + $units = $this->getUnitsForColumnsToDisplay(); + $this->view->setAxisYUnits($units); + + $this->addSeriesPickerToView(); + } } diff --git a/core/ViewDataTable/GenerateGraphData/ChartEvolution.php b/core/ViewDataTable/GenerateGraphData/ChartEvolution.php index a505cff5dc..65d7988216 100644 --- a/core/ViewDataTable/GenerateGraphData/ChartEvolution.php +++ b/core/ViewDataTable/GenerateGraphData/ChartEvolution.php @@ -1,340 +1,309 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Piwik_ViewDataTable_GenerateGraphData for the Evolution graph (eg. Last 30 days visits) using Piwik_Visualization_Chart_Evolution - * + * * @package Piwik * @subpackage Piwik_ViewDataTable */ class Piwik_ViewDataTable_GenerateGraphData_ChartEvolution extends Piwik_ViewDataTable_GenerateGraphData { - - // used for the row picker - // (the series picker configuration resides in the parent class) - protected $rowPicker = false; - protected $visibleRows = array(); - protected $rowPickerConfig = array(); - - protected function checkStandardDataTable() - { - // DataTable_Array and DataTable allowed for the evolution chart - Piwik::checkObjectTypeIs($this->dataTable, array('Piwik_DataTable_Array', 'Piwik_DataTable')); - } - - protected function getViewDataTableId() - { - return 'generateDataChartEvolution'; - } - - function __construct() - { - $this->view = new Piwik_Visualization_Chart_Evolution(); - } - - /** - * Adds the same series picker as parent::setSelectableColumns but the selectable series are not - * columns of a single row but the same column across multiple rows, e.g. the number of visits - * for each referrer type. - * @param array $visibleRows the rows that are initially visible - * @param string $matchBy the way the items in $visibleRows are matched with the data. possible values: - * - label: matches the label of the row - */ - public function addRowPicker($visibleRows, $matchBy='label') - { - $this->rowPicker = $matchBy; - - if (!is_array($visibleRows)) - { - $visibleRows = array($visibleRows); - } - $this->visibleRows = $visibleRows; - } - - /** - * This method is called for every row of every table in the DataTable_Array. - * It incrementally builds the row picker configuration and determines whether - * the row is initially visible or not. - * @param string $rowLabel - * @return bool - */ - protected function handleRowForRowPicker(&$rowLabel) - { - // determine whether row is visible - $isVisible = true; - switch ($this->rowPicker) - { - case 'label': - $isVisible = in_array($rowLabel, $this->visibleRows); - break; - } - - // build config - if (!isset($this->rowPickerConfig[$rowLabel])) - { - $this->rowPickerConfig[$rowLabel] = array( - 'label' => $rowLabel, - 'matcher' => $rowLabel, - 'displayed' => $isVisible - ); - } - - return $isVisible; - } - - protected function loadDataTableFromAPI() - { - $period = Piwik_Common::getRequestVar('period'); - // period will be overridden when 'range' is requested in the UI - // but the graph will display for each day of the range. - // Default 'range' behavior is to return the 'sum' for the range - if($period == 'range') - { - $_GET['period'] = 'day'; - } - // throws exception if no view access - parent::loadDataTableFromAPI(); - if($period == 'range') - { - $_GET['period'] = $period; - } - } - - protected function initChartObjectData() - { - // if the loaded datatable is a simple DataTable, it is most likely a plugin plotting some custom data - // we don't expect plugin developers to return a well defined Piwik_DataTable_Array - if($this->dataTable instanceof Piwik_DataTable) - { - return parent::initChartObjectData(); - } - - $this->dataTable->applyQueuedFilters(); - if(!($this->dataTable instanceof Piwik_DataTable_Array)) - { - throw new Exception("Expecting a DataTable_Array with custom format to draw an evolution chart"); - } - - // the X label is extracted from the 'period' object in the table's metadata - $xLabels = $uniqueIdsDataTable = array(); - foreach($this->dataTable->getArray() as $idDataTable => $metadataDataTable) - { - //eg. "Aug 2009" - $xLabels[] = $metadataDataTable->getMetadata('period')->getLocalizedShortString(); - // we keep track of all unique data table that we need to set a Y value for - $uniqueIdsDataTable[] = $idDataTable; - } - - $idSite = Piwik_Common::getRequestVar('idSite', null, 'int'); - $requestedColumnNames = $this->getColumnsToDisplay(); - $units = $this->getUnitsForColumnsToDisplay(); - - $yAxisLabelToUnit = array(); - $yAxisLabelToValue = array(); - foreach($this->dataTable->getArray() as $idDataTable => $dataTable) - { - foreach($dataTable->getRows() as $row) - { - $rowLabel = $row->getColumn('label'); - - // put together configuration for row picker. - // do this for every data table in the array because rows do not - // have to present for each date. - if ($this->rowPicker !== false) - { - $rowVisible = $this->handleRowForRowPicker($rowLabel); - if (!$rowVisible) - { - continue; - } - } - - // build data for request columns - foreach($requestedColumnNames as $requestedColumnName) - { - $yAxisLabel = $this->getSeriesLabel($rowLabel, $requestedColumnName); - if(($columnValue = $row->getColumn($requestedColumnName)) !== false) - { - $yAxisLabelToValue[$yAxisLabel][$idDataTable] = $columnValue; - $yAxisLabelToUnit[$yAxisLabel] = $units[$requestedColumnName]; - } - } - } - } - - // make sure all column values are set to at least zero (no gap in the graph) - $yAxisLabelToValueCleaned = array(); - foreach($uniqueIdsDataTable as $uniqueIdDataTable) - { - foreach($yAxisLabelToValue as $yAxisLabel => $idDataTableToColumnValue) - { - if(isset($idDataTableToColumnValue[$uniqueIdDataTable])) - { - $columnValue = $idDataTableToColumnValue[$uniqueIdDataTable]; - } - else - { - $columnValue = 0; - } - $yAxisLabelToValueCleaned[$yAxisLabel][] = $columnValue; - } - } - - $this->view->setAxisXLabels($xLabels); - $this->view->setAxisYValues($yAxisLabelToValueCleaned); - $this->view->setAxisYUnits($yAxisLabelToUnit); - - $countGraphElements = $this->dataTable->getRowsCount(); - $dataTables = $this->dataTable->getArray(); - $firstDatatable = reset($dataTables); - $period = $firstDatatable->getMetadata('period'); - - $stepSize = $this->getXAxisStepSize($period->getLabel(), $countGraphElements); - $this->view->setXSteps($stepSize); - - if($this->isLinkEnabled()) - { - $axisXOnClick = array(); - $queryStringAsHash = $this->getQueryStringAsHash(); - foreach($this->dataTable->getArray() as $idDataTable => $metadataDataTable) - { - $period = $metadataDataTable->getMetadata('period'); - $dateInUrl = $period->getDateStart(); - $parameters = array( - 'idSite' => $idSite, - 'period' => $period->getLabel(), - 'date' => $dateInUrl->toString(), - 'segment' => Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('segment', false)) - ); - $hash = ''; - if(!empty($queryStringAsHash)) - { - $hash = '#' . Piwik_Url::getQueryStringFromParameters( $queryStringAsHash + $parameters); - } - $link = 'index.php?' . - Piwik_Url::getQueryStringFromParameters( array( - 'module' => 'CoreHome', - 'action' => 'index', - ) + $parameters) - . $hash; - $axisXOnClick[] = $link; - } - $this->view->setAxisXOnClick($axisXOnClick); - } - - $this->addSeriesPickerToView(); - if ($this->rowPicker !== false) - { - // configure the row picker - $this->view->setSelectableRows(array_values($this->rowPickerConfig)); - } - } - - /** - * Derive the series label from the row label and the column name. - * If the row label is set, both the label and the column name are displayed. - * @param string $rowLabel - * @param string $columnName - * @return string - */ - private function getSeriesLabel($rowLabel, $columnName) - { - $metricLabel = $this->getColumnTranslation($columnName); - - if($rowLabel !== false) - { - // eg. "Yahoo! (Visits)" - $label = "$rowLabel ($metricLabel)"; - } - else - { - // eg. "Visits" - $label = $metricLabel; - } - - return $label; - } - - /** - * 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() - { - static $linkEnabled; - if(!isset($linkEnabled)) - { - // 1) Custom Date Range always have link disabled, otherwise - // the graph data set is way too big and fails to display - // 2) disableLink parameter is set in the Widgetize "embed" code - $linkEnabled = !Piwik_Common::getRequestVar('disableLink', 0, 'int') - && Piwik_Common::getRequestVar('period', 'day') != 'range'; - } - return $linkEnabled; - } - - private function getXAxisStepSize( $periodLabel, $countGraphElements ) - { - // when the number of elements plotted can be small, make sure the X legend is useful - if ($countGraphElements <= 7) - { - return 1; - } - - switch ($periodLabel) - { - case 'day': - $steps = 5; - break; - case 'week': - $steps = 4; - break; - case 'month': - $steps = 5; - break; - case 'year': - $steps = 5; - break; - default: - $steps = 5; - break; - } - - $paddedCount = $countGraphElements + 2; // pad count so last label won't be cut off - return ceil($paddedCount / $steps); - } + + // used for the row picker + // (the series picker configuration resides in the parent class) + protected $rowPicker = false; + protected $visibleRows = array(); + protected $rowPickerConfig = array(); + + protected function checkStandardDataTable() + { + // DataTable_Array and DataTable allowed for the evolution chart + Piwik::checkObjectTypeIs($this->dataTable, array('Piwik_DataTable_Array', 'Piwik_DataTable')); + } + + protected function getViewDataTableId() + { + return 'generateDataChartEvolution'; + } + + function __construct() + { + $this->view = new Piwik_Visualization_Chart_Evolution(); + } + + /** + * Adds the same series picker as parent::setSelectableColumns but the selectable series are not + * columns of a single row but the same column across multiple rows, e.g. the number of visits + * for each referrer type. + * @param array $visibleRows the rows that are initially visible + * @param string $matchBy the way the items in $visibleRows are matched with the data. possible values: + * - label: matches the label of the row + */ + public function addRowPicker($visibleRows, $matchBy = 'label') + { + $this->rowPicker = $matchBy; + + if (!is_array($visibleRows)) { + $visibleRows = array($visibleRows); + } + $this->visibleRows = $visibleRows; + } + + /** + * This method is called for every row of every table in the DataTable_Array. + * It incrementally builds the row picker configuration and determines whether + * the row is initially visible or not. + * @param string $rowLabel + * @return bool + */ + protected function handleRowForRowPicker(&$rowLabel) + { + // determine whether row is visible + $isVisible = true; + switch ($this->rowPicker) { + case 'label': + $isVisible = in_array($rowLabel, $this->visibleRows); + break; + } + + // build config + if (!isset($this->rowPickerConfig[$rowLabel])) { + $this->rowPickerConfig[$rowLabel] = array( + 'label' => $rowLabel, + 'matcher' => $rowLabel, + 'displayed' => $isVisible + ); + } + + return $isVisible; + } + + protected function loadDataTableFromAPI() + { + $period = Piwik_Common::getRequestVar('period'); + // period will be overridden when 'range' is requested in the UI + // but the graph will display for each day of the range. + // Default 'range' behavior is to return the 'sum' for the range + if ($period == 'range') { + $_GET['period'] = 'day'; + } + // throws exception if no view access + parent::loadDataTableFromAPI(); + if ($period == 'range') { + $_GET['period'] = $period; + } + } + + protected function initChartObjectData() + { + // if the loaded datatable is a simple DataTable, it is most likely a plugin plotting some custom data + // we don't expect plugin developers to return a well defined Piwik_DataTable_Array + if ($this->dataTable instanceof Piwik_DataTable) { + return parent::initChartObjectData(); + } + + $this->dataTable->applyQueuedFilters(); + if (!($this->dataTable instanceof Piwik_DataTable_Array)) { + throw new Exception("Expecting a DataTable_Array with custom format to draw an evolution chart"); + } + + // the X label is extracted from the 'period' object in the table's metadata + $xLabels = $uniqueIdsDataTable = array(); + foreach ($this->dataTable->getArray() as $idDataTable => $metadataDataTable) { + //eg. "Aug 2009" + $xLabels[] = $metadataDataTable->getMetadata('period')->getLocalizedShortString(); + // we keep track of all unique data table that we need to set a Y value for + $uniqueIdsDataTable[] = $idDataTable; + } + + $idSite = Piwik_Common::getRequestVar('idSite', null, 'int'); + $requestedColumnNames = $this->getColumnsToDisplay(); + $units = $this->getUnitsForColumnsToDisplay(); + + $yAxisLabelToUnit = array(); + $yAxisLabelToValue = array(); + foreach ($this->dataTable->getArray() as $idDataTable => $dataTable) { + foreach ($dataTable->getRows() as $row) { + $rowLabel = $row->getColumn('label'); + + // put together configuration for row picker. + // do this for every data table in the array because rows do not + // have to present for each date. + if ($this->rowPicker !== false) { + $rowVisible = $this->handleRowForRowPicker($rowLabel); + if (!$rowVisible) { + continue; + } + } + + // build data for request columns + foreach ($requestedColumnNames as $requestedColumnName) { + $yAxisLabel = $this->getSeriesLabel($rowLabel, $requestedColumnName); + if (($columnValue = $row->getColumn($requestedColumnName)) !== false) { + $yAxisLabelToValue[$yAxisLabel][$idDataTable] = $columnValue; + $yAxisLabelToUnit[$yAxisLabel] = $units[$requestedColumnName]; + } + } + } + } + + // make sure all column values are set to at least zero (no gap in the graph) + $yAxisLabelToValueCleaned = array(); + foreach ($uniqueIdsDataTable as $uniqueIdDataTable) { + foreach ($yAxisLabelToValue as $yAxisLabel => $idDataTableToColumnValue) { + if (isset($idDataTableToColumnValue[$uniqueIdDataTable])) { + $columnValue = $idDataTableToColumnValue[$uniqueIdDataTable]; + } else { + $columnValue = 0; + } + $yAxisLabelToValueCleaned[$yAxisLabel][] = $columnValue; + } + } + + $this->view->setAxisXLabels($xLabels); + $this->view->setAxisYValues($yAxisLabelToValueCleaned); + $this->view->setAxisYUnits($yAxisLabelToUnit); + + $countGraphElements = $this->dataTable->getRowsCount(); + $dataTables = $this->dataTable->getArray(); + $firstDatatable = reset($dataTables); + $period = $firstDatatable->getMetadata('period'); + + $stepSize = $this->getXAxisStepSize($period->getLabel(), $countGraphElements); + $this->view->setXSteps($stepSize); + + if ($this->isLinkEnabled()) { + $axisXOnClick = array(); + $queryStringAsHash = $this->getQueryStringAsHash(); + foreach ($this->dataTable->getArray() as $idDataTable => $metadataDataTable) { + $period = $metadataDataTable->getMetadata('period'); + $dateInUrl = $period->getDateStart(); + $parameters = array( + 'idSite' => $idSite, + 'period' => $period->getLabel(), + 'date' => $dateInUrl->toString(), + 'segment' => Piwik_Common::unsanitizeInputValue(Piwik_Common::getRequestVar('segment', false)) + ); + $hash = ''; + if (!empty($queryStringAsHash)) { + $hash = '#' . Piwik_Url::getQueryStringFromParameters($queryStringAsHash + $parameters); + } + $link = 'index.php?' . + Piwik_Url::getQueryStringFromParameters(array( + 'module' => 'CoreHome', + 'action' => 'index', + ) + $parameters) + . $hash; + $axisXOnClick[] = $link; + } + $this->view->setAxisXOnClick($axisXOnClick); + } + + $this->addSeriesPickerToView(); + if ($this->rowPicker !== false) { + // configure the row picker + $this->view->setSelectableRows(array_values($this->rowPickerConfig)); + } + } + + /** + * Derive the series label from the row label and the column name. + * If the row label is set, both the label and the column name are displayed. + * @param string $rowLabel + * @param string $columnName + * @return string + */ + private function getSeriesLabel($rowLabel, $columnName) + { + $metricLabel = $this->getColumnTranslation($columnName); + + if ($rowLabel !== false) { + // eg. "Yahoo! (Visits)" + $label = "$rowLabel ($metricLabel)"; + } else { + // eg. "Visits" + $label = $metricLabel; + } + + return $label; + } + + /** + * 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() + { + static $linkEnabled; + if (!isset($linkEnabled)) { + // 1) Custom Date Range always have link disabled, otherwise + // the graph data set is way too big and fails to display + // 2) disableLink parameter is set in the Widgetize "embed" code + $linkEnabled = !Piwik_Common::getRequestVar('disableLink', 0, 'int') + && Piwik_Common::getRequestVar('period', 'day') != 'range'; + } + return $linkEnabled; + } + + private function getXAxisStepSize($periodLabel, $countGraphElements) + { + // when the number of elements plotted can be small, make sure the X legend is useful + if ($countGraphElements <= 7) { + return 1; + } + + switch ($periodLabel) { + case 'day': + $steps = 5; + break; + case 'week': + $steps = 4; + break; + case 'month': + $steps = 5; + break; + case 'year': + $steps = 5; + break; + default: + $steps = 5; + break; + } + + $paddedCount = $countGraphElements + 2; // pad count so last label won't be cut off + return ceil($paddedCount / $steps); + } } diff --git a/core/ViewDataTable/GenerateGraphData/ChartPie.php b/core/ViewDataTable/GenerateGraphData/ChartPie.php index 91661ef485..6acd8f440e 100644 --- a/core/ViewDataTable/GenerateGraphData/ChartPie.php +++ b/core/ViewDataTable/GenerateGraphData/ChartPie.php @@ -1,43 +1,43 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Piwik_ViewDataTable_GenerateGraphData for the pie chart, using Piwik_Visualization_Chart_Pie - * + * * @package Piwik * @subpackage Piwik_ViewDataTable */ class Piwik_ViewDataTable_GenerateGraphData_ChartPie extends Piwik_ViewDataTable_GenerateGraphData { - protected $graphLimit = 6; - - protected function getViewDataTableId() - { - return 'generateDataChartPie'; - } - - function __construct() - { - $this->view = new Piwik_Visualization_Chart_Pie(); - } + protected $graphLimit = 6; + + protected function getViewDataTableId() + { + return 'generateDataChartPie'; + } + + function __construct() + { + $this->view = new Piwik_Visualization_Chart_Pie(); + } + + /** + * Manipulate the configuration of the series picker since only one metric is selectable + * for pie charts + * @param bool $multiSelect + */ + protected function addSeriesPickerToView($multiSelect = false) + { + // force $multiSelect=false + parent::addSeriesPickerToView(false); + } - /** - * Manipulate the configuration of the series picker since only one metric is selectable - * for pie charts - * @param bool $multiSelect - */ - protected function addSeriesPickerToView($multiSelect=false) - { - // force $multiSelect=false - parent::addSeriesPickerToView(false); - } - } diff --git a/core/ViewDataTable/GenerateGraphData/ChartVerticalBar.php b/core/ViewDataTable/GenerateGraphData/ChartVerticalBar.php index 4188a39091..b4c462b7ba 100644 --- a/core/ViewDataTable/GenerateGraphData/ChartVerticalBar.php +++ b/core/ViewDataTable/GenerateGraphData/ChartVerticalBar.php @@ -1,40 +1,40 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Piwik_ViewDataTable_GenerateGraphData for the vertical bar graph, using Piwik_Visualization_Chart_VerticalBar - * + * * @package Piwik * @subpackage Piwik_ViewDataTable */ class Piwik_ViewDataTable_GenerateGraphData_ChartVerticalBar extends Piwik_ViewDataTable_GenerateGraphData { - protected $graphLimit = 6; - - protected function getViewDataTableId() - { - return 'generateDataChartVerticalBar'; - } - - function __construct() - { - $this->view = new Piwik_Visualization_Chart_VerticalBar(); - } - - protected function getUnitsForColumnsToDisplay() - { - // the bar charts contain the labels a first series - // this series has to be removed from the units - $units = parent::getUnitsForColumnsToDisplay(); - array_shift($units); - return $units; - } + protected $graphLimit = 6; + + protected function getViewDataTableId() + { + return 'generateDataChartVerticalBar'; + } + + function __construct() + { + $this->view = new Piwik_Visualization_Chart_VerticalBar(); + } + + protected function getUnitsForColumnsToDisplay() + { + // the bar charts contain the labels a first series + // this series has to be removed from the units + $units = parent::getUnitsForColumnsToDisplay(); + array_shift($units); + return $units; + } } diff --git a/core/ViewDataTable/GenerateGraphHTML.php b/core/ViewDataTable/GenerateGraphHTML.php index ce3cd191bf..7a7550bbb4 100644 --- a/core/ViewDataTable/GenerateGraphHTML.php +++ b/core/ViewDataTable/GenerateGraphHTML.php @@ -18,184 +18,181 @@ */ abstract class Piwik_ViewDataTable_GenerateGraphHTML extends Piwik_ViewDataTable { - - protected $width = '100%'; - protected $height = 250; - protected $graphType = 'unknown'; - - /** - * Parameters to send to GenerateGraphData instance. Parameters are passed - * via the $_GET array. - * - * @var array - */ - protected $generateGraphDataParams = array(); - - /** - * @see Piwik_ViewDataTable::init() - * @param string $currentControllerName - * @param string $currentControllerAction - * @param string $apiMethodToRequestDataTable - * @param null $controllerActionCalledWhenRequestSubTable - */ - function init($currentControllerName, - $currentControllerAction, - $apiMethodToRequestDataTable, - $controllerActionCalledWhenRequestSubTable = null) - { - parent::init($currentControllerName, - $currentControllerAction, - $apiMethodToRequestDataTable, - $controllerActionCalledWhenRequestSubTable); - - $this->dataTableTemplate = 'CoreHome/templates/graph.tpl'; - - $this->disableOffsetInformationAndPaginationControls(); - $this->disableExcludeLowPopulation(); - $this->disableSearchBox(); - $this->enableShowExportAsImageIcon(); - - $this->parametersToModify = array( - 'viewDataTable' => $this->getViewDataTableIdToLoad(), - // in the case this controller is being executed by another controller - // eg. when being widgetized in an IFRAME - // we need to put in the URL of the graph data the real module and action - 'module' => $currentControllerName, - 'action' => $currentControllerAction, - ); - } - - public function enableShowExportAsImageIcon() - { - $this->viewProperties['show_export_as_image_icon'] = true; - } - - public function addRowEvolutionSeriesToggle($initiallyShowAllMetrics) { - $this->viewProperties['externalSeriesToggle'] = 'RowEvolutionSeriesToggle'; - $this->viewProperties['externalSeriesToggleShowAll'] = $initiallyShowAllMetrics; - } - - /** - * Sets parameters to modify in the future generated URL - * @param array $array array('nameParameter' => $newValue, ...) - */ - public function setParametersToModify($array) - { - $this->parametersToModify = array_merge($this->parametersToModify, $array); - } - - /** - * Show every x-axis tick instead of just every other one. - */ - public function showAllTicks() - { - $this->generateGraphDataParams['show_all_ticks'] = 1; - } - - /** - * Adds a row to the report containing totals for contained metrics. Mainly useful - * for evolution graphs where displaying the totals w/ the metrics is useful. - */ - public function addTotalRow() - { - $this->generateGraphDataParams['add_total_row'] = 1; - } - - /** - * 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. - * @return array - */ - protected function getJavascriptVariablesToSet() - { - $original = parent::getJavascriptVariablesToSet(); - $originalViewDataTable = $original['viewDataTable']; - - $result = $this->parametersToModify + $original;; - $result['viewDataTable'] = $originalViewDataTable; - - return $result; - } - - /** - * @see Piwik_ViewDataTable::main() - * @return null - */ - public function main() - { - if($this->mainAlreadyExecuted) - { - return; - } - $this->mainAlreadyExecuted = true; - - $this->view = $this->buildView(); - } - - protected function buildView() - { - // access control - $idSite = Piwik_Common::getRequestVar('idSite', 1, 'int'); - Piwik_API_Request::reloadAuthUsingTokenAuth(); - if(!Piwik::isUserHasViewAccess($idSite)) - { - throw new Exception(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite',array("'view'", $idSite))); - } - - // collect data - $this->parametersToModify['action'] = $this->currentControllerAction; - $this->parametersToModify = array_merge($this->variablesDefault, $this->parametersToModify); - $this->graphData = $this->getGraphData(); - - // build view - $view = new Piwik_View($this->dataTableTemplate); - - $view->width = $this->width; - $view->height = $this->height; - $view->chartDivId = $this->getUniqueIdViewDataTable()."Chart"; - $view->graphType = $this->graphType; - - $view->data = $this->graphData; - $view->isDataAvailable = strpos($this->graphData, '"series":[]') === false; - - $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); - $view->properties = $this->getViewProperties(); - - $view->reportDocumentation = $this->getReportDocumentation(); - - // if it's likely that the report data for this data table has been purged, - // set whether we should display a message to that effect. - $view->showReportDataWasPurgedMessage = $this->hasReportBeenPurged(); - $view->deleteReportsOlderThan = Piwik_GetOption('delete_reports_older_than'); - - return $view; - } - - protected function getGraphData() - { - $saveGet = $_GET; - - $params = array_merge($this->generateGraphDataParams, $this->parametersToModify); - foreach($params as $key => $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)) - { - $val = implode(',', $val); - } - $_GET[$key] = $val; - } - $content = Piwik_FrontController::getInstance()->fetchDispatch($this->currentControllerName, $this->currentControllerAction, array()); - - $_GET = $saveGet; - - return str_replace(array("\r", "\n"), '', $content); - } + + protected $width = '100%'; + protected $height = 250; + protected $graphType = 'unknown'; + + /** + * Parameters to send to GenerateGraphData instance. Parameters are passed + * via the $_GET array. + * + * @var array + */ + protected $generateGraphDataParams = array(); + + /** + * @see Piwik_ViewDataTable::init() + * @param string $currentControllerName + * @param string $currentControllerAction + * @param string $apiMethodToRequestDataTable + * @param null $controllerActionCalledWhenRequestSubTable + */ + function init($currentControllerName, + $currentControllerAction, + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable = null) + { + parent::init($currentControllerName, + $currentControllerAction, + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable); + + $this->dataTableTemplate = 'CoreHome/templates/graph.tpl'; + + $this->disableOffsetInformationAndPaginationControls(); + $this->disableExcludeLowPopulation(); + $this->disableSearchBox(); + $this->enableShowExportAsImageIcon(); + + $this->parametersToModify = array( + 'viewDataTable' => $this->getViewDataTableIdToLoad(), + // in the case this controller is being executed by another controller + // eg. when being widgetized in an IFRAME + // we need to put in the URL of the graph data the real module and action + 'module' => $currentControllerName, + 'action' => $currentControllerAction, + ); + } + + public function enableShowExportAsImageIcon() + { + $this->viewProperties['show_export_as_image_icon'] = true; + } + + public function addRowEvolutionSeriesToggle($initiallyShowAllMetrics) + { + $this->viewProperties['externalSeriesToggle'] = 'RowEvolutionSeriesToggle'; + $this->viewProperties['externalSeriesToggleShowAll'] = $initiallyShowAllMetrics; + } + + /** + * Sets parameters to modify in the future generated URL + * @param array $array array('nameParameter' => $newValue, ...) + */ + public function setParametersToModify($array) + { + $this->parametersToModify = array_merge($this->parametersToModify, $array); + } + + /** + * Show every x-axis tick instead of just every other one. + */ + public function showAllTicks() + { + $this->generateGraphDataParams['show_all_ticks'] = 1; + } + + /** + * Adds a row to the report containing totals for contained metrics. Mainly useful + * for evolution graphs where displaying the totals w/ the metrics is useful. + */ + public function addTotalRow() + { + $this->generateGraphDataParams['add_total_row'] = 1; + } + + /** + * 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. + * @return array + */ + protected function getJavascriptVariablesToSet() + { + $original = parent::getJavascriptVariablesToSet(); + $originalViewDataTable = $original['viewDataTable']; + + $result = $this->parametersToModify + $original; + ; + $result['viewDataTable'] = $originalViewDataTable; + + return $result; + } + + /** + * @see Piwik_ViewDataTable::main() + * @return null + */ + public function main() + { + if ($this->mainAlreadyExecuted) { + return; + } + $this->mainAlreadyExecuted = true; + + $this->view = $this->buildView(); + } + + protected function buildView() + { + // access control + $idSite = Piwik_Common::getRequestVar('idSite', 1, 'int'); + Piwik_API_Request::reloadAuthUsingTokenAuth(); + if (!Piwik::isUserHasViewAccess($idSite)) { + throw new Exception(Piwik_TranslateException('General_ExceptionPrivilegeAccessWebsite', array("'view'", $idSite))); + } + + // collect data + $this->parametersToModify['action'] = $this->currentControllerAction; + $this->parametersToModify = array_merge($this->variablesDefault, $this->parametersToModify); + $this->graphData = $this->getGraphData(); + + // build view + $view = new Piwik_View($this->dataTableTemplate); + + $view->width = $this->width; + $view->height = $this->height; + $view->chartDivId = $this->getUniqueIdViewDataTable() . "Chart"; + $view->graphType = $this->graphType; + + $view->data = $this->graphData; + $view->isDataAvailable = strpos($this->graphData, '"series":[]') === false; + + $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); + $view->properties = $this->getViewProperties(); + + $view->reportDocumentation = $this->getReportDocumentation(); + + // if it's likely that the report data for this data table has been purged, + // set whether we should display a message to that effect. + $view->showReportDataWasPurgedMessage = $this->hasReportBeenPurged(); + $view->deleteReportsOlderThan = Piwik_GetOption('delete_reports_older_than'); + + return $view; + } + + protected function getGraphData() + { + $saveGet = $_GET; + + $params = array_merge($this->generateGraphDataParams, $this->parametersToModify); + foreach ($params as $key => $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)) { + $val = implode(',', $val); + } + $_GET[$key] = $val; + } + $content = Piwik_FrontController::getInstance()->fetchDispatch($this->currentControllerName, $this->currentControllerAction, array()); + + $_GET = $saveGet; + + return str_replace(array("\r", "\n"), '', $content); + } } diff --git a/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php b/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php index 26cad88ea0..2a3f921405 100644 --- a/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php +++ b/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php @@ -18,189 +18,181 @@ class Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution extends Piwik_ViewDataTable_GenerateGraphHTML { - protected $height = 170; - protected $graphType = 'evolution'; - - /** - * The value of the date query parameter (or a default value) before it is turned - * into a date range. Set in 'calculateEvolutionDateRange' and used by - * 'getJavascriptVariablesToSet'. - * - * @var string - */ - private $originalDate; - - protected function getViewDataTableId() - { - return 'graphEvolution'; - } - - protected function getViewDataTableIdToLoad() - { - return 'generateDataChartEvolution'; - } - - function init($currentControllerName, - $currentControllerAction, - $apiMethodToRequestDataTable, - $controllerActionCalledWhenRequestSubTable = null) - { - parent::init($currentControllerName, - $currentControllerAction, - $apiMethodToRequestDataTable, - $controllerActionCalledWhenRequestSubTable); - - $this->calculateEvolutionDateRange(); - $this->disableShowAllViewsIcons(); - $this->disableShowTable(); - $this->disableShowAllColumns(); - $this->showAnnotationsView(); - } - - /** - * Makes sure 'date' parameter is not overridden. - */ - protected function getJavascriptVariablesToSet() - { - $result = parent::getJavascriptVariablesToSet(); - - // Graphs use a Range instead of the input date - we will use this same range for "Export" icons - $result['dateUsedInGraph'] = $result['date']; - - // Other datatable features may require the original input date (eg. the limit dropdown below evolution graph) - $result['date'] = $this->originalDate; - return $result; - } - - /** + protected $height = 170; + protected $graphType = 'evolution'; + + /** + * The value of the date query parameter (or a default value) before it is turned + * into a date range. Set in 'calculateEvolutionDateRange' and used by + * 'getJavascriptVariablesToSet'. + * + * @var string + */ + private $originalDate; + + protected function getViewDataTableId() + { + return 'graphEvolution'; + } + + protected function getViewDataTableIdToLoad() + { + return 'generateDataChartEvolution'; + } + + function init($currentControllerName, + $currentControllerAction, + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable = null) + { + parent::init($currentControllerName, + $currentControllerAction, + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable); + + $this->calculateEvolutionDateRange(); + $this->disableShowAllViewsIcons(); + $this->disableShowTable(); + $this->disableShowAllColumns(); + $this->showAnnotationsView(); + } + + /** + * Makes sure 'date' parameter is not overridden. + */ + protected function getJavascriptVariablesToSet() + { + $result = parent::getJavascriptVariablesToSet(); + + // Graphs use a Range instead of the input date - we will use this same range for "Export" icons + $result['dateUsedInGraph'] = $result['date']; + + // Other datatable features may require the original input date (eg. the limit dropdown below evolution graph) + $result['date'] = $this->originalDate; + return $result; + } + + /** * 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 - * @return null|string - */ - public function getUniqueIdViewDataTable() - { - $id = parent::getUniqueIdViewDataTable(); - if (!empty($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) - * - * @param array $columnsNames Array of column names eg. array('nb_visits','nb_hits') - */ - public function setColumnsToDisplay($columnsNames) - { - if (!is_array($columnsNames)) - { - if (strpos($columnsNames, ',') !== false) - { - // array values are comma separated - $columnsNames = explode(',', $columnsNames); - } - else - { - $columnsNames = array($columnsNames); - } - } - $this->setParametersToModify(array('columns' => $columnsNames)); - } - - /** - * Based on the period, date and evolution_{$period}_last_n query parameters, - * calculates the date range this evolution chart will display data for. - */ - private function calculateEvolutionDateRange() - { - $period = Piwik_Common::getRequestVar('period'); - - $defaultLastN = self::getDefaultLastN($period); - $this->originalDate = Piwik_Common::getRequestVar('date', 'last'.$defaultLastN, 'string'); - - if ($period != 'range') // show evolution limit if the period is not a range - { - $this->alwaysShowLimitDropdown(); - - // set the evolution_{$period}_last_n query param - if (Piwik_Period_Range::parseDateRange($this->originalDate)) // if a multiple period - { - // overwrite last_n param using the date range - $oPeriod = new Piwik_Period_Range($period, $this->originalDate); - $lastN = count($oPeriod->getSubperiods()); - } - else // if not a multiple period - { - list($newDate, $lastN) = self::getDateRangeAndLastN($period, $this->originalDate, $defaultLastN); - $this->setParametersToModify(array('date' => $newDate)); - } - $lastNParamName = self::getLastNParamName($period); - $this->setParametersToModify(array($lastNParamName => $lastN)); - } - } - - /** - * Returns the entire date range and lastN value for the current request, based on - * a period type and end date. - * - * @param string $period The period type, 'day', 'week', 'month' or 'year' - * @param string $endDate The end date. - * @param int|null $defaultLastN The default lastN to use. If null, the result of - * getDefaultLastN is used. - * @return array An array w/ two elements. The first is a whole date range and the second - * is the lastN number used, ie, array('2010-01-01,2012-01-02', 2). - */ - public static function getDateRangeAndLastN( $period, $endDate, $defaultLastN = null ) - { - if ($defaultLastN === null) - { - $defaultLastN = self::getDefaultLastN($period); - } - - $lastNParamName = self::getLastNParamName($period); - $lastN = Piwik_Common::getRequestVar($lastNParamName, $defaultLastN, 'int'); - - $site = new Piwik_Site(Piwik_Common::getRequestVar('idSite')); - - $dateRange = Piwik_Controller::getDateRangeRelativeToEndDate($period, 'last'.$lastN, $endDate, $site); - - return array($dateRange, $lastN); - } - - /** - * Returns the default last N number of dates to display for a given period. - * - * @param string $period 'day', 'week', 'month' or 'year' - * @return int - */ - public static function getDefaultLastN( $period ) - { - switch ($period) - { - case 'week': - return 26; - case 'month': - return 24; - case 'year': - return 5; - case 'day': - default: - return 30; - } - } - - /** - * Returns the query parameter that stores the lastN number of periods to get for - * the evolution graph. - * - * @param string $period The period type, 'day', 'week', 'month' or 'year'. - * @return string - */ - public static function getLastNParamName( $period ) - { - return "evolution_{$period}_last_n"; - } + * @return null|string + */ + public function getUniqueIdViewDataTable() + { + $id = parent::getUniqueIdViewDataTable(); + if (!empty($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) + * + * @param array $columnsNames Array of column names eg. array('nb_visits','nb_hits') + */ + public function setColumnsToDisplay($columnsNames) + { + if (!is_array($columnsNames)) { + if (strpos($columnsNames, ',') !== false) { + // array values are comma separated + $columnsNames = explode(',', $columnsNames); + } else { + $columnsNames = array($columnsNames); + } + } + $this->setParametersToModify(array('columns' => $columnsNames)); + } + + /** + * Based on the period, date and evolution_{$period}_last_n query parameters, + * calculates the date range this evolution chart will display data for. + */ + private function calculateEvolutionDateRange() + { + $period = Piwik_Common::getRequestVar('period'); + + $defaultLastN = self::getDefaultLastN($period); + $this->originalDate = Piwik_Common::getRequestVar('date', 'last' . $defaultLastN, 'string'); + + if ($period != 'range') // show evolution limit if the period is not a range + { + $this->alwaysShowLimitDropdown(); + + // set the evolution_{$period}_last_n query param + if (Piwik_Period_Range::parseDateRange($this->originalDate)) // if a multiple period + { + // overwrite last_n param using the date range + $oPeriod = new Piwik_Period_Range($period, $this->originalDate); + $lastN = count($oPeriod->getSubperiods()); + } else // if not a multiple period + { + list($newDate, $lastN) = self::getDateRangeAndLastN($period, $this->originalDate, $defaultLastN); + $this->setParametersToModify(array('date' => $newDate)); + } + $lastNParamName = self::getLastNParamName($period); + $this->setParametersToModify(array($lastNParamName => $lastN)); + } + } + + /** + * Returns the entire date range and lastN value for the current request, based on + * a period type and end date. + * + * @param string $period The period type, 'day', 'week', 'month' or 'year' + * @param string $endDate The end date. + * @param int|null $defaultLastN The default lastN to use. If null, the result of + * getDefaultLastN is used. + * @return array An array w/ two elements. The first is a whole date range and the second + * is the lastN number used, ie, array('2010-01-01,2012-01-02', 2). + */ + public static function getDateRangeAndLastN($period, $endDate, $defaultLastN = null) + { + if ($defaultLastN === null) { + $defaultLastN = self::getDefaultLastN($period); + } + + $lastNParamName = self::getLastNParamName($period); + $lastN = Piwik_Common::getRequestVar($lastNParamName, $defaultLastN, 'int'); + + $site = new Piwik_Site(Piwik_Common::getRequestVar('idSite')); + + $dateRange = Piwik_Controller::getDateRangeRelativeToEndDate($period, 'last' . $lastN, $endDate, $site); + + return array($dateRange, $lastN); + } + + /** + * Returns the default last N number of dates to display for a given period. + * + * @param string $period 'day', 'week', 'month' or 'year' + * @return int + */ + public static function getDefaultLastN($period) + { + switch ($period) { + case 'week': + return 26; + case 'month': + return 24; + case 'year': + return 5; + case 'day': + default: + return 30; + } + } + + /** + * Returns the query parameter that stores the lastN number of periods to get for + * the evolution graph. + * + * @param string $period The period type, 'day', 'week', 'month' or 'year'. + * @return string + */ + public static function getLastNParamName($period) + { + return "evolution_{$period}_last_n"; + } } diff --git a/core/ViewDataTable/GenerateGraphHTML/ChartPie.php b/core/ViewDataTable/GenerateGraphHTML/ChartPie.php index 7a847050c4..ccc40d0c3f 100644 --- a/core/ViewDataTable/GenerateGraphHTML/ChartPie.php +++ b/core/ViewDataTable/GenerateGraphHTML/ChartPie.php @@ -18,16 +18,16 @@ class Piwik_ViewDataTable_GenerateGraphHTML_ChartPie extends Piwik_ViewDataTable_GenerateGraphHTML { - - protected $graphType = 'pie'; - - protected function getViewDataTableId() - { - return 'graphPie'; - } - - protected function getViewDataTableIdToLoad() - { - return 'generateDataChartPie'; - } + + protected $graphType = 'pie'; + + protected function getViewDataTableId() + { + return 'graphPie'; + } + + protected function getViewDataTableIdToLoad() + { + return 'generateDataChartPie'; + } } diff --git a/core/ViewDataTable/GenerateGraphHTML/ChartVerticalBar.php b/core/ViewDataTable/GenerateGraphHTML/ChartVerticalBar.php index fd629d238d..3ee015f3e5 100644 --- a/core/ViewDataTable/GenerateGraphHTML/ChartVerticalBar.php +++ b/core/ViewDataTable/GenerateGraphHTML/ChartVerticalBar.php @@ -19,16 +19,16 @@ class Piwik_ViewDataTable_GenerateGraphHTML_ChartVerticalBar extends Piwik_ViewDataTable_GenerateGraphHTML { - - protected $graphType = 'bar'; - - protected function getViewDataTableId() - { - return 'graphVerticalBar'; - } - - protected function getViewDataTableIdToLoad() - { - return 'generateDataChartVerticalBar'; - } + + protected $graphType = 'bar'; + + protected function getViewDataTableId() + { + return 'graphVerticalBar'; + } + + protected function getViewDataTableIdToLoad() + { + return 'generateDataChartVerticalBar'; + } } diff --git a/core/ViewDataTable/HtmlTable.php b/core/ViewDataTable/HtmlTable.php index 962d2dec07..a371230b86 100644 --- a/core/ViewDataTable/HtmlTable.php +++ b/core/ViewDataTable/HtmlTable.php @@ -19,221 +19,212 @@ */ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable { - /** - * Set to true when the DataTable must be loaded along with all its children subtables - * Useful when searching for a pattern in the DataTable Actions (we display the full hierarchy) - * - * @var bool - */ - protected $recursiveDataTableLoad = false; - - /** - * PHP array conversion of the Piwik_DataTable - * - * @var array - */ - public $arrayDataTable; // phpArray - - /** - * @see Piwik_ViewDataTable::init() - * @param string $currentControllerName - * @param string $currentControllerAction - * @param string $apiMethodToRequestDataTable - * @param null|string $controllerActionCalledWhenRequestSubTable - */ - function init($currentControllerName, - $currentControllerAction, - $apiMethodToRequestDataTable, - $controllerActionCalledWhenRequestSubTable = null) - { - parent::init($currentControllerName, - $currentControllerAction, - $apiMethodToRequestDataTable, - $controllerActionCalledWhenRequestSubTable); - $this->dataTableTemplate = 'CoreHome/templates/datatable.tpl'; - $this->variablesDefault['enable_sort'] = '1'; - $this->setSortedColumn('nb_visits', 'desc'); - $this->setLimit(Piwik_Config::getInstance()->General['datatable_default_limit']); - $this->handleLowPopulation(); - } - - protected function getViewDataTableId() - { - return 'table'; - } - - /** - * @see Piwik_ViewDataTable::main() - * @throws Exception|Piwik_Access_NoAccessException - * @return null - */ - public function main() - { - if($this->mainAlreadyExecuted) - { - return; - } - $this->mainAlreadyExecuted = true; - - $this->isDataAvailable = true; - try { - $this->loadDataTableFromAPI(); - } catch(Piwik_Access_NoAccessException $e) { - throw $e; - } catch(Exception $e) { - Piwik::log("Failed to get data from API: ".$e->getMessage()); - - $this->isDataAvailable = false; - } - - $this->postDataTableLoadedFromAPI(); - $this->view = $this->buildView(); - } - - /** - * @return Piwik_View with all data set - */ - protected function buildView() - { - $view = new Piwik_View($this->dataTableTemplate); - - if(!$this->isDataAvailable) - { - $view->arrayDataTable = array(); - } - else - { - $columns = $this->getColumnsToDisplay(); - $columnTranslations = $columnDocumentation = array(); - foreach($columns as $columnName) - { - $columnTranslations[$columnName] = $this->getColumnTranslation($columnName); - $columnDocumentation[$columnName] = $this->getMetricDocumentation($columnName); - } - $nbColumns = count($columns); - // case no data in the array we use the number of columns set to be displayed - if($nbColumns == 0) - { - $nbColumns = count($this->columnsToDisplay); - } - - $view->arrayDataTable = $this->getPHPArrayFromDataTable(); - $view->dataTableColumns = $columns; - $view->reportDocumentation = $this->getReportDocumentation(); - $view->columnTranslations = $columnTranslations; - $view->columnDocumentation = $columnDocumentation; - $view->nbColumns = $nbColumns; - $view->defaultWhenColumnValueNotDefined = '-'; - - // if it's likely that the report data for this data table has been purged, - // set whether we should display a message to that effect. - $view->showReportDataWasPurgedMessage = $this->hasReportBeenPurged(); - $view->deleteReportsOlderThan = Piwik_GetOption('delete_reports_older_than'); - } - $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); - $view->properties = $this->getViewProperties(); - return $view; - } - - protected function handleLowPopulation( $columnToApplyFilter = null) - { - if(Piwik_Common::getRequestVar('enable_filter_excludelowpop', '0', 'string' ) == '0') - { - return; - } - if(is_null($columnToApplyFilter)) - { - $columnToApplyFilter = 'nb_visits'; - } - $this->setExcludeLowPopulation( $columnToApplyFilter); - } - - /** - * Returns friendly php array from the Piwik_DataTable - * @see Piwik_DataTable_Renderer_Php - * @return array - */ - protected function getPHPArrayFromDataTable() - { - $renderer = Piwik_DataTable_Renderer::factory('php'); - $renderer->setTable($this->dataTable); - $renderer->setSerialize( false ); - // we get the php array from the datatable but conserving the original datatable format, - // ie. rows 'columns', 'metadata' and 'idsubdatatable' - $phpArray = $renderer->originalRender(); - return $phpArray; - } - - - /** - * Adds a column to the list of columns to be displayed - * - * @param string $columnName - */ - public function addColumnToDisplay( $columnName ) - { - $this->columnsToDisplay[] = $columnName; - } - - /** - * Sets the search on a table to be recursive (also searches in subtables) - * Works only on Actions/Downloads/Outlinks tables. - * - * @return bool If the pattern for a recursive search was set in the URL - */ - public function setSearchRecursive() - { - $this->variablesDefault['search_recursive'] = true; - return $this->setRecursiveLoadDataTableIfSearchingForPattern(); - } - - protected function getRequestString() - { - $requestString = parent::getRequestString(); - if ($this->recursiveDataTableLoad - && !Piwik_Common::getRequestVar('flat', false)) - { - $requestString .= '&expanded=1'; - } - return $requestString; - } - - /** - * Set the flag to load the datatable recursively so we can search on subtables as well - * - * @return bool if recursive search is enabled - */ - protected function setRecursiveLoadDataTableIfSearchingForPattern() - { - try{ - $requestValue = Piwik_Common::getRequestVar('filter_column_recursive'); - $requestValue = Piwik_Common::getRequestVar('filter_pattern_recursive'); - // if the 2 variables are set we are searching for something. - // we have to load all the children subtables in this case - - $this->recursiveDataTableLoad = true; - return true; - } - catch(Exception $e) { - $this->recursiveDataTableLoad = false; - return false; - } - } - - /** - * Disable the row evolution feature which is enabled by default - */ - public function disableRowEvolution() - { - $this->variablesDefault['disable_row_evolution'] = true; - } - - /** - * Disables row actions. - */ - public function disableRowActions() - { - $this->variablesDefault['disable_row_actions'] = true; - } - + /** + * Set to true when the DataTable must be loaded along with all its children subtables + * Useful when searching for a pattern in the DataTable Actions (we display the full hierarchy) + * + * @var bool + */ + protected $recursiveDataTableLoad = false; + + /** + * PHP array conversion of the Piwik_DataTable + * + * @var array + */ + public $arrayDataTable; // phpArray + + /** + * @see Piwik_ViewDataTable::init() + * @param string $currentControllerName + * @param string $currentControllerAction + * @param string $apiMethodToRequestDataTable + * @param null|string $controllerActionCalledWhenRequestSubTable + */ + function init($currentControllerName, + $currentControllerAction, + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable = null) + { + parent::init($currentControllerName, + $currentControllerAction, + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable); + $this->dataTableTemplate = 'CoreHome/templates/datatable.tpl'; + $this->variablesDefault['enable_sort'] = '1'; + $this->setSortedColumn('nb_visits', 'desc'); + $this->setLimit(Piwik_Config::getInstance()->General['datatable_default_limit']); + $this->handleLowPopulation(); + } + + protected function getViewDataTableId() + { + return 'table'; + } + + /** + * @see Piwik_ViewDataTable::main() + * @throws Exception|Piwik_Access_NoAccessException + * @return null + */ + public function main() + { + if ($this->mainAlreadyExecuted) { + return; + } + $this->mainAlreadyExecuted = true; + + $this->isDataAvailable = true; + try { + $this->loadDataTableFromAPI(); + } catch (Piwik_Access_NoAccessException $e) { + throw $e; + } catch (Exception $e) { + Piwik::log("Failed to get data from API: " . $e->getMessage()); + + $this->isDataAvailable = false; + } + + $this->postDataTableLoadedFromAPI(); + $this->view = $this->buildView(); + } + + /** + * @return Piwik_View with all data set + */ + protected function buildView() + { + $view = new Piwik_View($this->dataTableTemplate); + + if (!$this->isDataAvailable) { + $view->arrayDataTable = array(); + } else { + $columns = $this->getColumnsToDisplay(); + $columnTranslations = $columnDocumentation = array(); + foreach ($columns as $columnName) { + $columnTranslations[$columnName] = $this->getColumnTranslation($columnName); + $columnDocumentation[$columnName] = $this->getMetricDocumentation($columnName); + } + $nbColumns = count($columns); + // case no data in the array we use the number of columns set to be displayed + if ($nbColumns == 0) { + $nbColumns = count($this->columnsToDisplay); + } + + $view->arrayDataTable = $this->getPHPArrayFromDataTable(); + $view->dataTableColumns = $columns; + $view->reportDocumentation = $this->getReportDocumentation(); + $view->columnTranslations = $columnTranslations; + $view->columnDocumentation = $columnDocumentation; + $view->nbColumns = $nbColumns; + $view->defaultWhenColumnValueNotDefined = '-'; + + // if it's likely that the report data for this data table has been purged, + // set whether we should display a message to that effect. + $view->showReportDataWasPurgedMessage = $this->hasReportBeenPurged(); + $view->deleteReportsOlderThan = Piwik_GetOption('delete_reports_older_than'); + } + $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); + $view->properties = $this->getViewProperties(); + return $view; + } + + protected function handleLowPopulation($columnToApplyFilter = null) + { + if (Piwik_Common::getRequestVar('enable_filter_excludelowpop', '0', 'string') == '0') { + return; + } + if (is_null($columnToApplyFilter)) { + $columnToApplyFilter = 'nb_visits'; + } + $this->setExcludeLowPopulation($columnToApplyFilter); + } + + /** + * Returns friendly php array from the Piwik_DataTable + * @see Piwik_DataTable_Renderer_Php + * @return array + */ + protected function getPHPArrayFromDataTable() + { + $renderer = Piwik_DataTable_Renderer::factory('php'); + $renderer->setTable($this->dataTable); + $renderer->setSerialize(false); + // we get the php array from the datatable but conserving the original datatable format, + // ie. rows 'columns', 'metadata' and 'idsubdatatable' + $phpArray = $renderer->originalRender(); + return $phpArray; + } + + + /** + * Adds a column to the list of columns to be displayed + * + * @param string $columnName + */ + public function addColumnToDisplay($columnName) + { + $this->columnsToDisplay[] = $columnName; + } + + /** + * Sets the search on a table to be recursive (also searches in subtables) + * Works only on Actions/Downloads/Outlinks tables. + * + * @return bool If the pattern for a recursive search was set in the URL + */ + public function setSearchRecursive() + { + $this->variablesDefault['search_recursive'] = true; + return $this->setRecursiveLoadDataTableIfSearchingForPattern(); + } + + protected function getRequestString() + { + $requestString = parent::getRequestString(); + if ($this->recursiveDataTableLoad + && !Piwik_Common::getRequestVar('flat', false) + ) { + $requestString .= '&expanded=1'; + } + return $requestString; + } + + /** + * Set the flag to load the datatable recursively so we can search on subtables as well + * + * @return bool if recursive search is enabled + */ + protected function setRecursiveLoadDataTableIfSearchingForPattern() + { + try { + $requestValue = Piwik_Common::getRequestVar('filter_column_recursive'); + $requestValue = Piwik_Common::getRequestVar('filter_pattern_recursive'); + // if the 2 variables are set we are searching for something. + // we have to load all the children subtables in this case + + $this->recursiveDataTableLoad = true; + return true; + } catch (Exception $e) { + $this->recursiveDataTableLoad = false; + return false; + } + } + + /** + * Disable the row evolution feature which is enabled by default + */ + public function disableRowEvolution() + { + $this->variablesDefault['disable_row_evolution'] = true; + } + + /** + * Disables row actions. + */ + public function disableRowActions() + { + $this->variablesDefault['disable_row_actions'] = true; + } + } diff --git a/core/ViewDataTable/HtmlTable/AllColumns.php b/core/ViewDataTable/HtmlTable/AllColumns.php index 10802b052a..dcd754de49 100644 --- a/core/ViewDataTable/HtmlTable/AllColumns.php +++ b/core/ViewDataTable/HtmlTable/AllColumns.php @@ -1,10 +1,10 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ @@ -13,55 +13,53 @@ * @package Piwik * @subpackage Piwik_ViewDataTable */ -class Piwik_ViewDataTable_HtmlTable_AllColumns extends Piwik_ViewDataTable_HtmlTable +class Piwik_ViewDataTable_HtmlTable_AllColumns extends Piwik_ViewDataTable_HtmlTable { - protected function getViewDataTableId() - { - return 'tableAllColumns'; - } - - public function main() - { - $this->viewProperties['show_exclude_low_population'] = true; - parent::main(); - } - - protected function getRequestString() - { - $requestString = parent::getRequestString(); - return $requestString . '&filter_add_columns_when_show_all_columns=1'; - } - - protected function postDataTableLoadedFromAPI() - { - $valid = parent::postDataTableLoadedFromAPI(); - if(!$valid) return false; - - Piwik_Controller::setPeriodVariablesView($this); - $columnUniqueVisitors = false; - if($this->period == 'day') - { - $columnUniqueVisitors = 'nb_uniq_visitors'; - } - - // only display conversion rate for the plugins that do not provide "per goal" metrics - // otherwise, conversion rate is meaningless as a whole (since we don't process 'cross goals' conversions) - $columnConversionRate = false; - if(empty($this->viewProperties['show_goals'])) - { - $columnConversionRate = 'conversion_rate'; - } - $this->setColumnsToDisplay(array('label', - 'nb_visits', - $columnUniqueVisitors, - 'nb_actions', - 'nb_actions_per_visit', - 'avg_time_on_site', - 'bounce_rate', - $columnConversionRate - )); - $this->dataTable->filter('ColumnCallbackReplace', array('avg_time_on_site', create_function('$averageTimeOnSite', 'return Piwik::getPrettyTimeFromSeconds($averageTimeOnSite);'))); - - return true; - } + protected function getViewDataTableId() + { + return 'tableAllColumns'; + } + + public function main() + { + $this->viewProperties['show_exclude_low_population'] = true; + parent::main(); + } + + protected function getRequestString() + { + $requestString = parent::getRequestString(); + return $requestString . '&filter_add_columns_when_show_all_columns=1'; + } + + protected function postDataTableLoadedFromAPI() + { + $valid = parent::postDataTableLoadedFromAPI(); + if (!$valid) return false; + + Piwik_Controller::setPeriodVariablesView($this); + $columnUniqueVisitors = false; + if ($this->period == 'day') { + $columnUniqueVisitors = 'nb_uniq_visitors'; + } + + // only display conversion rate for the plugins that do not provide "per goal" metrics + // otherwise, conversion rate is meaningless as a whole (since we don't process 'cross goals' conversions) + $columnConversionRate = false; + if (empty($this->viewProperties['show_goals'])) { + $columnConversionRate = 'conversion_rate'; + } + $this->setColumnsToDisplay(array('label', + 'nb_visits', + $columnUniqueVisitors, + 'nb_actions', + 'nb_actions_per_visit', + 'avg_time_on_site', + 'bounce_rate', + $columnConversionRate + )); + $this->dataTable->filter('ColumnCallbackReplace', array('avg_time_on_site', create_function('$averageTimeOnSite', 'return Piwik::getPrettyTimeFromSeconds($averageTimeOnSite);'))); + + return true; + } } diff --git a/core/ViewDataTable/HtmlTable/Goals.php b/core/ViewDataTable/HtmlTable/Goals.php index 1fce3b19a8..a2ff04a12f 100644 --- a/core/ViewDataTable/HtmlTable/Goals.php +++ b/core/ViewDataTable/HtmlTable/Goals.php @@ -15,270 +15,246 @@ */ class Piwik_ViewDataTable_HtmlTable_Goals extends Piwik_ViewDataTable_HtmlTable { - protected function getViewDataTableId() - { - return 'tableGoals'; - } - - public function main() - { - $this->idSite = Piwik_Common::getRequestVar('idSite', null, 'int'); - $this->processOnlyIdGoal = Piwik_Common::getRequestVar('idGoal', Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW, 'string'); - $this->isEcommerce = $this->processOnlyIdGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER; - $this->viewProperties['show_exclude_low_population'] = true; - $this->viewProperties['show_goals'] = true; - - if (Piwik_Common::getRequestVar('documentationForGoalsPage', 0, 'int') == 1) { - $this->setReportDocumentation(Piwik_Translate('Goals_ConversionByTypeReportDocumentation', - array('<br />', '<br />', '<a href="http://piwik.org/docs/tracking-goals-web-analytics/" target="_blank">', '</a>'))); - } - - - $this->setMetricDocumentation('nb_visits', Piwik_Translate('Goals_ColumnVisits')); - if($this->isEcommerce) - { - $this->setMetricDocumentation('revenue_per_visit', Piwik_Translate('Goals_ColumnRevenuePerVisitDocumentation', Piwik_Translate('General_EcommerceOrders') )); - $this->setColumnsTranslations( array( - 'goal_%s_conversion_rate' => Piwik_Translate('Goals_ConversionRate'), - 'goal_%s_nb_conversions' => Piwik_Translate('General_EcommerceOrders'), - 'goal_%s_revenue' => Piwik_Translate('General_TotalRevenue'), - 'goal_%s_revenue_per_visit' => Piwik_Translate('General_ColumnValuePerVisit'), - 'goal_%s_avg_order_revenue' => Piwik_Translate('General_AverageOrderValue'), - 'goal_%s_items' => Piwik_Translate('General_PurchasedProducts'), - )); - $this->setColumnsToDisplay( array( - 'label', - 'nb_visits', - 'goal_%s_nb_conversions', - 'goal_%s_revenue', - 'goal_%s_conversion_rate', - 'goal_%s_avg_order_revenue', - 'goal_%s_items', - 'goal_%s_revenue_per_visit', - )); - - // Default sort column - $this->setSortedColumn('goal_ecommerceOrder_revenue', 'desc'); - } - else - { - $this->setMetricDocumentation('revenue_per_visit', Piwik_Translate('Goals_ColumnRevenuePerVisitDocumentation', Piwik_Translate('Goals_EcommerceAndGoalsMenu') )); - $this->setColumnsTranslations( array( - 'goal_%s_conversion_rate' => Piwik_Translate('Goals_ConversionRate'), - 'goal_%s_nb_conversions' => Piwik_Translate('Goals_Conversions'), - 'goal_%s_revenue' => '%s ' . Piwik_Translate('Goals_ColumnRevenue'), - 'goal_%s_revenue_per_visit' => '%s ' . Piwik_Translate('General_ColumnValuePerVisit'), - - 'nb_conversions' => Piwik_Translate('Goals_ColumnConversions'), - 'conversion_rate' => Piwik_Translate('General_ColumnConversionRate'), - 'revenue' => Piwik_Translate('Goals_ColumnRevenue'), - 'revenue_per_visit' => Piwik_Translate('General_ColumnValuePerVisit'), - )); - $this->setColumnsToDisplay( array( - 'label', - 'nb_visits', - 'goal_%s_nb_conversions', - 'goal_%s_conversion_rate', - 'goal_%s_revenue', - 'goal_%s_revenue_per_visit', - 'revenue_per_visit', - )); - - // Default sort column - $columnsToDisplay = $this->getColumnsToDisplay(); - $columnNbConversionsCurrentGoal = $columnsToDisplay[2]; - if($this->processOnlyIdGoal > 0 - && strpos($columnNbConversionsCurrentGoal, '_nb_conversions') !== false) - { - $this->setSortedColumn($columnNbConversionsCurrentGoal, 'desc'); - } - } - - parent::main(); - } - - public function disableSubTableWhenShowGoals() - { - $this->controllerActionCalledWhenRequestSubTable = null; - } - - public function setColumnsToDisplay($columnsNames) - { - $newColumnsNames = array(); - $goals = array(); - $idSite = $this->getIdSite(); - if($idSite) - { - $goals = Piwik_Goals_API::getInstance()->getGoals( $idSite ); - - $ecommerceGoal = array( - 'idgoal' => Piwik_Archive::LABEL_ECOMMERCE_ORDER, - 'name' => Piwik_Translate('Goals_EcommerceOrder') - ); - - $site = new Piwik_Site($idSite); - //Case Ecommerce report table - if($this->isEcommerce) - { - $goals = array($ecommerceGoal); - } - // Case tableGoals - elseif($site->isEcommerceEnabled()) - { - $goals = array_merge( - array($ecommerceGoal), - $goals - ); - } - } - foreach($columnsNames as $columnName) - { - if(in_array($columnName, array( - 'goal_%s_conversion_rate', - 'goal_%s_nb_conversions', - 'goal_%s_revenue_per_visit', - 'goal_%s_revenue', - 'goal_%s_avg_order_revenue', - 'goal_%s_items', - - ))) - { - foreach($goals as $goal) - { - $idgoal = $goal['idgoal']; + protected function getViewDataTableId() + { + return 'tableGoals'; + } - // Columns names are escaped in smarty via | escape:'html' - $goal['name'] = Piwik_Common::unsanitizeInputValue($goal['name']); + public function main() + { + $this->idSite = Piwik_Common::getRequestVar('idSite', null, 'int'); + $this->processOnlyIdGoal = Piwik_Common::getRequestVar('idGoal', Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW, 'string'); + $this->isEcommerce = $this->processOnlyIdGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER; + $this->viewProperties['show_exclude_low_population'] = true; + $this->viewProperties['show_goals'] = true; - if($this->processOnlyIdGoal > Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_FULL_TABLE - && $this->processOnlyIdGoal != $idgoal - && !$this->isEcommerce) - { - continue; - } - $name = Piwik_Translate($this->getColumnTranslation($columnName), $goal['name']); - $columnNameGoal = str_replace('%s', $idgoal, $columnName); - $this->setColumnTranslation($columnNameGoal, $name); - $this->setDynamicMetricDocumentation($columnName, $columnNameGoal, $goal['name'], $goal['idgoal']); - if(strpos($columnNameGoal, '_rate') === false - // For the goal table (when the flag icon is clicked), we only display the per Goal Conversion rate - && $this->processOnlyIdGoal == Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW) - { - continue; - } + if (Piwik_Common::getRequestVar('documentationForGoalsPage', 0, 'int') == 1) { + $this->setReportDocumentation(Piwik_Translate('Goals_ConversionByTypeReportDocumentation', + array('<br />', '<br />', '<a href="http://piwik.org/docs/tracking-goals-web-analytics/" target="_blank">', '</a>'))); + } - if(strstr($columnNameGoal, '_revenue') !== false) - { - $this->columnsToRevenueFilter[] = $columnNameGoal; - } - else - { - $this->columnsToConversionFilter[] = $columnNameGoal; - } - $newColumnsNames[] = $columnNameGoal; - } - } - else - { - $newColumnsNames[] = $columnName; - } - } - parent::setColumnsToDisplay($newColumnsNames); - } - /** - * Find the appropriate metric documentation for a goal column - * @param string $genericMetricName - * @param string $metricName - * @param string $goalName - * @param int $idGoal - */ - private function setDynamicMetricDocumentation($genericMetricName, $metricName, $goalName, $idGoal) - { - if($idGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER) - { - $goalName = Piwik_Translate('General_EcommerceOrders'); - } - else - { - $goalName = '"'.$goalName.'"'; - } - - $langString = false; - switch ($genericMetricName) - { - case 'goal_%s_nb_conversions': - $langString = 'Goals_ColumnConversionsDocumentation'; - break; - case 'goal_%s_conversion_rate': - $langString = 'Goals_ColumnConversionRateDocumentation'; - break; - case 'goal_%s_revenue_per_visit': - $langString = 'Goals_ColumnRevenuePerVisitDocumentation'; - break; - case 'goal_%s_revenue': - $langString = 'Goals_ColumnRevenueDocumentation'; - break; - case 'goal_%s_avg_order_revenue': - $langString = 'Goals_ColumnAverageOrderRevenueDocumentation'; - break; - case 'goal_%s_items': - $langString = 'Goals_ColumnPurchasedProductsDocumentation'; - break; - } - - if ($langString) - { - $doc = Piwik_Translate($langString, $goalName); - $this->setMetricDocumentation($metricName, $doc); - } - } - - protected function getRequestString() - { - $requestString = parent::getRequestString(); - if($this->processOnlyIdGoal > Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_FULL_TABLE - || $this->isEcommerce - ) - { - $requestString .= "&idGoal=".$this->processOnlyIdGoal; - } - return $requestString . '&filter_update_columns_when_show_all_goals=1'; - } - - protected $columnsToRevenueFilter = array(); - protected $columnsToConversionFilter = array(); - protected $idSite = false; - - private function getIdSite() - { - return $this->idSite; - } - - protected function postDataTableLoadedFromAPI() - { - $valid = parent::postDataTableLoadedFromAPI(); - if($valid === false) return false; - - foreach($this->getColumnsToDisplay() as $columnName) - { - if(strpos($columnName, 'conversion_rate')) - { - $this->dataTable->filter('ColumnCallbackReplace', array($columnName, create_function('$rate', 'if($rate==0) return "0%"; else return $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;'))); - } - return true; - } + $this->setMetricDocumentation('nb_visits', Piwik_Translate('Goals_ColumnVisits')); + if ($this->isEcommerce) { + $this->setMetricDocumentation('revenue_per_visit', Piwik_Translate('Goals_ColumnRevenuePerVisitDocumentation', Piwik_Translate('General_EcommerceOrders'))); + $this->setColumnsTranslations(array( + 'goal_%s_conversion_rate' => Piwik_Translate('Goals_ConversionRate'), + 'goal_%s_nb_conversions' => Piwik_Translate('General_EcommerceOrders'), + 'goal_%s_revenue' => Piwik_Translate('General_TotalRevenue'), + 'goal_%s_revenue_per_visit' => Piwik_Translate('General_ColumnValuePerVisit'), + 'goal_%s_avg_order_revenue' => Piwik_Translate('General_AverageOrderValue'), + 'goal_%s_items' => Piwik_Translate('General_PurchasedProducts'), + )); + $this->setColumnsToDisplay(array( + 'label', + 'nb_visits', + 'goal_%s_nb_conversions', + 'goal_%s_revenue', + 'goal_%s_conversion_rate', + 'goal_%s_avg_order_revenue', + 'goal_%s_items', + 'goal_%s_revenue_per_visit', + )); + + // Default sort column + $this->setSortedColumn('goal_ecommerceOrder_revenue', 'desc'); + } else { + $this->setMetricDocumentation('revenue_per_visit', Piwik_Translate('Goals_ColumnRevenuePerVisitDocumentation', Piwik_Translate('Goals_EcommerceAndGoalsMenu'))); + $this->setColumnsTranslations(array( + 'goal_%s_conversion_rate' => Piwik_Translate('Goals_ConversionRate'), + 'goal_%s_nb_conversions' => Piwik_Translate('Goals_Conversions'), + 'goal_%s_revenue' => '%s ' . Piwik_Translate('Goals_ColumnRevenue'), + 'goal_%s_revenue_per_visit' => '%s ' . Piwik_Translate('General_ColumnValuePerVisit'), + + 'nb_conversions' => Piwik_Translate('Goals_ColumnConversions'), + 'conversion_rate' => Piwik_Translate('General_ColumnConversionRate'), + 'revenue' => Piwik_Translate('Goals_ColumnRevenue'), + 'revenue_per_visit' => Piwik_Translate('General_ColumnValuePerVisit'), + )); + $this->setColumnsToDisplay(array( + 'label', + 'nb_visits', + 'goal_%s_nb_conversions', + 'goal_%s_conversion_rate', + 'goal_%s_revenue', + 'goal_%s_revenue_per_visit', + 'revenue_per_visit', + )); + + // Default sort column + $columnsToDisplay = $this->getColumnsToDisplay(); + $columnNbConversionsCurrentGoal = $columnsToDisplay[2]; + if ($this->processOnlyIdGoal > 0 + && strpos($columnNbConversionsCurrentGoal, '_nb_conversions') !== false + ) { + $this->setSortedColumn($columnNbConversionsCurrentGoal, 'desc'); + } + } + + parent::main(); + } + + public function disableSubTableWhenShowGoals() + { + $this->controllerActionCalledWhenRequestSubTable = null; + } + + public function setColumnsToDisplay($columnsNames) + { + $newColumnsNames = array(); + $goals = array(); + $idSite = $this->getIdSite(); + if ($idSite) { + $goals = Piwik_Goals_API::getInstance()->getGoals($idSite); + + $ecommerceGoal = array( + 'idgoal' => Piwik_Archive::LABEL_ECOMMERCE_ORDER, + 'name' => Piwik_Translate('Goals_EcommerceOrder') + ); + + $site = new Piwik_Site($idSite); + //Case Ecommerce report table + if ($this->isEcommerce) { + $goals = array($ecommerceGoal); + } // Case tableGoals + elseif ($site->isEcommerceEnabled()) { + $goals = array_merge( + array($ecommerceGoal), + $goals + ); + } + } + foreach ($columnsNames as $columnName) { + if (in_array($columnName, array( + 'goal_%s_conversion_rate', + 'goal_%s_nb_conversions', + 'goal_%s_revenue_per_visit', + 'goal_%s_revenue', + 'goal_%s_avg_order_revenue', + 'goal_%s_items', + + )) + ) { + foreach ($goals as $goal) { + $idgoal = $goal['idgoal']; + + // Columns names are escaped in smarty via | escape:'html' + $goal['name'] = Piwik_Common::unsanitizeInputValue($goal['name']); + + if ($this->processOnlyIdGoal > Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_FULL_TABLE + && $this->processOnlyIdGoal != $idgoal + && !$this->isEcommerce + ) { + continue; + } + $name = Piwik_Translate($this->getColumnTranslation($columnName), $goal['name']); + $columnNameGoal = str_replace('%s', $idgoal, $columnName); + $this->setColumnTranslation($columnNameGoal, $name); + $this->setDynamicMetricDocumentation($columnName, $columnNameGoal, $goal['name'], $goal['idgoal']); + if (strpos($columnNameGoal, '_rate') === false + // For the goal table (when the flag icon is clicked), we only display the per Goal Conversion rate + && $this->processOnlyIdGoal == Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_OVERVIEW + ) { + continue; + } + + if (strstr($columnNameGoal, '_revenue') !== false) { + $this->columnsToRevenueFilter[] = $columnNameGoal; + } else { + $this->columnsToConversionFilter[] = $columnNameGoal; + } + $newColumnsNames[] = $columnNameGoal; + } + } else { + $newColumnsNames[] = $columnName; + } + } + parent::setColumnsToDisplay($newColumnsNames); + } + + /** + * Find the appropriate metric documentation for a goal column + * @param string $genericMetricName + * @param string $metricName + * @param string $goalName + * @param int $idGoal + */ + private function setDynamicMetricDocumentation($genericMetricName, $metricName, $goalName, $idGoal) + { + if ($idGoal == Piwik_Archive::LABEL_ECOMMERCE_ORDER) { + $goalName = Piwik_Translate('General_EcommerceOrders'); + } else { + $goalName = '"' . $goalName . '"'; + } + + $langString = false; + switch ($genericMetricName) { + case 'goal_%s_nb_conversions': + $langString = 'Goals_ColumnConversionsDocumentation'; + break; + case 'goal_%s_conversion_rate': + $langString = 'Goals_ColumnConversionRateDocumentation'; + break; + case 'goal_%s_revenue_per_visit': + $langString = 'Goals_ColumnRevenuePerVisitDocumentation'; + break; + case 'goal_%s_revenue': + $langString = 'Goals_ColumnRevenueDocumentation'; + break; + case 'goal_%s_avg_order_revenue': + $langString = 'Goals_ColumnAverageOrderRevenueDocumentation'; + break; + case 'goal_%s_items': + $langString = 'Goals_ColumnPurchasedProductsDocumentation'; + break; + } + + if ($langString) { + $doc = Piwik_Translate($langString, $goalName); + $this->setMetricDocumentation($metricName, $doc); + } + } + + protected function getRequestString() + { + $requestString = parent::getRequestString(); + if ($this->processOnlyIdGoal > Piwik_DataTable_Filter_AddColumnsProcessedMetricsGoal::GOALS_FULL_TABLE + || $this->isEcommerce + ) { + $requestString .= "&idGoal=" . $this->processOnlyIdGoal; + } + return $requestString . '&filter_update_columns_when_show_all_goals=1'; + } + + protected $columnsToRevenueFilter = array(); + protected $columnsToConversionFilter = array(); + protected $idSite = false; + + private function getIdSite() + { + return $this->idSite; + } + + protected function postDataTableLoadedFromAPI() + { + $valid = parent::postDataTableLoadedFromAPI(); + if ($valid === false) return false; + + foreach ($this->getColumnsToDisplay() as $columnName) { + if (strpos($columnName, 'conversion_rate')) { + $this->dataTable->filter('ColumnCallbackReplace', array($columnName, create_function('$rate', 'if($rate==0) return "0%"; else return $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;'))); + } + return true; + } } diff --git a/core/ViewDataTable/Sparkline.php b/core/ViewDataTable/Sparkline.php index 842227d395..062a9267b2 100644 --- a/core/ViewDataTable/Sparkline.php +++ b/core/ViewDataTable/Sparkline.php @@ -1,59 +1,56 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** * Reads the requested DataTable from the API and prepare data for the Sparkline view. - * + * * @package Piwik * @subpackage Piwik_ViewDataTable */ class Piwik_ViewDataTable_Sparkline extends Piwik_ViewDataTable { - protected function getViewDataTableId() - { - return 'sparkline'; - } + protected function getViewDataTableId() + { + return 'sparkline'; + } - /** - * @see Piwik_ViewDataTable::main() - * @return mixed - */ - public function main() - { - if($this->mainAlreadyExecuted) - { - return; - } - $this->mainAlreadyExecuted = true; - - // If period=range, we force the sparkline to draw daily data points - $period = Piwik_Common::getRequestVar('period'); - if($period == 'range') - { - $_GET['period'] = 'day'; - } - $this->loadDataTableFromAPI(); - // then revert the hack for potentially subsequent getRequestVar - $_GET['period'] = $period; - - $values = $this->getValuesFromDataTable($this->dataTable); - $this->isDataAvailable = true; - if(empty($values)) - { - $values = array_fill(0, 30, 0); - $this->isDataAvailable = false; - } + /** + * @see Piwik_ViewDataTable::main() + * @return mixed + */ + public function main() + { + if ($this->mainAlreadyExecuted) { + return; + } + $this->mainAlreadyExecuted = true; + + // If period=range, we force the sparkline to draw daily data points + $period = Piwik_Common::getRequestVar('period'); + if ($period == 'range') { + $_GET['period'] = 'day'; + } + $this->loadDataTableFromAPI(); + // then revert the hack for potentially subsequent getRequestVar + $_GET['period'] = $period; - $graph = new Piwik_Visualization_Sparkline(); - $graph->setValues($values); + $values = $this->getValuesFromDataTable($this->dataTable); + $this->isDataAvailable = true; + if (empty($values)) { + $values = array_fill(0, 30, 0); + $this->isDataAvailable = false; + } + + $graph = new Piwik_Visualization_Sparkline(); + $graph->setValues($values); $height = Piwik_Common::getRequestVar('height', 0, 'int'); if (!empty($height)) { @@ -66,60 +63,50 @@ class Piwik_ViewDataTable_Sparkline extends Piwik_ViewDataTable } $graph->main(); - - $this->view = $graph; - } - - protected function getValuesFromDataTableArray( $dataTableArray, $columnToPlot ) - { - $dataTableArray->applyQueuedFilters(); - $values = array(); - foreach($dataTableArray->getArray() as $table) - { - if($table->getRowsCount() > 1) - { - throw new Exception("Expecting only one row per DataTable"); - } - $value = 0; - $onlyRow = $table->getFirstRow(); - if($onlyRow !== false) - { - if(!empty($columnToPlot)) - { - $value = $onlyRow->getColumn($columnToPlot); - } - // if not specified, we load by default the first column found - // eg. case of getLastDistinctCountriesGraph - else - { - $columns = $onlyRow->getColumns(); - $value = current($columns); - } - } - $values[] = $value; - } - return $values; - } - - protected function getValuesFromDataTable( $dataTable ) - { - $columns = $this->getColumnsToDisplay(); - $columnToPlot = false; - if(!empty($columns)) - { - $columnToPlot = $columns[0]; - } - $values = false; - // a Piwik_DataTable_Array is returned when using the normal code path to request data from Archives, in all core plugins - // however plugins can also return simple datatable, hence why the sparkline can accept both data types - if($this->dataTable instanceof Piwik_DataTable_Array) - { - $values = $this->getValuesFromDataTableArray($dataTable, $columnToPlot); - } - elseif($this->dataTable instanceof Piwik_DataTable) - { - $values = $this->dataTable->getColumn($columnToPlot); - } - return $values; - } + + $this->view = $graph; + } + + protected function getValuesFromDataTableArray($dataTableArray, $columnToPlot) + { + $dataTableArray->applyQueuedFilters(); + $values = array(); + foreach ($dataTableArray->getArray() as $table) { + if ($table->getRowsCount() > 1) { + throw new Exception("Expecting only one row per DataTable"); + } + $value = 0; + $onlyRow = $table->getFirstRow(); + if ($onlyRow !== false) { + if (!empty($columnToPlot)) { + $value = $onlyRow->getColumn($columnToPlot); + } // if not specified, we load by default the first column found + // eg. case of getLastDistinctCountriesGraph + else { + $columns = $onlyRow->getColumns(); + $value = current($columns); + } + } + $values[] = $value; + } + return $values; + } + + protected function getValuesFromDataTable($dataTable) + { + $columns = $this->getColumnsToDisplay(); + $columnToPlot = false; + if (!empty($columns)) { + $columnToPlot = $columns[0]; + } + $values = false; + // a Piwik_DataTable_Array is returned when using the normal code path to request data from Archives, in all core plugins + // however plugins can also return simple datatable, hence why the sparkline can accept both data types + if ($this->dataTable instanceof Piwik_DataTable_Array) { + $values = $this->getValuesFromDataTableArray($dataTable, $columnToPlot); + } elseif ($this->dataTable instanceof Piwik_DataTable) { + $values = $this->dataTable->getColumn($columnToPlot); + } + return $values; + } } diff --git a/core/Visualization/Chart.php b/core/Visualization/Chart.php index e3e664b473..5971c6c106 100644 --- a/core/Visualization/Chart.php +++ b/core/Visualization/Chart.php @@ -17,177 +17,166 @@ */ abstract class Piwik_Visualization_Chart implements Piwik_View_Interface { - - // the data kept here conforms to the jqplot data layout - // @see http://www.jqplot.com/docs/files/jqPlotOptions-txt.html - protected $series = array(); - protected $data = array(); - protected $axes = array(); - protected $tooltip = array(); - protected $seriesColors = array('#000000'); - protected $seriesPicker = array(); - - // other attributes (not directly used for jqplot) - protected $maxValue; - protected $yUnit = ''; - protected $displayPercentageInTooltip = true; - protected $xSteps = 2; - - /** - * Whether to show every x-axis tick or only every other one. - */ - protected $showAllTicks = false; - - public function setAxisXLabels(&$xLabels) - { - $this->axes['xaxis']['ticks'] = &$xLabels; - } - - public function setAxisXOnClick(&$onClick) - { - $this->axes['xaxis']['onclick'] = &$onClick; - } - - public function setAxisYValues(&$values) - { - foreach ($values as $label => &$data) - { - $this->series[] = array( - 'label' => $label, - 'internalLabel' => $label - ); - - array_walk($data, create_function('&$v', '$v = (float)$v;')); - $this->data[] = &$data; - } - } - - protected function addTooltipToValue($seriesIndex, $valueIndex, $tooltipTitle, $tooltipText) - { - $this->tooltip[$seriesIndex][$valueIndex] = array($tooltipTitle, $tooltipText); - } - - public function setAxisYUnit($yUnit) - { - $yUnits = array(); - for ($i = 0; $i < count($this->data); $i++) - { - $yUnits[] = $yUnit; - } - $this->setAxisYUnits($yUnits); - } - - public function setAxisYUnits($yUnits) - { - // generate an axis config for each unit - $axesIds = array(); - // associate each series with the appropriate axis - $seriesAxes = array(); - // units for tooltips - $seriesUnits = array(); - foreach ($yUnits as $unit) - { - // handle axes ids: first y[]axis, then y[2]axis, y[3]axis... - $nextAxisId = empty($axesIds) ? '' : count($axesIds) + 1; - - $unit = $unit ? $unit : ''; - if (!isset($axesIds[$unit])) - { - $axesIds[$unit] = array('id' => $nextAxisId, 'unit' => $unit); - $seriesAxes[] = 'y'.$nextAxisId.'axis'; - } - else - { - // reuse existing axis - $seriesAxes[] = 'y'.$axesIds[$unit]['id'].'axis'; - } - $seriesUnits[] = $unit; - } - - // generate jqplot axes config - foreach ($axesIds as $axis) { - $axisKey = 'y'.$axis['id'].'axis'; - $this->axes[$axisKey]['tickOptions']['formatString'] = '%s'.$axis['unit']; - } - - $this->tooltip['yUnits'] = $seriesUnits; - - // add axis config to series - foreach ($seriesAxes as $i => $axisName) { - $this->series[$i]['yaxis'] = $axisName; - } - } - - public function setAxisYLabels($labels) - { - foreach ($this->series as &$series) - { - $label = $series['internalLabel']; - if (isset($labels[$label])) - { - $series['label'] = $labels[$label]; - } - } - } - - public function setSelectableColumns($selectableColumns, $multiSelect=true) - { - $this->seriesPicker['selectableColumns'] = $selectableColumns; - $this->seriesPicker['multiSelect'] = $multiSelect; - } - - public function setDisplayPercentageInTooltip($display) - { - $this->displayPercentageInTooltip = $display; - } - - public function setXSteps($steps) - { - $this->xSteps = $steps; - } - - /** - * Show every x-axis tick instead of just every other one. - */ - public function showAllTicks() - { - $this->showAllTicks = true; - } - - public function render() - { - Piwik::overrideCacheControlHeaders(); + + // the data kept here conforms to the jqplot data layout + // @see http://www.jqplot.com/docs/files/jqPlotOptions-txt.html + protected $series = array(); + protected $data = array(); + protected $axes = array(); + protected $tooltip = array(); + protected $seriesColors = array('#000000'); + protected $seriesPicker = array(); + + // other attributes (not directly used for jqplot) + protected $maxValue; + protected $yUnit = ''; + protected $displayPercentageInTooltip = true; + protected $xSteps = 2; + + /** + * Whether to show every x-axis tick or only every other one. + */ + protected $showAllTicks = false; + + public function setAxisXLabels(&$xLabels) + { + $this->axes['xaxis']['ticks'] = & $xLabels; + } + + public function setAxisXOnClick(&$onClick) + { + $this->axes['xaxis']['onclick'] = & $onClick; + } + + public function setAxisYValues(&$values) + { + foreach ($values as $label => &$data) { + $this->series[] = array( + 'label' => $label, + 'internalLabel' => $label + ); + + array_walk($data, create_function('&$v', '$v = (float)$v;')); + $this->data[] = & $data; + } + } + + protected function addTooltipToValue($seriesIndex, $valueIndex, $tooltipTitle, $tooltipText) + { + $this->tooltip[$seriesIndex][$valueIndex] = array($tooltipTitle, $tooltipText); + } + + public function setAxisYUnit($yUnit) + { + $yUnits = array(); + for ($i = 0; $i < count($this->data); $i++) { + $yUnits[] = $yUnit; + } + $this->setAxisYUnits($yUnits); + } + + public function setAxisYUnits($yUnits) + { + // generate an axis config for each unit + $axesIds = array(); + // associate each series with the appropriate axis + $seriesAxes = array(); + // units for tooltips + $seriesUnits = array(); + foreach ($yUnits as $unit) { + // handle axes ids: first y[]axis, then y[2]axis, y[3]axis... + $nextAxisId = empty($axesIds) ? '' : count($axesIds) + 1; + + $unit = $unit ? $unit : ''; + if (!isset($axesIds[$unit])) { + $axesIds[$unit] = array('id' => $nextAxisId, 'unit' => $unit); + $seriesAxes[] = 'y' . $nextAxisId . 'axis'; + } else { + // reuse existing axis + $seriesAxes[] = 'y' . $axesIds[$unit]['id'] . 'axis'; + } + $seriesUnits[] = $unit; + } + + // generate jqplot axes config + foreach ($axesIds as $axis) { + $axisKey = 'y' . $axis['id'] . 'axis'; + $this->axes[$axisKey]['tickOptions']['formatString'] = '%s' . $axis['unit']; + } + + $this->tooltip['yUnits'] = $seriesUnits; + + // add axis config to series + foreach ($seriesAxes as $i => $axisName) { + $this->series[$i]['yaxis'] = $axisName; + } + } + + public function setAxisYLabels($labels) + { + foreach ($this->series as &$series) { + $label = $series['internalLabel']; + if (isset($labels[$label])) { + $series['label'] = $labels[$label]; + } + } + } + + public function setSelectableColumns($selectableColumns, $multiSelect = true) + { + $this->seriesPicker['selectableColumns'] = $selectableColumns; + $this->seriesPicker['multiSelect'] = $multiSelect; + } + + public function setDisplayPercentageInTooltip($display) + { + $this->displayPercentageInTooltip = $display; + } + + public function setXSteps($steps) + { + $this->xSteps = $steps; + } + + /** + * Show every x-axis tick instead of just every other one. + */ + public function showAllTicks() + { + $this->showAllTicks = true; + } + + public function render() + { + Piwik::overrideCacheControlHeaders(); // See http://www.jqplot.com/docs/files/jqPlotOptions-txt.html - $data = array( - 'params' => array( - 'axes' => &$this->axes, - 'series' => &$this->series, - 'seriesColors' => &$this->seriesColors - ), - 'data' => &$this->data, - 'tooltip' => &$this->tooltip, - 'seriesPicker' => &$this->seriesPicker - ); + $data = array( + 'params' => array( + 'axes' => &$this->axes, + 'series' => &$this->series, + 'seriesColors' => &$this->seriesColors + ), + 'data' => &$this->data, + 'tooltip' => &$this->tooltip, + 'seriesPicker' => &$this->seriesPicker + ); Piwik_PostEvent('Visualization_Chart.render', $data); - return Piwik_Common::json_encode($data); - } - - public function customizeChartProperties() - { - // x axis labels with steps - if (isset($this->axes['xaxis']['ticks'])) - { - foreach ($this->axes['xaxis']['ticks'] as $i => &$xLabel) - { - $this->axes['xaxis']['labels'][$i] = $xLabel; - if (!$this->showAllTicks && ($i % $this->xSteps) != 0) - { - $xLabel = ' '; - } - } - } - } - + return Piwik_Common::json_encode($data); + } + + public function customizeChartProperties() + { + // x axis labels with steps + if (isset($this->axes['xaxis']['ticks'])) { + foreach ($this->axes['xaxis']['ticks'] as $i => &$xLabel) { + $this->axes['xaxis']['labels'][$i] = $xLabel; + if (!$this->showAllTicks && ($i % $this->xSteps) != 0) { + $xLabel = ' '; + } + } + } + } + } diff --git a/core/Visualization/Chart/Evolution.php b/core/Visualization/Chart/Evolution.php index f7dad557a1..e8e82527cf 100644 --- a/core/Visualization/Chart/Evolution.php +++ b/core/Visualization/Chart/Evolution.php @@ -10,38 +10,38 @@ */ /** - * Customize the Evolution chart style + * Customize the Evolution chart style * * @package Piwik * @subpackage Piwik_Visualization */ class Piwik_Visualization_Chart_Evolution extends Piwik_Visualization_Chart { - - protected $seriesColors = array('#5170AE','#F29007', '#CC3399', '#9933CC', '#80a033', - '#246AD2', '#FD16EA', '#49C100'); - - public function customizeChartProperties() - { - parent::customizeChartProperties(); - - // if one column is a percentage we set the grid accordingly - // note: it is invalid to plot a percentage dataset along with a numeric dataset - if ($this->yUnit == '%' - && $this->maxValue > 90) - { - $this->axes['yaxis']['ticks'] = array(0, 50, 100); - } - } - - public function getSeriesColors() - { - return $this->seriesColors; - } - public function setSelectableRows($selectableRows) - { - $this->seriesPicker['selectableRows'] = $selectableRows; - } - + protected $seriesColors = array('#5170AE', '#F29007', '#CC3399', '#9933CC', '#80a033', + '#246AD2', '#FD16EA', '#49C100'); + + public function customizeChartProperties() + { + parent::customizeChartProperties(); + + // if one column is a percentage we set the grid accordingly + // note: it is invalid to plot a percentage dataset along with a numeric dataset + if ($this->yUnit == '%' + && $this->maxValue > 90 + ) { + $this->axes['yaxis']['ticks'] = array(0, 50, 100); + } + } + + public function getSeriesColors() + { + return $this->seriesColors; + } + + public function setSelectableRows($selectableRows) + { + $this->seriesPicker['selectableRows'] = $selectableRows; + } + } diff --git a/core/Visualization/Chart/Pie.php b/core/Visualization/Chart/Pie.php index 00985db7c1..f37b079ddc 100644 --- a/core/Visualization/Chart/Pie.php +++ b/core/Visualization/Chart/Pie.php @@ -17,42 +17,38 @@ */ class Piwik_Visualization_Chart_Pie extends Piwik_Visualization_Chart { - - protected $seriesColors = array('#59727F', '#7DAAC0', '#7F7259', '#C09E7D', '#9BB39B', - '#B1D8B3', '#B39BA7', '#D8B1C5', '#A5A5A5'); - - function customizeChartProperties() - { - if (count($this->data) == 0) - { - return; - } - - // make sure we only have one series - $series = &$this->series[0]; - $this->series = array(&$series); - - $data = &$this->data[0]; - $this->data = array(&$data); - - // we never plot empty pie slices (eg. visits by server time pie chart) - foreach ($data as $i => $value) - { - if ($value <= 0) - { - unset($data[$i]); - unset($this->axes['xaxis']['ticks'][$i]); - } - } - $data = array_values($data); - $this->axes['xaxis']['ticks'] = array_values($this->axes['xaxis']['ticks']); - - // prepare percentages for tooltip - $sum = array_sum($data); - foreach ($data as $i => $value) - { - $value = (float) $value; - $this->tooltip['percentages'][0][$i] = round(100 * $value / $sum); - } - } + + protected $seriesColors = array('#59727F', '#7DAAC0', '#7F7259', '#C09E7D', '#9BB39B', + '#B1D8B3', '#B39BA7', '#D8B1C5', '#A5A5A5'); + + function customizeChartProperties() + { + if (count($this->data) == 0) { + return; + } + + // make sure we only have one series + $series = & $this->series[0]; + $this->series = array(&$series); + + $data = & $this->data[0]; + $this->data = array(&$data); + + // we never plot empty pie slices (eg. visits by server time pie chart) + foreach ($data as $i => $value) { + if ($value <= 0) { + unset($data[$i]); + unset($this->axes['xaxis']['ticks'][$i]); + } + } + $data = array_values($data); + $this->axes['xaxis']['ticks'] = array_values($this->axes['xaxis']['ticks']); + + // prepare percentages for tooltip + $sum = array_sum($data); + foreach ($data as $i => $value) { + $value = (float)$value; + $this->tooltip['percentages'][0][$i] = round(100 * $value / $sum); + } + } } diff --git a/core/Visualization/Chart/VerticalBar.php b/core/Visualization/Chart/VerticalBar.php index 7259683ad5..89fe7fb9a1 100644 --- a/core/Visualization/Chart/VerticalBar.php +++ b/core/Visualization/Chart/VerticalBar.php @@ -17,34 +17,30 @@ */ class Piwik_Visualization_Chart_VerticalBar extends Piwik_Visualization_Chart { - - protected $seriesColors = array('#5170AE','#F3A010', '#CC3399', '#9933CC', '#80a033', - '#246AD2', '#FD16EA', '#49C100'); - - public function customizeChartProperties() - { - parent::customizeChartProperties(); - - if ($this->displayPercentageInTooltip) - { - foreach ($this->data as $seriesIndex => &$series) - { - $sum = array_sum($series); - - foreach ($series as $valueIndex => $value) - { - $value = (float) $value; - - $percentage = 0; - if ($sum > 0) - { - $percentage = round(100 * $value / $sum); - } - - $this->tooltip['percentages'][$seriesIndex][$valueIndex] = $percentage; - } - } - } - } - + + protected $seriesColors = array('#5170AE', '#F3A010', '#CC3399', '#9933CC', '#80a033', + '#246AD2', '#FD16EA', '#49C100'); + + public function customizeChartProperties() + { + parent::customizeChartProperties(); + + if ($this->displayPercentageInTooltip) { + foreach ($this->data as $seriesIndex => &$series) { + $sum = array_sum($series); + + foreach ($series as $valueIndex => $value) { + $value = (float)$value; + + $percentage = 0; + if ($sum > 0) { + $percentage = round(100 * $value / $sum); + } + + $this->tooltip['percentages'][$seriesIndex][$valueIndex] = $percentage; + } + } + } + } + } diff --git a/core/Visualization/Cloud.php b/core/Visualization/Cloud.php index ba3a3ebabd..bf7a2227ee 100644 --- a/core/Visualization/Cloud.php +++ b/core/Visualization/Cloud.php @@ -11,20 +11,20 @@ /** * Generates a tag cloud from a given data array. - * The generated tag cloud can be in PHP format, or in HTML. + * The generated tag cloud can be in PHP format, or in HTML. * * Inspired from Derek Harvey (www.derekharvey.co.uk) - * + * * @package Piwik * @subpackage Piwik_Visualization */ class Piwik_Visualization_Cloud implements Piwik_View_Interface { - /** Used by integration tests to make sure output is consistent. */ - public static $debugDisableShuffle = false; + /** Used by integration tests to make sure output is consistent. */ + public static $debugDisableShuffle = false; - protected $wordsArray = array(); - public $truncatingLimit = 50; + protected $wordsArray = array(); + public $truncatingLimit = 50; /** * Assign word to array @@ -32,94 +32,82 @@ class Piwik_Visualization_Cloud implements Piwik_View_Interface * @param int $value * @return string */ - function addWord($word, $value = 1) - { - if (isset($this->wordsArray[$word])) - { - $this->wordsArray[$word] += $value; - } - else - { - $this->wordsArray[$word] = $value; - } - } + function addWord($word, $value = 1) + { + if (isset($this->wordsArray[$word])) { + $this->wordsArray[$word] += $value; + } else { + $this->wordsArray[$word] = $value; + } + } + + public function render() + { + $this->shuffleCloud(); + $return = array(); + if (empty($this->wordsArray)) { + return array(); + } + $maxValue = max($this->wordsArray); + foreach ($this->wordsArray as $word => $popularity) { + $wordTruncated = $word; + if (Piwik_Common::mb_strlen($word) > $this->truncatingLimit) { + $wordTruncated = Piwik_Common::mb_substr($word, 0, $this->truncatingLimit - 3) . '...'; + } + + // case hideFutureHoursWhenToday=1 shows hours with no visits + if ($maxValue == 0) { + $percent = 0; + } else { + $percent = ($popularity / $maxValue) * 100; + } + // CSS style value + $sizeRange = $this->getClassFromPercent($percent); + + $return[$word] = array( + 'word' => $word, + 'wordTruncated' => $wordTruncated, + 'value' => $popularity, + 'size' => $sizeRange, + 'percent' => $percent, + ); + } + return $return; + } + + /** + * Shuffle associated names in array + */ + protected function shuffleCloud() + { + if (self::$debugDisableShuffle) { + return; + } + + $keys = array_keys($this->wordsArray); - public function render() - { - $this->shuffleCloud(); - $return = array(); - if(empty($this->wordsArray)) { - return array(); - } - $maxValue = max($this->wordsArray); - foreach ($this->wordsArray as $word => $popularity) - { - $wordTruncated = $word; - if(Piwik_Common::mb_strlen($word) > $this->truncatingLimit) - { - $wordTruncated = Piwik_Common::mb_substr($word, 0, $this->truncatingLimit - 3).'...'; - } - - // case hideFutureHoursWhenToday=1 shows hours with no visits - if($maxValue == 0) - { - $percent = 0; - } - else - { - $percent = ($popularity / $maxValue) * 100; - } - // CSS style value - $sizeRange = $this->getClassFromPercent($percent); + shuffle($keys); - $return[$word] = array( - 'word' => $word, - 'wordTruncated' => $wordTruncated, - 'value' => $popularity, - 'size' => $sizeRange, - 'percent' => $percent, - ); - } - return $return; - } - - /** - * Shuffle associated names in array - */ - protected function shuffleCloud() - { - if (self::$debugDisableShuffle) - { - return; - } + if (count($keys) && is_array($keys)) { + $tmpArray = $this->wordsArray; + $this->wordsArray = array(); + foreach ($keys as $key => $value) + $this->wordsArray[$value] = $tmpArray[$value]; + } + } - $keys = array_keys($this->wordsArray); - - shuffle($keys); - - if (count($keys) && is_array($keys)) - { - $tmpArray = $this->wordsArray; - $this->wordsArray = array(); - foreach ($keys as $key => $value) - $this->wordsArray[$value] = $tmpArray[$value]; - } - } - - /** - * Get the class range using a percentage - * - * @return int $class - */ - protected function getClassFromPercent($percent) - { - $mapping = array(95, 70, 50, 30, 15, 5, 0); - foreach($mapping as $key => $value) - { - if($percent >= $value) - { - return $key; - } - } - } + /** + * Get the class range using a percentage + * + * @return int $class + */ + protected function getClassFromPercent($percent) + { + $mapping = array(95, 70, 50, 30, 15, 5, 0); + foreach ($mapping as $key => $value) { + if ($percent >= $value) { + return $key; + } + } + } } diff --git a/core/Visualization/Sparkline.php b/core/Visualization/Sparkline.php index d728bc877c..5ea0ca1571 100644 --- a/core/Visualization/Sparkline.php +++ b/core/Visualization/Sparkline.php @@ -17,8 +17,8 @@ require_once PIWIK_INCLUDE_PATH . '/libs/sparkline/lib/Sparkline_Line.php'; /** * Renders a sparkline image given a PHP data array. - * Using the Sparkline PHP Graphing Library sparkline.org - * + * Using the Sparkline PHP Graphing Library sparkline.org + * * @package Piwik * @subpackage Piwik_Visualization */ @@ -36,39 +36,41 @@ class Piwik_Visualization_Sparkline implements Piwik_View_Interface */ protected $_height = 25; - /** - * Array with format: array( x, y, z, ... ) - * @param array $data - */ - function setValues($data) - { - $this->values = $data; - } + /** + * Array with format: array( x, y, z, ... ) + * @param array $data + */ + function setValues($data) + { + $this->values = $data; + } /** * Sets the height of the sparkline * @param int $height */ - public function setHeight($height) { + public function setHeight($height) + { if (!is_numeric($height) || $height <= 0) { return; } - $this->_height = (int) $height; + $this->_height = (int)$height; } /** * Sets the width of the sparkline * @param int $width */ - public function setWidth($width) { + public function setWidth($width) + { if (!is_numeric($width) || $width <= 0) { return; } - $this->_width = (int) $width; + $this->_width = (int)$width; } /** @@ -76,71 +78,67 @@ class Piwik_Visualization_Sparkline implements Piwik_View_Interface * @return int */ public function getWidth() - { - return $this->_width; - } + { + return $this->_width; + } /** * Returns the height of the sparkline * @return int */ public function getHeight() - { - return $this->_height; - } - - function main() - { - $width = $this->getWidth(); - $height = $this->getHeight(); - - $sparkline = new Sparkline_Line(); - $sparkline->SetColor('lineColor', 22, 44, 74); // dark blue - $sparkline->SetColorHtml('red', '#FF7F7F'); - $sparkline->SetColorHtml('blue', '#55AAFF'); - $sparkline->SetColorHtml('green', '#75BF7C'); - - $min = $max = $last = null; - $i = 0; - $toRemove = array('%', str_replace('%s', '', Piwik_Translate('General_Seconds'))); - foreach($this->values as $value) - { - // 50% and 50s should be plotted as 50 - $value = str_replace($toRemove, '', $value); - // replace localized decimal separator - $value = str_replace(',', '.', $value); - if ($value == '') - { - $value = 0; - } - - $sparkline->SetData($i, $value); - - if( null == $min || $value <= $min[1]) - { - $min = array($i, $value); - } - if(null == $max || $value >= $max[1]) - { - $max = array($i, $value); - } - $last = array($i, $value); - $i++; - } - $sparkline->SetYMin(0); - $sparkline->SetYMax($max[1]); - $sparkline->SetPadding( 3, 0, 2, 0 ); // top, right, bottom, left - $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; - $sparkline->RenderResampled($width*$ratio, $height*$ratio); - $this->sparkline = $sparkline; - } - - function render() - { - $this->sparkline->Output(); - } + { + return $this->_height; + } + + function main() + { + $width = $this->getWidth(); + $height = $this->getHeight(); + + $sparkline = new Sparkline_Line(); + $sparkline->SetColor('lineColor', 22, 44, 74); // dark blue + $sparkline->SetColorHtml('red', '#FF7F7F'); + $sparkline->SetColorHtml('blue', '#55AAFF'); + $sparkline->SetColorHtml('green', '#75BF7C'); + + $min = $max = $last = null; + $i = 0; + $toRemove = array('%', str_replace('%s', '', Piwik_Translate('General_Seconds'))); + foreach ($this->values as $value) { + // 50% and 50s should be plotted as 50 + $value = str_replace($toRemove, '', $value); + // replace localized decimal separator + $value = str_replace(',', '.', $value); + if ($value == '') { + $value = 0; + } + + $sparkline->SetData($i, $value); + + if (null == $min || $value <= $min[1]) { + $min = array($i, $value); + } + if (null == $max || $value >= $max[1]) { + $max = array($i, $value); + } + $last = array($i, $value); + $i++; + } + $sparkline->SetYMin(0); + $sparkline->SetYMax($max[1]); + $sparkline->SetPadding(3, 0, 2, 0); // top, right, bottom, left + $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; + $sparkline->RenderResampled($width * $ratio, $height * $ratio); + $this->sparkline = $sparkline; + } + + function render() + { + $this->sparkline->Output(); + } } diff --git a/core/testMinimumPhpVersion.php b/core/testMinimumPhpVersion.php index 508602e5d0..4646cdbc95 100644 --- a/core/testMinimumPhpVersion.php +++ b/core/testMinimumPhpVersion.php @@ -1,16 +1,16 @@ <?php /** * Piwik - Open source web analytics - * + * * @link http://piwik.org * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later - * + * * @category Piwik * @package Piwik */ /** - * This file is executed before anything else. + * This file is executed before anything else. * It checks the minimum PHP version required to run Piwik. * This file must be compatible PHP4. */ @@ -20,48 +20,41 @@ $piwik_errorMessage = ''; // Minimum requirement: ->newInstanceArgs in 5.1.3 $piwik_minimumPHPVersion = '5.1.3RC'; $piwik_currentPHPVersion = PHP_VERSION; -$minimumPhpInvalid = version_compare($piwik_minimumPHPVersion , $piwik_currentPHPVersion ) > 0; -if( $minimumPhpInvalid ) -{ - $piwik_errorMessage .= "<p><b>To run Piwik you need at least PHP version $piwik_minimumPHPVersion</b></p> +$minimumPhpInvalid = version_compare($piwik_minimumPHPVersion, $piwik_currentPHPVersion) > 0; +if ($minimumPhpInvalid) { + $piwik_errorMessage .= "<p><b>To run Piwik you need at least PHP version $piwik_minimumPHPVersion</b></p> <p>Unfortunately it seems your webserver is using PHP version $piwik_currentPHPVersion. </p> <p>Please try to update your PHP version, Piwik is really worth it! Nowadays most web hosts support PHP $piwik_minimumPHPVersion.</p> <p>Also see the FAQ: <a href='http://piwik.org/faq/how-to-install/#faq_77'>My Web host supports PHP4 by default. How can I enable PHP5?</a></p>"; -} -else -{ - $piwik_zend_compatibility_mode = ini_get("zend.ze1_compatibility_mode"); - if($piwik_zend_compatibility_mode == 1) - { - $piwik_errorMessage .= "<p><b>Piwik is not compatible with the directive <code>zend.ze1_compatibility_mode = On</code></b></p> +} else { + $piwik_zend_compatibility_mode = ini_get("zend.ze1_compatibility_mode"); + if ($piwik_zend_compatibility_mode == 1) { + $piwik_errorMessage .= "<p><b>Piwik is not compatible with the directive <code>zend.ze1_compatibility_mode = On</code></b></p> <p>It seems your php.ini file has <pre>zend.ze1_compatibility_mode = On</pre>It makes PHP5 behave like PHP4. If you want to use Piwik you need to set <pre>zend.ze1_compatibility_mode = Off</pre> in your php.ini configuration file, and restart your web server. You may have to ask your system administrator.</p>"; - } + } - if(!class_exists('ArrayObject')) - { - $piwik_errorMessage .= "<p><b>Piwik and Zend Framework require the SPL extension</b></p> + if (!class_exists('ArrayObject')) { + $piwik_errorMessage .= "<p><b>Piwik and Zend Framework require the SPL extension</b></p> <p>It appears your PHP was compiled with <pre>--disable-spl</pre>. To enjoy Piwik, you need PHP compiled without that configure option.</p>"; - } + } - if(!extension_loaded('session')) - { - $piwik_errorMessage .= "<p><b>Piwik and Zend_Session require the session extension</b></p> + if (!extension_loaded('session')) { + $piwik_errorMessage .= "<p><b>Piwik and Zend_Session require the session extension</b></p> <p>It appears your PHP was compiled with <pre>--disable-session</pre>. To enjoy Piwik, you need PHP compiled without that configure option.</p>"; - } + } - if(!function_exists('ini_set')) - { - $piwik_errorMessage .= "<p><b>Piwik and Zend_Session require the <code>ini_set()</code> function</b></p> + if (!function_exists('ini_set')) { + $piwik_errorMessage .= "<p><b>Piwik and Zend_Session require the <code>ini_set()</code> function</b></p> <p>It appears your PHP has disabled this function. To enjoy Piwik, you need remove <pre>ini_set</pre> from your <pre>disable_functions</pre> directive in php.ini, and restart your webserver.</p>"; - } + } } -if(!function_exists('Piwik_ExitWithMessage')) { +if (!function_exists('Piwik_ExitWithMessage')) { /** * Displays info/warning/error message in a friendly UI and exits. * @@ -72,12 +65,10 @@ if(!function_exists('Piwik_ExitWithMessage')) { function Piwik_ExitWithMessage($message, $optionalTrace = false, $optionalLinks = false) { @header('Content-Type: text/html; charset=utf-8'); - if($optionalTrace) - { - $optionalTrace = '<span style="color:#888888">Backtrace:<br /><pre>'.$optionalTrace.'</pre></span>'; + if ($optionalTrace) { + $optionalTrace = '<span style="color:#888888">Backtrace:<br /><pre>' . $optionalTrace . '</pre></span>'; } - if($optionalLinks) - { + if ($optionalLinks) { $optionalLinks = '<ul> <li><a target="_blank" href="http://piwik.org">Piwik.org homepage</a></li> <li><a target="_blank" href="http://piwik.org/faq/">Piwik Frequently Asked Questions</a></li> @@ -90,17 +81,16 @@ if(!function_exists('Piwik_ExitWithMessage')) { $footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/themes/default/simple_structure_footer.tpl'); $headerPage = str_replace('{$HTML_TITLE}', 'Piwik › Error', $headerPage); - $content = '<p>'.$message.'</p> + $content = '<p>' . $message . '</p> <p><a href="index.php">Go to Piwik</a><br/> <a href="index.php?module=Login">Login</a></p> - '. $optionalTrace .' '. $optionalLinks; + ' . $optionalTrace . ' ' . $optionalLinks; echo $headerPage . $content . $footerPage; exit; } } -if(!empty($piwik_errorMessage)) -{ - Piwik_ExitWithMessage($piwik_errorMessage, false, true); +if (!empty($piwik_errorMessage)) { + Piwik_ExitWithMessage($piwik_errorMessage, false, true); } |