From 0c3ef698010708ba0484aabd5a01db3b62f47365 Mon Sep 17 00:00:00 2001 From: mattpiwik Date: Wed, 17 Dec 2008 17:26:15 +0000 Subject: - adding Goal Tracking related goodness in core, and plugins - goal table icon below datatable that have goal segmentation - rss export icon below datatable - cleaning code, refactoring, renaming goodness - adding switch enable_detect_unique_visitor_using_settings that enables/disable heuristic based on config hash - refactoring how plugins handle archiving for more clarity git-svn-id: http://dev.piwik.org/svn/trunk@838 59fd770c-687e-43c8-a1e3-f5a4ff64c105 --- config/global.ini.php | 14 +- core/API/DataTableGenericFilter.php | 9 +- core/API/ResponseBuilder.php | 2 +- core/Access.php | 14 +- core/Archive.php | 486 ++++++----- core/Archive/Single.php | 3 +- core/ArchiveProcessing.php | 26 +- core/ArchiveProcessing/Day.php | 208 ++++- core/ArchiveProcessing/Period.php | 46 +- core/ArchiveProcessing/Record/BlobArray.php | 4 +- core/Common.php | 32 +- core/Controller.php | 10 + core/DataTable.php | 18 +- core/DataTable/Filter.php | 3 +- .../Filter/AddColumnsWhenShowAllColumns.php | 3 +- core/DataTable/Filter/AddSummaryRow.php | 2 +- core/DataTable/Filter/ExactMatch.php | 104 +-- core/DataTable/Filter/ExcludeLowPopulation.php | 20 +- core/DataTable/Filter/ReplaceColumnNames.php | 31 +- core/DataTable/Filter/Sort.php | 56 +- .../Filter/UpdateColumnsWhenShowAllGoals.php | 115 +++ core/DataTable/Renderer/Csv.php | 21 +- core/DataTable/Renderer/Php.php | 2 +- core/DataTable/Renderer/Rss.php | 16 +- core/DataTable/Renderer/Xml.php | 19 +- core/DataTable/Row.php | 65 +- core/Date.php | 22 +- core/FrontController.php | 1 - core/Period/Range.php | 1 - core/Piwik.php | 29 +- core/Plugin.php | 9 - core/SmartyPlugins/function.logoHtml.php | 32 + core/SmartyPlugins/function.url.php | 62 +- core/Timer.php | 4 +- core/Tracker.php | 51 +- core/Tracker/Action.php | 130 +-- core/Tracker/Generator.php | 3 +- core/Tracker/Generator/Tracker.php | 1 - core/Tracker/Generator/Visit.php | 6 +- core/Tracker/GoalManager.php | 147 ++++ core/Tracker/Visit.php | 480 +++++------ core/Updates/0.2.28.php | 9 + core/Version.php | 2 +- core/View.php | 2 +- core/ViewDataTable.php | 142 ++- core/ViewDataTable/Cloud.php | 6 +- .../GenerateGraphData/ChartEvolution.php | 384 ++++----- core/ViewDataTable/GenerateGraphHTML.php | 14 +- .../GenerateGraphHTML/ChartEvolution.php | 26 +- core/ViewDataTable/HtmlTable.php | 45 +- core/ViewDataTable/HtmlTable/AllColumns.php | 19 +- core/ViewDataTable/HtmlTable/Goals.php | 77 ++ core/ViewDataTable/Sparkline.php | 21 +- core/Visualization/ChartEvolution.php | 152 ++-- misc/TODO | 37 +- misc/generateVisits.php | 8 +- piwik.php | 1 + plugins/Actions/Actions.php | 591 ++++++------- plugins/Actions/Controller.php | 2 +- plugins/CoreHome/Controller.php | 104 +-- plugins/CoreHome/templates/cloud.tpl | 2 +- plugins/CoreHome/templates/datatable.css | 14 - plugins/CoreHome/templates/datatable.js | 39 +- plugins/CoreHome/templates/datatable.tpl | 10 +- plugins/CoreHome/templates/datatable_actions.tpl | 2 +- .../CoreHome/templates/datatable_actions_js.tpl | 6 +- .../templates/datatable_actions_recursive.tpl | 2 +- .../templates/datatable_actions_subdatable.tpl | 2 +- plugins/CoreHome/templates/datatable_footer.tpl | 16 +- plugins/CoreHome/templates/datatable_js.tpl | 6 +- plugins/CoreHome/templates/graph.tpl | 2 +- plugins/ExampleAPI/API.php | 1 + plugins/ExamplePlugin/ExamplePlugin.php | 18 + plugins/Installation/Controller.php | 952 ++++++++++----------- plugins/Provider/API.php | 13 - plugins/Provider/Controller.php | 4 +- plugins/Provider/Provider.php | 3 +- plugins/Referers/API.php | 4 +- plugins/Referers/Controller.php | 62 +- plugins/Referers/Referers.php | 238 ++++-- plugins/Referers/functions.php | 3 +- plugins/Referers/index.tpl | 39 - plugins/Referers/searchEngines_Keywords.tpl | 9 - plugins/Referers/templates/index.tpl | 39 + .../Referers/templates/searchEngines_Keywords.tpl | 9 + plugins/UserCountry/Controller.php | 28 +- plugins/UserCountry/UserCountry.php | 43 +- plugins/UserSettings/UserSettings.php | 12 +- plugins/VisitFrequency/API.php | 6 + plugins/VisitFrequency/Controller.php | 3 +- plugins/VisitFrequency/VisitFrequency.php | 5 +- plugins/VisitTime/Controller.php | 1 + plugins/VisitTime/VisitTime.php | 47 +- plugins/VisitsSummary/API.php | 6 + plugins/VisitsSummary/Controller.php | 1 + tests/core/DataTable.test.php | 8 +- tests/core/DataTable/Renderer.test.php | 22 +- tests/core/Piwik.test.php | 5 +- tests/datasets/referer-xss.txt | 7 + themes/default/common.css | 16 + themes/default/images/feed.png | Bin 0 -> 691 bytes themes/default/images/goal.png | Bin 0 -> 672 bytes 102 files changed, 3313 insertions(+), 2341 deletions(-) create mode 100644 core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php create mode 100644 core/SmartyPlugins/function.logoHtml.php create mode 100644 core/Tracker/GoalManager.php create mode 100644 core/Updates/0.2.28.php create mode 100644 core/ViewDataTable/HtmlTable/Goals.php delete mode 100644 plugins/Referers/index.tpl delete mode 100644 plugins/Referers/searchEngines_Keywords.tpl create mode 100644 plugins/Referers/templates/index.tpl create mode 100644 plugins/Referers/templates/searchEngines_Keywords.tpl create mode 100644 tests/datasets/referer-xss.txt create mode 100644 themes/default/images/feed.png create mode 100644 themes/default/images/goal.png diff --git a/config/global.ini.php b/config/global.ini.php index 4331a20df7..4cf9ad1006 100755 --- a/config/global.ini.php +++ b/config/global.ini.php @@ -61,7 +61,6 @@ PluginsInstalled[] = Installation [Plugins_Tracker] - [Debug] ; if set to true, the archiving process will always be triggered, even if the archive has already been computed ; this is useful when making changes to the archiving code so we can force the archiving process @@ -98,6 +97,9 @@ enable_browser_archiving_triggering = true ; the page first-post in the subcategory development which belongs to the blog category action_category_delimiter = / +; currency used by default when reporting money in Piwik +default_currency = "$" + ; if you want all your users to use Piwik in only one language, disable the LanguagesManager ; plugin, and set this default_language (users won't see the language drop down) default_language = en @@ -152,6 +154,16 @@ campaign_keyword_var_name = piwik_kwd ; name of the cookie used to store the visitor information cookie_name = piwik_visitor +; if set to false, any goal conversion will be credited to the last more recent non empty referer. +; when set to true, the first ever referer used to reach the website will be used +use_first_referer_to_determine_goal_referer = false + +; if set to true, Piwik will try to match visitors without cookie to a previous visitor that has the same +; configuration: OS, browser, resolution, IP, etc. This heuristic adds an extra SQL query for each page view without cookie. +; it is advised to set it to true for more accurate detection of unique visitors. +; However when most users have the same IP, and the same configuration, it is advised to set it to false +enable_detect_unique_visitor_using_settings = false + [log] ;possible values for log: screen, database, file diff --git a/core/API/DataTableGenericFilter.php b/core/API/DataTableGenericFilter.php index bfe6367027..4304da0c40 100644 --- a/core/API/DataTableGenericFilter.php +++ b/core/API/DataTableGenericFilter.php @@ -42,13 +42,16 @@ class Piwik_API_DataTableGenericFilter ), 'ExcludeLowPopulation' => array( 'filter_excludelowpop' => array('string'), - 'filter_excludelowpop_value'=> array('float'), + 'filter_excludelowpop_value'=> array('float', '0'), ), 'AddColumnsWhenShowAllColumns' => array( 'filter_add_columns_when_show_all_columns' => array('integer') ), + 'UpdateColumnsWhenShowAllGoals' => array( + 'filter_update_columns_when_show_all_goals' => array('integer') + ), 'Sort' => array( - 'filter_sort_column' => array('string', 'nb_visits'), + 'filter_sort_column' => array('string', Piwik_Archive::INDEX_NB_VISITS), 'filter_sort_order' => array('string', Zend_Registry::get('config')->General->dataTable_default_sort_order), ), 'Limit' => array( @@ -110,7 +113,7 @@ class Piwik_API_DataTableGenericFilter break; } } - + if(!$exceptionRaised) { // a generic filter class name must follow this pattern diff --git a/core/API/ResponseBuilder.php b/core/API/ResponseBuilder.php index 2a97febdb0..f0455c95c9 100644 --- a/core/API/ResponseBuilder.php +++ b/core/API/ResponseBuilder.php @@ -245,7 +245,7 @@ class Piwik_API_ResponseBuilder protected function handleDataTable($datatable) { // if the flag disable_generic_filters is defined we skip the generic filters - if(Piwik_Common::getRequestVar('disable_generic_filters', 'false', 'string', $this->request) == 'false') + if('false' == Piwik_Common::getRequestVar('disable_generic_filters', 'false', 'string', $this->request)) { $genericFilter = new Piwik_API_DataTableGenericFilter($datatable, $this->request); $genericFilter->filter(); diff --git a/core/Access.php b/core/Access.php index f6251ec2a1..36a91f5c77 100644 --- a/core/Access.php +++ b/core/Access.php @@ -253,7 +253,6 @@ class Piwik_Access /** * If the user doesn't have an ADMIN access for at least one website, throws an exception - * * @throws Exception */ public function checkUserHasSomeAdminAccess() @@ -264,6 +263,19 @@ class Piwik_Access throw new Piwik_Access_NoAccessException("You can't access this resource as it requires an 'admin' access for at least one website."); } } + + /** + * If the user doesn't have any view permission, throw exception + * @throws Exception + */ + public function checkUserHasSomeViewAccess() + { + $idSitesAccessible = $this->getSitesIdWithAtLeastViewAccess(); + if(count($idSitesAccessible) == 0) + { + throw new Piwik_Access_NoAccessException("You can't access this resource as it requires a 'view' access for at least one website."); + } + } /** * This method checks that the user has ADMIN access for the given list of websites. diff --git a/core/Archive.php b/core/Archive.php index b06bc11891..f6ca63c2b6 100644 --- a/core/Archive.php +++ b/core/Archive.php @@ -1,255 +1,273 @@ - - * $archive = Piwik_Archive::build($idSite = 1, $period = 'week', '2008-03-08' ); - * $dataTable = $archive->getDataTable('Provider_hostnameExt'); - * $dataTable->queueFilter('Piwik_DataTable_Filter_ReplaceColumnNames'); - * return $dataTable; - * - * - * Example bis: - *
- * 		$archive = Piwik_Archive::build($idSite = 3, $period = 'day', $date = 'today' );
- * 		$nbVisits = $archive->getNumeric('nb_visits');
- * 		return $nbVisits;		
- * 
- * - * 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; + string indexed column name +require_once 'Period.php'; +require_once 'Date.php'; +require_once 'ArchiveProcessing.php'; +require_once 'Archive/Single.php'; + +/** + * The archive object is used to query specific data for a day or a period of statistics for a given website. + * + * Example: + *
+ * 		$archive = Piwik_Archive::build($idSite = 1, $period = 'week', '2008-03-08' );
+ * 		$dataTable = $archive->getDataTable('Provider_hostnameExt');
+ * 		$dataTable->queueFilter('Piwik_DataTable_Filter_ReplaceColumnNames');
+ * 		return $dataTable;
+ * 
+ * + * Example bis: + *
+ * 		$archive = Piwik_Archive::build($idSite = 3, $period = 'day', $date = 'today' );
+ * 		$nbVisits = $archive->getNumeric('nb_visits');
+ * 		return $nbVisits;		
+ * 
+ * + * 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_GOAL_NB_CONVERSIONS = 1; + const INDEX_GOAL_REVENUE = 2; + 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_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', ); + + public static $mappingFromIdToNameGoal = array( + Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 'nb_conversions', + Piwik_Archive::INDEX_GOAL_REVENUE => 'revenue', + ); + /* * string indexed column name => Integer indexed column name */ 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, - ); - - /** - * Website Piwik_Site - * - * @var Piwik_Site - */ - protected $site = null; - - /** - * Stores the already built archives. - * Act as a big caching array - * - * @var array of Piwik_Archive - */ - static protected $alreadyBuilt = array(); - - /** - * Builds an Archive object or returns the same archive if previously built. - * - * @param string|int idSite integer, or comma separated list of integer - * @param string|Piwik_Date $date 'YYYY-MM-DD' or magic keywords 'today' @see Piwik_Date::factory() - * @param string $period 'week' 'day' etc. - * - * @return Piwik_Archive - */ - static public function build($idSite, $period, $strDate ) - { - if($idSite === 'all') - { - $sites = Piwik_SitesManager_API::getSitesIdWithAtLeastViewAccess(); - } - else - { - $sites = Piwik_Site::getIdSitesFromIdSitesString($idSite); + '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, + ); + + /** + * Website Piwik_Site + * + * @var Piwik_Site + */ + protected $site = null; + + /** + * Stores the already built archives. + * Act as a big caching array + * + * @var array of Piwik_Archive + */ + static protected $alreadyBuilt = array(); + + /** + * Builds an Archive object or returns the same archive if previously built. + * + * @param string|int idSite integer, or comma separated list of integer + * @param string|Piwik_Date $date 'YYYY-MM-DD' or magic keywords 'today' @see Piwik_Date::factory() + * @param string $period 'week' 'day' etc. + * + * @return Piwik_Archive + */ + static public function build($idSite, $period, $strDate ) + { + if($idSite === 'all') + { + $sites = Piwik_SitesManager_API::getSitesIdWithAtLeastViewAccess(); } - - // idSite=1,3 or idSite=all + else + { + $sites = Piwik_Site::getIdSitesFromIdSitesString($idSite); + } + + // idSite=1,3 or idSite=all if( count($sites) > 1 - || $idSite === 'all' ) - { - require_once 'Archive/Array/IndexedBySite.php'; - $archive = new Piwik_Archive_Array_IndexedBySite($sites, $period, $strDate); - } - // if a period date string is detected: either 'last30', 'previous10' or 'YYYY-MM-DD,YYYY-MM-DD' - elseif(is_string($strDate) - && ( - ereg('^(last|previous){1}([0-9]*)$', $strDate, $regs) - || ereg('^([0-9]{4}-[0-9]{1,2}-[0-9]{1,2}),([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})$', $strDate, $regs) - ) - ) - { - $oSite = new Piwik_Site($idSite); - require_once 'Archive/Array/IndexedByDate.php'; - $archive = new Piwik_Archive_Array_IndexedByDate($oSite, $period, $strDate); - } - // case we request a single archive - else - { - if(is_string($strDate)) - { - $oDate = Piwik_Date::factory($strDate); - } - else - { - $oDate = $strDate; - } - $date = $oDate->toString(); - + || $idSite === 'all' ) + { + require_once 'Archive/Array/IndexedBySite.php'; + $archive = new Piwik_Archive_Array_IndexedBySite($sites, $period, $strDate); + } + // if a period date string is detected: either 'last30', 'previous10' or 'YYYY-MM-DD,YYYY-MM-DD' + elseif(is_string($strDate) + && ( + ereg('^(last|previous){1}([0-9]*)$', $strDate, $regs) + || ereg('^([0-9]{4}-[0-9]{1,2}-[0-9]{1,2}),([0-9]{4}-[0-9]{1,2}-[0-9]{1,2})$', $strDate, $regs) + ) + ) + { + $oSite = new Piwik_Site($idSite); + require_once 'Archive/Array/IndexedByDate.php'; + $archive = new Piwik_Archive_Array_IndexedByDate($oSite, $period, $strDate); + } + // case we request a single archive + else + { + if(is_string($strDate)) + { + $oDate = Piwik_Date::factory($strDate); + } + else + { + $oDate = $strDate; + } + $date = $oDate->toString(); + if(isset(self::$alreadyBuilt[$idSite][$date][$period])) { - return self::$alreadyBuilt[$idSite][$date][$period]; - } - - $oPeriod = Piwik_Period::factory($period, $oDate); - - $archive = new Piwik_Archive_Single(); - $archive->setPeriod($oPeriod); + return self::$alreadyBuilt[$idSite][$date][$period]; + } + + $oPeriod = Piwik_Period::factory($period, $oDate); + + $archive = new Piwik_Archive_Single(); + $archive->setPeriod($oPeriod); $archive->setSite(new Piwik_Site($idSite)); - $archiveJustProcessed = $archive->prepareArchive(); + $archiveJustProcessed = $archive->prepareArchive(); //we don't cache the archives just processed, the datatable were freed from memory if(!$archiveJustProcessed) - { + { self::$alreadyBuilt[$idSite][$date][$period] = $archive; - } - } - - return $archive; - } - - 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 ); + } + } + + return $archive; + } + + 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 ); + + /** + * + * @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 $idSubTable or null if requesting the parent table * @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 $idSubTable or null if requesting the parent table - * @return Piwik_DataTable - */ - abstract public function getDataTableExpanded($name, $idSubTable = null); - - /** - * Sets the site - * - * @param Piwik_Site $site - */ - public function setSite( Piwik_Site $site ) - { - $this->site = $site; - } - - /** - * Gets the site - * - * @param Piwik_Site $site - */ - public function getSite() - { - return $this->site; - } - - /** - * Returns the Id site associated with this archive - * - * @return int - */ - public function getIdSite() - { - return $this->site->getId(); - } - -} - - - - - + */ + abstract public function getDataTableExpanded($name, $idSubTable = null); + + /** + * Sets the site + * + * @param Piwik_Site $site + */ + public function setSite( Piwik_Site $site ) + { + $this->site = $site; + } + + /** + * Gets the site + * + * @param Piwik_Site $site + */ + public function getSite() + { + return $this->site; + } + + /** + * Returns the Id site associated with this archive + * + * @return int + */ + public function getIdSite() + { + return $this->site->getId(); + } + +} + + + + + diff --git a/core/Archive/Single.php b/core/Archive/Single.php index faf53de1c2..4035e2804b 100644 --- a/core/Archive/Single.php +++ b/core/Archive/Single.php @@ -372,6 +372,7 @@ class Piwik_Archive_Single extends Piwik_Archive * 'nb_actions', * 'sum_visit_length', * 'bounce_count', + * 'nb_visits_converted' * ); * * @param string|array $fields Name or array of names of Archive fields @@ -454,4 +455,4 @@ class Piwik_Archive_Single extends Piwik_Archive return $dataTableToLoad; } } -?> \ No newline at end of file +?> diff --git a/core/ArchiveProcessing.php b/core/ArchiveProcessing.php index 6a1ef954bb..9a99636841 100644 --- a/core/ArchiveProcessing.php +++ b/core/ArchiveProcessing.php @@ -334,6 +334,7 @@ abstract class Piwik_ArchiveProcessing $this->logTable = Piwik::prefixTable('log_visit'); $this->logVisitActionTable = Piwik::prefixTable('log_link_visit_action'); $this->logActionTable = Piwik::prefixTable('log_action'); + $this->logConversionTable = Piwik::prefixTable('log_conversion'); } /** @@ -430,6 +431,28 @@ abstract class Piwik_ArchiveProcessing 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 * @@ -499,7 +522,7 @@ abstract class Piwik_ArchiveProcessing ); $timeStampWhere = " AND UNIX_TIMESTAMP(ts_archived) >= ? "; $bindSQL[] = $this->maxTimestampArchive; - + $sqlQuery = " SELECT idarchive, value, name, UNIX_TIMESTAMP(date1) as timestamp FROM ".$this->tableArchiveNumeric->getTableName()." WHERE idsite = ? @@ -510,7 +533,6 @@ abstract class Piwik_ArchiveProcessing OR name = 'nb_visits') $timeStampWhere ORDER BY ts_archived DESC"; - $results = Zend_Registry::get('db')->fetchAll($sqlQuery, $bindSQL ); if(empty($results)) { diff --git a/core/ArchiveProcessing/Day.php b/core/ArchiveProcessing/Day.php index f3a4f5d6c7..300234f3ce 100644 --- a/core/ArchiveProcessing/Day.php +++ b/core/ArchiveProcessing/Day.php @@ -11,8 +11,9 @@ /** - * Handles the archiving process for a day. - * The class provides generic methods to manipulate data from the DB, easily create Piwik_DataTable objects. + * Handles the archiving process for a day. + * The class provides generic helper methods to manipulate data from the DB, + * easily create Piwik_DataTable objects from running SELECT ... GROUP BY on the log_visit table. * * All the logic of the archiving is done inside the plugins listening to the event 'ArchiveProcessing_Day.compute' * @@ -51,7 +52,8 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing sum(visit_total_actions) as nb_actions, max(visit_total_actions) as max_actions, sum(visit_total_time) as sum_visit_length, - sum(case visit_total_actions when 1 then 1 else 0 end) as bounce_count + sum(case visit_total_actions when 1 then 1 else 0 end) as bounce_count, + sum(case visit_goal_converted when 1 then 1 else 0 end) as nb_visits_converted FROM ".$this->logTable." WHERE visit_server_date = ? AND idsite = ? @@ -70,7 +72,8 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing { $record = new Piwik_ArchiveProcessing_Record_Numeric($name, $value); } - + $this->setNumberOfVisits($row['nb_visits']); + $this->setNumberOfVisitsConverted($row['nb_visits_converted']); Piwik_PostEvent('ArchiveProcessing_Day.compute', $this); } @@ -138,8 +141,26 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing return $table; } + public function getDataTableFromArray( $array ) + { + $table = new Piwik_DataTable; + $table->addRowsFromArrayWithIndexLabel($array); + return $table; + } + /** - * Helper function that returns common statistics for a given database field distinct values. + * 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 @@ -150,18 +171,18 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing * - 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 DataTable will have a row per distinct operating systems, - * and a column per stat (nb of visits, max actions, etc) + * The returned array will have a row per distinct operating systems, + * and a column per stat (nb of visits, max actions, etc) * - * label nb_uniq_visitors nb_visits nb_actions max_actions sum_visit_length bounce_count - * Linux 27 66 66 1 660 66 - * Windows XP 12 39 39 1 390 39 - * Mac OS 15 36 36 1 360 36 + * '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 Piwik_DataTable + * @return array */ - public function getDataTableInterestForLabel( $label ) + public function getArrayInterestForLabel($label) { $query = "SELECT $label as label, count(distinct visitor_idcookie) as nb_uniq_visitors, @@ -169,34 +190,21 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing sum(visit_total_actions) as nb_actions, max(visit_total_actions) as max_actions, sum(visit_total_time) as sum_visit_length, - sum(case visit_total_actions when 1 then 1 else 0 end) as bounce_count + sum(case visit_total_actions when 1 then 1 else 0 end) as bounce_count, + sum(case visit_goal_converted when 1 then 1 else 0 end) as nb_visits_converted FROM ".$this->logTable." WHERE visit_server_date = ? AND idsite = ? GROUP BY label"; - $query = $this->db->query($query, array( $this->strDateStart, $this->idsite ) ); $interest = array(); - while($rowBefore = $query->fetch()) + while($row = $query->fetch()) { - $row = array( - Piwik_Archive::INDEX_NB_UNIQ_VISITORS => $rowBefore['nb_uniq_visitors'], - Piwik_Archive::INDEX_NB_VISITS => $rowBefore['nb_visits'], - Piwik_Archive::INDEX_NB_ACTIONS => $rowBefore['nb_actions'], - Piwik_Archive::INDEX_MAX_ACTIONS => $rowBefore['max_actions'], - Piwik_Archive::INDEX_SUM_VISIT_LENGTH => $rowBefore['sum_visit_length'], - Piwik_Archive::INDEX_BOUNCE_COUNT => $rowBefore['bounce_count'], - 'label' => $rowBefore['label'] - ); - if(!isset($interest[$row['label']])) $interest[$row['label']]= $this->getNewInterestRow(); $this->updateInterestStats( $row, $interest[$row['label']]); } - - $table = new Piwik_DataTable; - $table->addRowsFromArrayWithIndexLabel($interest); - return $table; + return $interest; } /** @@ -259,17 +267,19 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing /** * 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. + * 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, * ) @@ -279,10 +289,9 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing * @param array of Piwik_DataTable $subArrayLevel1ByKey * @return array Array with N elements: the strings of the datatable serialized */ - public function getDataTablesSerialized( $arrayLevel0, $subArrayLevel1ByKey, $maximumRowsInDataTableLevelZero = null, $maximumRowsInSubDataTable = null) + public function getDataTableWithSubtablesFromArraysIndexedByLabel( $arrayLevel0, $subArrayLevel1ByKey ) { $tablesByLabel = array(); - foreach($arrayLevel0 as $label => $aAllRowsForThisLabel) { $table = new Piwik_DataTable; @@ -292,8 +301,7 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing $parentTableLevel0 = new Piwik_DataTable; $parentTableLevel0->addRowsFromArrayWithIndexLabel($subArrayLevel1ByKey, $tablesByLabel); - $toReturn = $parentTableLevel0->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable); - return $toReturn; + return $parentTableLevel0; } /** @@ -308,7 +316,8 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing 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_BOUNCE_COUNT => 0, + Piwik_Archive::INDEX_NB_VISITS_CONVERTED=> 0, ); } @@ -339,13 +348,124 @@ class Piwik_ArchiveProcessing_Day extends Piwik_ArchiveProcessing * @param array $oldRowToUpdate */ public function updateInterestStats( $newRowToAdd, &$oldRowToUpdate) - { - $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]; - $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_UNIQ_VISITORS] += $newRowToAdd['nb_uniq_visitors']; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_VISITS] += $newRowToAdd['nb_visits']; + $oldRowToUpdate[Piwik_Archive::INDEX_NB_ACTIONS] += $newRowToAdd['nb_actions']; + $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']; + } + + //TODO comment + public function queryConversionsBySegment($segments = '') + { + if(!empty($segments)) + { + $segments = ", ". $segments; + } + $query = "SELECT idgoal, + count(*) as nb_conversions, + sum(revenue) as revenue + $segments + FROM ".$this->logConversionTable." + WHERE visit_server_date = ? + AND idsite = ? + GROUP BY idgoal $segments"; + $query = $this->db->query($query, array( $this->strDateStart, $this->idsite )); + return $query; + } + + public function queryConversionsBySingleSegment($segment) + { + $query = "SELECT idgoal, + count(*) as nb_conversions, + sum(revenue) as revenue, + $segment as label + FROM ".$this->logConversionTable." + WHERE visit_server_date = ? + AND idsite = ? + GROUP BY idgoal, label"; + $query = $this->db->query($query, array( $this->strDateStart, $this->idsite )); + return $query; + } + + /** + * 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_GOALS => array( + * idgoal1 => array( [...] ), + * idgoal2 => array( [...] ), + * ), + * [...] ), + * LABEL2 => array( Piwik_Archive::INDEX_NB_VISITS => Y, [...] ) + * ); + * ) + * @param array by reference, will be modified + * @return void (array by reference is modified) + */ + function enrichConversionsByLabelArray(&$interestByLabel) + { + foreach($interestByLabel as $label => &$values) + { + if(isset($values[Piwik_Archive::INDEX_GOALS])) + { + $revenue = $conversions = 0; + foreach($values[Piwik_Archive::INDEX_GOALS] as $idgoal => $goalValues) + { + $revenue += $goalValues[Piwik_Archive::INDEX_GOAL_REVENUE]; + $conversions += $goalValues[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; + } + $values[Piwik_Archive::INDEX_NB_CONVERSIONS] = $conversions; + $values[Piwik_Archive::INDEX_REVENUE] = $revenue; + } + } + } + + /** + * @param array $interestByLabelAndSubLabel + * @return void (array by reference is modified) + */ + function enrichConversionsByLabelArrayHasTwoLevels(&$interestByLabelAndSubLabel) + { + foreach($interestByLabelAndSubLabel as $mainLabel => &$interestBySubLabel) + { + $this->enrichConversionsByLabelArray($interestBySubLabel); + } + } + + function updateGoalStats($newRowToAdd, &$oldRowToUpdate) + { + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS] += $newRowToAdd['nb_conversions']; + $oldRowToUpdate[Piwik_Archive::INDEX_GOAL_REVENUE] += $newRowToAdd['revenue']; + } + + function getNewGoalRow() + { + return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => 0, + Piwik_Archive::INDEX_GOAL_REVENUE => 0, + ); + } + + function getGoalRowFromQueryRow($queryRow) + { + return array( Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS => $queryRow['nb_conversions'], + Piwik_Archive::INDEX_GOAL_REVENUE => $queryRow['revenue'], + ); } } diff --git a/core/ArchiveProcessing/Period.php b/core/ArchiveProcessing/Period.php index 7e834bec62..a4deaa3faf 100644 --- a/core/ArchiveProcessing/Period.php +++ b/core/ArchiveProcessing/Period.php @@ -119,7 +119,7 @@ class Piwik_ArchiveProcessing_Period extends Piwik_ArchiveProcessing /** - * This powerful method will compute the sum of DataTables over the period for the given fields $aRecordName. + * 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' * @@ -223,20 +223,24 @@ class Piwik_ArchiveProcessing_Period extends Piwik_ArchiveProcessing { $this->archiveNumericValuesMax( 'max_actions' ); $toSum = array( - 'nb_uniq_visitors', + 'nb_uniq_visitors', //TODO fix 'nb_visits', 'nb_actions', 'sum_visit_length', 'bounce_count', + 'nb_visits_converted', ); $record = $this->archiveNumericValuesSum($toSum); - $this->isThereSomeVisits = ($record['nb_visits']->value != 0); + $nbVisits = $record['nb_visits']->value; + $nbVisitsConverted = $record['nb_visits_converted']->value; + $this->isThereSomeVisits = ( $nbVisits!= 0); if($this->isThereSomeVisits === false) { return; } - + $this->setNumberOfVisits($nbVisits); + $this->setNumberOfVisitsConverted($nbVisitsConverted); Piwik_PostEvent('ArchiveProcessing_Period.compute', $this); } @@ -250,21 +254,25 @@ class Piwik_ArchiveProcessing_Period extends Piwik_ArchiveProcessing { parent::postCompute(); - // we delete records that are now out of date - // in the case of a period we delete archives that were archived before the end of the period - // and only if they are at least 1 day old (so we don't delete archives computed today that may be stil valid) - $blobTable = $this->tableArchiveBlob->getTableName(); - $numericTable = $this->tableArchiveNumeric->getTableName(); - - $query = " DELETE - FROM %s - WHERE period > ? - AND DATE(ts_archived) <= date2 - AND date(ts_archived) < date_sub(CURRENT_DATE(), INTERVAL 1 DAY) - "; - - Zend_Registry::get('db')->query(sprintf($query, $blobTable), Piwik::$idPeriods['day']); - Zend_Registry::get('db')->query(sprintf($query, $numericTable), Piwik::$idPeriods['day']); + //TODO should be done in a different asynchronous job + if(rand(0, 15) == 5) + { + // we delete records that are now out of date + // in the case of a period we delete archives that were archived before the end of the period + // and only if they are at least 1 day old (so we don't delete archives computed today that may be stil valid) + $blobTable = $this->tableArchiveBlob->getTableName(); + $numericTable = $this->tableArchiveNumeric->getTableName(); + + $query = " DELETE + FROM %s + WHERE period > ? + AND DATE(ts_archived) <= date2 + AND date(ts_archived) < date_sub(CURRENT_DATE(), INTERVAL 1 DAY) + "; + + Zend_Registry::get('db')->query(sprintf($query, $blobTable), Piwik::$idPeriods['day']); + Zend_Registry::get('db')->query(sprintf($query, $numericTable), Piwik::$idPeriods['day']); + } } } diff --git a/core/ArchiveProcessing/Record/BlobArray.php b/core/ArchiveProcessing/Record/BlobArray.php index 4e3165489e..de187ac62d 100644 --- a/core/ArchiveProcessing/Record/BlobArray.php +++ b/core/ArchiveProcessing/Record/BlobArray.php @@ -32,7 +32,6 @@ */ class Piwik_ArchiveProcessing_Record_BlobArray extends Piwik_ArchiveProcessing_Record { - function __construct( $name, $aValue) { foreach($aValue as $id => $value) @@ -49,13 +48,14 @@ class Piwik_ArchiveProcessing_Record_BlobArray extends Piwik_ArchiveProcessing_R $newName = $name . '_' . $id; } $record = new Piwik_ArchiveProcessing_Record_Blob( $newName, $value ); - } } + public function __toString() { throw new Exception( 'Not valid' ); } + public function delete() { throw new Exception( 'Not valid' ); diff --git a/core/Common.php b/core/Common.php index 2cca73ce91..7b43f2e693 100644 --- a/core/Common.php +++ b/core/Common.php @@ -124,22 +124,22 @@ class Piwik_Common if( false !== strpos($value, '=')) { $exploded = explode('=',$value); - $name = $exploded[0]; - - // if array without indexes - if( substr($name,-2,2) == '[]' ) - { - $name = substr($name, 0, -2); - if( isset($nameToValue[$name]) == false || is_array($nameToValue[$name]) == false ) - { - $nameToValue[$name] = array(); - } - array_push($nameToValue[$name],$exploded[1]); - } - else - { - $nameToValue[$name] = $exploded[1]; - } + $name = $exploded[0]; + + // if array without indexes + if( substr($name,-2,2) == '[]' ) + { + $name = substr($name, 0, -2); + if( isset($nameToValue[$name]) == false || is_array($nameToValue[$name]) == false ) + { + $nameToValue[$name] = array(); + } + array_push($nameToValue[$name],$exploded[1]); + } + else + { + $nameToValue[$name] = $exploded[1]; + } } } return $nameToValue; diff --git a/core/Controller.php b/core/Controller.php index 95356c27bf..e544796d58 100644 --- a/core/Controller.php +++ b/core/Controller.php @@ -9,6 +9,7 @@ * @package Piwik */ +require_once "ViewDataTable.php"; /** * Parent class of all plugins Controllers (located in /plugins/PluginName/Controller.php * It defines some helper functions controllers can use. @@ -78,6 +79,15 @@ abstract class Piwik_Controller */ protected function renderView( Piwik_ViewDataTable $view, $fetch) { + 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) diff --git a/core/DataTable.php b/core/DataTable.php index 2d94c53ee4..3f52385512 100644 --- a/core/DataTable.php +++ b/core/DataTable.php @@ -169,6 +169,13 @@ class Piwik_DataTable */ protected $indexNotUpToDate = false; + /** + * Column name of last time the table was sorted + * + * @var string + */ + protected $tableSortedBy = false; + /** * List of Piwik_DataTable_Filter queued to this table * @@ -218,10 +225,12 @@ class Piwik_DataTable * 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 ) + public function sort( $functionCallback, $columnSortedBy ) { $this->indexNotUpToDate = true; + $this->tableSortedBy = $columnSortedBy; usort( $this->rows, $functionCallback ); if($this->enableRecursiveSort === true) @@ -232,12 +241,17 @@ class Piwik_DataTable { $table = Piwik_DataTable_Manager::getInstance()->getTable($idSubtable); $table->enableRecursiveSort(); - $table->sort($functionCallback); + $table->sort($functionCallback, $columnSortedBy); } } } } + 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 diff --git a/core/DataTable/Filter.php b/core/DataTable/Filter.php index de03b1cfec..46984d5426 100644 --- a/core/DataTable/Filter.php +++ b/core/DataTable/Filter.php @@ -59,4 +59,5 @@ require_once "DataTable/Filter/AddSummaryRow.php"; require_once "DataTable/Filter/ReplaceSummaryRowLabel.php"; require_once "DataTable/Filter/ExactMatch.php"; require_once "DataTable/Filter/SafeDecodeLabel.php"; -require_once "DataTable/Filter/AddColumnsWhenShowAllColumns.php"; +require_once "DataTable/Filter/AddColumnsWhenShowAllColumns.php"; +require_once "DataTable/Filter/UpdateColumnsWhenShowAllGoals.php"; diff --git a/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php b/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php index fd5810512e..7e5427570f 100644 --- a/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php +++ b/core/DataTable/Filter/AddColumnsWhenShowAllColumns.php @@ -24,4 +24,5 @@ class Piwik_DataTable_Filter_AddColumnsWhenShowAllColumns extends Piwik_DataTabl $row->addColumn('bounce_rate', $bounceRate); } } -} \ No newline at end of file +} + diff --git a/core/DataTable/Filter/AddSummaryRow.php b/core/DataTable/Filter/AddSummaryRow.php index f5763ffa07..a62c5dc2da 100644 --- a/core/DataTable/Filter/AddSummaryRow.php +++ b/core/DataTable/Filter/AddSummaryRow.php @@ -60,8 +60,8 @@ class Piwik_DataTable_Filter_AddSummaryRow extends Piwik_DataTable_Filter $newRow->sumRow($rows[$i]); } } - $newRow->addColumn('label', $this->labelSummaryRow); + $newRow->addColumn('label', $this->labelSummaryRow); $filter = new Piwik_DataTable_Filter_Limit($this->table, 0, $this->startRowToSummarize); $this->table->addSummaryRow($newRow); } diff --git a/core/DataTable/Filter/ExactMatch.php b/core/DataTable/Filter/ExactMatch.php index 1027fc62a6..3f68658d61 100644 --- a/core/DataTable/Filter/ExactMatch.php +++ b/core/DataTable/Filter/ExactMatch.php @@ -1,52 +1,52 @@ -patternToSearch = $patternToSearch; - $this->columnToFilter = $columnToFilter; - $this->filter(); - } - - protected function filter() - { - foreach($this->table->getRows() as $key => $row) - { - if( is_array($this->patternToSearch) ) - { - if( in_array($row->getColumn($this->columnToFilter), $this->patternToSearch) == false ) - { - $this->table->deleteRow($key); - } - } - else if( $row->getColumn($this->columnToFilter) != $this->patternToSearch ) - { - $k = $row->getColumn($this->columnToFilter); - $this->table->deleteRow($key); - } - } - } -} - +patternToSearch = $patternToSearch; + $this->columnToFilter = $columnToFilter; + $this->filter(); + } + + protected function filter() + { + foreach($this->table->getRows() as $key => $row) + { + if( is_array($this->patternToSearch) ) + { + if( in_array($row->getColumn($this->columnToFilter), $this->patternToSearch) == false ) + { + $this->table->deleteRow($key); + } + } + else if( $row->getColumn($this->columnToFilter) != $this->patternToSearch ) + { + $k = $row->getColumn($this->columnToFilter); + $this->table->deleteRow($key); + } + } + } +} + diff --git a/core/DataTable/Filter/ExcludeLowPopulation.php b/core/DataTable/Filter/ExcludeLowPopulation.php index cb038d6d78..94acd0baea 100644 --- a/core/DataTable/Filter/ExcludeLowPopulation.php +++ b/core/DataTable/Filter/ExcludeLowPopulation.php @@ -24,21 +24,29 @@ class Piwik_DataTable_Filter_ExcludeLowPopulation extends Piwik_DataTable_Filter static public $minimumValue; public function __construct( $table, $columnToFilter, $minimumValue ) { + parent::__construct($table); $this->columnToFilter = $columnToFilter; + + if($minimumValue == 0) + { + $minimumPercentageThreshold = 0.02; + $allValues = $this->table->getColumn($this->columnToFilter); + $sumValues = array_sum($allValues); + $minimumValue = $sumValues * $minimumPercentageThreshold; + } self::$minimumValue = $minimumValue; - parent::__construct($table); - $this->filter(); + if(self::$minimumValue > 1) + { + $this->filter(); + } } function filter() { - $function = array("Piwik_DataTable_Filter_ExcludeLowPopulation", - "excludeLowPopulation"); - $filter = new Piwik_DataTable_Filter_ColumnCallbackDeleteRow( $this->table, $this->columnToFilter, - $function + array("Piwik_DataTable_Filter_ExcludeLowPopulation", "excludeLowPopulation") ); } diff --git a/core/DataTable/Filter/ReplaceColumnNames.php b/core/DataTable/Filter/ReplaceColumnNames.php index f1bc174c4a..8d25c960ac 100644 --- a/core/DataTable/Filter/ReplaceColumnNames.php +++ b/core/DataTable/Filter/ReplaceColumnNames.php @@ -55,7 +55,9 @@ class Piwik_DataTable_Filter_ReplaceColumnNames extends Piwik_DataTable_Filter { foreach($table->getRows() as $key => $row) { - $this->renameColumns($row); + $oldColumns = $row->getColumns(); + $newColumns = $this->getRenamedColumns($oldColumns); + $row->setColumns( $newColumns ); try { $subTable = Piwik_DataTable_Manager::getInstance()->getTable( $row->getIdSubDataTable() ); $this->filterTable($subTable); @@ -65,18 +67,31 @@ class Piwik_DataTable_Filter_ReplaceColumnNames extends Piwik_DataTable_Filter } } - protected function renameColumns($row) + protected function getRenamedColumns($columns) { - $columns = $row->getColumns(); - foreach($this->mappingToApply as $oldName => $newName) + $newColumns = array(); + foreach($columns as $columnName => $columnValue) { - if(isset($columns[$oldName])) + if(isset(Piwik_Archive::$mappingFromIdToName[$columnName])) { - $columns[$newName] = $columns[$oldName]; - unset($columns[$oldName]); + $columnName = Piwik_Archive::$mappingFromIdToName[$columnName]; + if($columnName == 'goals') + { + $newSubColumns = array(); + foreach($columnValue as $idGoal => $goalValues) + { + foreach($goalValues as $id => $goalValue) + { + $subColumnName = Piwik_Archive::$mappingFromIdToNameGoal[$id]; + $newSubColumns['idgoal='.$idGoal][$subColumnName] = $goalValue; + } + } + $columnValue = $newSubColumns; + } } + $newColumns[$columnName] = $columnValue; } - $row->setColumns($columns); + return $newColumns; } } diff --git a/core/DataTable/Filter/Sort.php b/core/DataTable/Filter/Sort.php index 805a8ed28e..148bb5b6e1 100644 --- a/core/DataTable/Filter/Sort.php +++ b/core/DataTable/Filter/Sort.php @@ -85,6 +85,40 @@ class Piwik_DataTable_Filter_Sort extends Piwik_DataTable_Filter ); } + /** + * @param Piwik_DataTable_Row + */ + 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); + + 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; + } + + return false; + } protected function filter() { if($this->table instanceof Piwik_DataTable_Simple) @@ -97,26 +131,14 @@ class Piwik_DataTable_Filter_Sort extends Piwik_DataTable_Filter return; } $row = current($rows); - $value = $row->getColumn($this->columnToSort); + $this->columnToSort = $this->selectColumnToSort($row); - if($value === false) + if($this->columnToSort === false) { - if(!isset(Piwik_Archive::$mappingFromNameToId[$this->columnToSort])) - { - // we don't throw the exception because we sometimes export a DataTable without a column labelled '2' - // and when the generic filters tries to sort by default using this column 2, this shouldnt raise an exception... - //throw new Exception("The column to sort by '".$this->columnToSort."' is unknown in the row ". implode(array_keys($row->getColumns()), ',')); - return; - } - // case we are sorting by "nb_visits" but the column is still integer indexed - $this->columnToSort = Piwik_Archive::$mappingFromNameToId[$this->columnToSort]; - $value = $row->getColumn($this->columnToSort); - if($value === false) - { - return; - } + return; } + $value = $row->getColumn($this->columnToSort); if( Piwik::isNumeric($value)) { $methodToUse = "sort"; @@ -132,7 +154,7 @@ class Piwik_DataTable_Filter_Sort extends Piwik_DataTable_Filter $methodToUse = "sortString"; } } - $this->table->sort( array($this,$methodToUse) ); + $this->table->sort( array($this,$methodToUse), $this->columnToSort ); } } diff --git a/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php b/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php new file mode 100644 index 0000000000..61ce48a8cc --- /dev/null +++ b/core/DataTable/Filter/UpdateColumnsWhenShowAllGoals.php @@ -0,0 +1,115 @@ +mappingIdToNameGoal = Piwik_Archive::$mappingFromIdToNameGoal; + $this->filter(); + } + + protected function filter() + { + $invalidDivision = 'N/A'; + $roundingPrecision = 2; + $expectedColumns = array(); + foreach($this->table->getRows() as $key => $row) + { + $currentColumns = $row->getColumns(); + $newColumns = array(); + + $nbVisits = 0; + // visits could be undefined when there is a convertion but no visit + if(isset($currentColumns[Piwik_Archive::INDEX_NB_VISITS])) + { + $nbVisits = $currentColumns[Piwik_Archive::INDEX_NB_VISITS]; + } + $newColumns['nb_visits'] = $nbVisits; + $newColumns['label'] = $currentColumns['label']; + + if(isset($currentColumns[Piwik_Archive::INDEX_GOALS])) + { + $nbVisitsConverted = $revenue = 0; + if(isset($currentColumns[Piwik_Archive::INDEX_NB_VISITS_CONVERTED])) + { + $nbVisitsConverted = $currentColumns[Piwik_Archive::INDEX_NB_VISITS_CONVERTED]; + $revenue = $currentColumns[Piwik_Archive::INDEX_REVENUE]; + } + + if($nbVisitsConverted == 0) + { + $conversionRate = $invalidDivision; + } + else + { + $conversionRate = round(100 * $nbVisitsConverted / $nbVisits, $roundingPrecision); + } + + if($nbVisits == 0) + { + $revenuePerVisit = $invalidDivision; + } + else + { + $revenuePerVisit = round( $revenue / $nbVisits, $roundingPrecision ); + } + foreach($currentColumns[Piwik_Archive::INDEX_GOALS] as $goalId => $columnValue) + { + $name = 'goal_' . $goalId . '_conversion_rate'; + if($nbVisits == 0) + { + $value = $invalidDivision; + } + else + { + $value = round(100 * $columnValue[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS] / $nbVisits, $roundingPrecision); + } + $newColumns[$name] = $value; + $expectedColumns[$name] = true; + + $name = 'goal_' . $goalId . '_nb_conversions'; + $newColumns[$name] = $columnValue[Piwik_Archive::INDEX_GOAL_NB_CONVERSIONS]; + $expectedColumns[$name] = true; + } + $newColumns['revenue_per_visit'] = $revenuePerVisit; + $newColumns['goals_conversion_rate'] = $conversionRate; + } + + $row->setColumns($newColumns); + } + $expectedColumns['revenue_per_visit'] = true; + $expectedColumns['goals_conversion_rate'] = 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 = $this->table->getRows(); + foreach($rows as &$row) + { + foreach($expectedColumns as $name) + { + if(false === $row->getColumn($name)) + { + $row->addColumn($name, 0); + } + } + } + } +} + diff --git a/core/DataTable/Renderer/Csv.php b/core/DataTable/Renderer/Csv.php index e686ba9023..d311981c51 100644 --- a/core/DataTable/Renderer/Csv.php +++ b/core/DataTable/Renderer/Csv.php @@ -132,11 +132,28 @@ class Piwik_DataTable_Renderer_Csv extends Piwik_DataTable_Renderer $columns = $row->getColumns(); foreach($columns as $name => $value) { - if(!isset($allColumns[$name])) + //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) + { + // goals_idgoal=1 + $columnName = $name . "_" . $key . "_" . $subKey; + $allColumns[$columnName] = true; + $csvRow[$columnName] = $subValue; + } + } + } + } + else { $allColumns[$name] = true; + $csvRow[$name] = $value; } - $csvRow[$name] = $value; } if($this->exportMetadata) diff --git a/core/DataTable/Renderer/Php.php b/core/DataTable/Renderer/Php.php index 5e472d7519..fee43640f1 100644 --- a/core/DataTable/Renderer/Php.php +++ b/core/DataTable/Renderer/Php.php @@ -173,7 +173,7 @@ class Piwik_DataTable_Renderer_Php extends Piwik_DataTable_Renderer 'columns' => $row->getColumns(), 'metadata' => $row->getMetadata(), 'idsubdatatable' => $row->getIdSubDataTable(), - ); + ); if($this->renderSubTables && $row->getIdSubDataTable() !== null) diff --git a/core/DataTable/Renderer/Rss.php b/core/DataTable/Renderer/Rss.php index 18574dfcd5..7f7efa1b23 100644 --- a/core/DataTable/Renderer/Rss.php +++ b/core/DataTable/Renderer/Rss.php @@ -97,7 +97,7 @@ class Piwik_DataTable_Renderer_Rss extends Piwik_DataTable_Renderer $generationDate"; return $header; } - + protected function renderDataTable($table) { @@ -110,7 +110,7 @@ class Piwik_DataTable_Renderer_Rss extends Piwik_DataTable_Renderer { $table->deleteColumn('label'); } - + $i = 1; $tableStructure = array(); @@ -125,6 +125,12 @@ class Piwik_DataTable_Renderer_Rss extends Piwik_DataTable_Renderer { 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; } @@ -146,14 +152,14 @@ class Piwik_DataTable_Renderer_Rss extends Piwik_DataTable_Renderer foreach($tableStructure as $row) { $html .= "\n\n"; - foreach($allColumns as $name => $toDisplay) + foreach($allColumns as $columnName => $toDisplay) { if($toDisplay !== false) { $value = "-"; - if(isset($row[$name])) + if(isset($row[$columnName])) { - $value = urldecode($row[$name]); + $value = urldecode($row[$columnName]); } $html .= "\n\t$value"; diff --git a/core/DataTable/Renderer/Xml.php b/core/DataTable/Renderer/Xml.php index 425f506ae3..1b3a77866d 100644 --- a/core/DataTable/Renderer/Xml.php +++ b/core/DataTable/Renderer/Xml.php @@ -29,6 +29,7 @@ class Piwik_DataTable_Renderer_Xml extends Piwik_DataTable_Renderer function render() { +// var_dump($this->table);exit; return $this->renderTable($this->table); } @@ -41,7 +42,7 @@ class Piwik_DataTable_Renderer_Xml extends Piwik_DataTable_Renderer protected function renderTable($table, $returnOnlyDataTableXml = false, $prefixLines = '') { $array = $this->getArrayFromDataTable($table); - +// var_dump($array);exit; if($table instanceof Piwik_DataTable_Array) { $out = $this->renderDataTableArray($table, $array, $prefixLines); @@ -235,9 +236,21 @@ class Piwik_DataTable_Renderer_Xml extends Piwik_DataTable_Renderer protected function renderDataTable( $array, $prefixLine = "" ) { $out = ''; - foreach($array as $row) + foreach($array as $rowId => $row) { - $out .= $prefixLine."\t"; + if(!is_array($row)) + { + $value = $this->formatValue($row); + $out .= $prefixLine."\t\t<$rowId>".$value."\n"; + continue; + } + $rowAttribute = ''; + if(($equalFound = strstr($rowId, '=')) !== false) + { + $rowAttribute = explode('=', $rowId); + $rowAttribute = " " . $rowAttribute[0] . "='" . $rowAttribute[1] . "'"; + } + $out .= $prefixLine."\t"; if(count($row) === 1 && key($row) === 0) diff --git a/core/DataTable/Row.php b/core/DataTable/Row.php index 9c5edfdd33..abc6aea288 100644 --- a/core/DataTable/Row.php +++ b/core/DataTable/Row.php @@ -329,39 +329,49 @@ class Piwik_DataTable_Row */ public function sumRow( Piwik_DataTable_Row $rowToSum ) { - foreach($rowToSum->getColumns() as $name => $value) + foreach($rowToSum->getColumns() as $columnToSumName => $columnToSumValue) { - if($name != 'label') + if($columnToSumName != 'label') { - if(Piwik::isNumeric($value)) - { - $current = $this->getColumn($name); - if($current === false) - { - $current = 0; - } - $this->setColumn( $name, $current + $value); - } - elseif(is_array($value)) + $thisColumnValue = $this->getColumn($columnToSumName); + $newValue = $this->sumRowArray($thisColumnValue, $columnToSumValue); + $this->setColumn( $columnToSumName, $newValue); + } + } + } + + protected function sumRowArray( $thisColumnValue, $columnToSumValue ) + { + $newValue = 0; + if(Piwik::isNumeric($columnToSumValue)) + { + if($thisColumnValue === false) + { + $thisColumnValue = 0; + } + $newValue = $thisColumnValue + $columnToSumValue; + } + elseif(is_array($columnToSumValue)) + { + $newValue = array(); + if($thisColumnValue == false) + { + $newValue = $columnToSumValue; + } + else + { + $newValue = $thisColumnValue; + foreach($columnToSumValue as $arrayIndex => $arrayValue) { - $current = $this->getColumn($name); - $newValue = array(); - if($current == false) + if(!isset($newValue[$arrayIndex])) { - $newValue = $value; + $newValue[$arrayIndex] = false; } - else - { - $newValue = $current; - foreach($value as $arrayIndex => $arrayValue) - { - $newValue[$arrayIndex] += $arrayValue; - } - } - $this->setColumn($name, $newValue); + $newValue[$arrayIndex] = $this->sumRowArray($newValue[$arrayIndex], $arrayValue); } } } + return $newValue; } /** @@ -381,10 +391,7 @@ class Piwik_DataTable_Row //same columns $cols1 = $row1->getColumns(); $cols2 = $row2->getColumns(); - - uksort($cols1, 'strnatcasecmp'); - uksort($cols2, 'strnatcasecmp'); - if($cols1 != $cols2) + if(array_diff($cols1, $cols2) !== array_diff($cols2, $cols1)) { return false; } diff --git a/core/Date.php b/core/Date.php index 450682df68..efcc526da4 100644 --- a/core/Date.php +++ b/core/Date.php @@ -41,6 +41,18 @@ class Piwik_Date return new Piwik_Date($dateString); } + protected $timestamp = null; + + /** + * Returns the unix timestamp of the date + * + * @return int + */ + public function getTimestamp() + { + return $this->timestamp; + } + /** * Builds a Piwik_Date object * @@ -78,16 +90,6 @@ class Piwik_Date return new Piwik_Date( strtotime( $this->get("j F Y") . " $time")); } - /** - * Returns the unix timestamp of the date - * - * @return int - */ - public function getTimestamp() - { - return $this->timestamp; - } - /** * Returns true if the current date is older than the given $date * diff --git a/core/FrontController.php b/core/FrontController.php index 2644f02938..718f4e997c 100644 --- a/core/FrontController.php +++ b/core/FrontController.php @@ -142,7 +142,6 @@ class Piwik_FrontController { throw new Exception("Action $action not found in the controller $controllerClassName."); } - try { return call_user_func_array( array($controller, $action ), $parameters); } catch(Piwik_Access_NoAccessException $e) { diff --git a/core/Period/Range.php b/core/Period/Range.php index 03c97d3e23..d2cfb8dcf9 100644 --- a/core/Period/Range.php +++ b/core/Period/Range.php @@ -131,7 +131,6 @@ class Piwik_Period_Range extends Piwik_Period { throw new Exception("The date '$this->strDate' is not a date range. Should have the following format: 'lastN' or 'previousN' or 'YYYY-MM-DD,YYYY-MM-DD'."); } - $endSubperiod = Piwik_Period::factory($this->strPeriod, $endDate); $arrayPeriods= array(); diff --git a/core/Piwik.php b/core/Piwik.php index 998d6da08b..9b2aa70801 100644 --- a/core/Piwik.php +++ b/core/Piwik.php @@ -423,7 +423,23 @@ class Piwik static public function isNumeric($value) { - return !is_array($value) && ereg('^([-]{0,1}[0-9]{1,}[.]{0,1}[0-9]*)$', $value); + return is_numeric($value); + } + + static public function getCurrency() + { + static $symbol = null; + if(is_null($symbol)) + { + $symbol = Zend_Registry::get('config')->General->default_currency; + } + return $symbol; + } + + static public function getPrettyMoney($value) + { + $symbol = self::getCurrency(); + return sprintf("$symbol%.2f", $value); } static public function getPrettyTimeFromSeconds($numberOfSeconds) @@ -664,7 +680,8 @@ class Piwik period TINYINT UNSIGNED NULL, ts_archived DATETIME NULL, value FLOAT NULL, - PRIMARY KEY(idarchive, name) + PRIMARY KEY(idarchive, name), + KEY `index_all` (`idsite`,`date1`,`date2`,`name`,`ts_archived`) ) ", 'archive_blob' => "CREATE TABLE {$prefixTables}archive_blob ( @@ -676,7 +693,8 @@ class Piwik period TINYINT UNSIGNED NULL, ts_archived DATETIME NULL, value MEDIUMBLOB NULL, - PRIMARY KEY(idarchive, name) + PRIMARY KEY(idarchive, name), + KEY `index_all` (`idsite`,`date1`,`date2`,`name`,`ts_archived`) ) ", ); @@ -777,6 +795,11 @@ class Piwik Zend_Registry::get('access')->checkUserHasSomeAdminAccess(); } + static public function checkUserHasSomeViewAccess() + { + Zend_Registry::get('access')->checkUserHasSomeViewAccess(); + } + static public function isUserHasViewAccess( $idSites ) { try{ diff --git a/core/Plugin.php b/core/Plugin.php index f606751736..de3e3a7f72 100644 --- a/core/Plugin.php +++ b/core/Plugin.php @@ -70,15 +70,6 @@ abstract class Piwik_Plugin return; } - /** - * Returns the names of the required plugins - * @var array - */ - public function getListRequiredPlugins() - { - return array(); - } - /** * Returns the plugin name * @var string diff --git a/core/SmartyPlugins/function.logoHtml.php b/core/SmartyPlugins/function.logoHtml.php new file mode 100644 index 0000000000..85a57b02e3 --- /dev/null +++ b/core/SmartyPlugins/function.logoHtml.php @@ -0,0 +1,32 @@ +"; +} diff --git a/core/SmartyPlugins/function.url.php b/core/SmartyPlugins/function.url.php index adadea3beb..a837a8459b 100644 --- a/core/SmartyPlugins/function.url.php +++ b/core/SmartyPlugins/function.url.php @@ -1,31 +1,31 @@ - - * {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 - * - * - * @see Piwik_Url::getCurrentQueryStringWithParametersModified() - * @param $name=$value of the parameters to modify in the generated URL - * @return string Something like index.php?module=X&action=Y - */ -function smarty_function_url($params, &$smarty) -{ - return htmlspecialchars(Piwik_Url::getCurrentScriptName() . Piwik_Url::getCurrentQueryStringWithParametersModified( $params )); -} + + * {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 + * + * + * @see Piwik_Url::getCurrentQueryStringWithParametersModified() + * @param $name=$value of the parameters to modify in the generated URL + * @return string Something like index.php?module=X&action=Y + */ +function smarty_function_url($params, &$smarty) +{ + return htmlspecialchars(Piwik_Url::getCurrentScriptName() . Piwik_Url::getCurrentQueryStringWithParametersModified( $params )); +} diff --git a/core/Timer.php b/core/Timer.php index 783dcff6ea..07c2adbd01 100644 --- a/core/Timer.php +++ b/core/Timer.php @@ -29,12 +29,12 @@ class Piwik_Timer $this->memoryStart = $this->getMemoryUsage(); } - public function getTime($decimals = 2) + public function getTime($decimals = 3) { return number_format($this->getMicrotime() - $this->timerStart, $decimals, '.', ''); } - public function getTimeMs($decimals = 2) + public function getTimeMs($decimals = 3) { return number_format(1000*($this->getMicrotime() - $this->timerStart), $decimals, '.', ''); } diff --git a/core/Tracker.php b/core/Tracker.php index 171871efe3..5c3b52e8a3 100644 --- a/core/Tracker.php +++ b/core/Tracker.php @@ -11,29 +11,10 @@ /** * Class used by the logging script piwik.php called by the javascript tag. - * Handles the visitor & his/her actions on the website, saves the data in the DB, saves information in the cookie, etc. + * Handles the visitor & his/her actions on the website, saves the data in the DB, + * saves information in the cookie, etc. * - * To maximise the performance of the logging module, we use different techniques. - * - * On the PHP-only side: - * - minimize the number of external files included. - * Ideally only one (the configuration file) in all the normal cases. - * We load the Loggers only when an error occurs ; this error is logged in the DB/File/etc - * depending on the loggers settings in the configuration file. - * - we may have to include external classes but we try to include only very - * simple code without any dependency, so that we could simply write a script - * that would merge all this simple code into a big piwik.php file. - * - * On the Database-related side: - * - write all the SQL queries without using any DB abstraction layer. Manual filtering of input values. - * - minimize the number of SQL queries necessary to complete the algorithm. - * - carefully index the tables used - * - try to have fixed length rows - * - * [ - use a partitionning by date for the tables ] - * - * Configuration options for the statsLogEngine module: - * - use_cookie ; defines if we try to get/set a cookie to help recognize a unique visitor + * We try to include as little files as possible (no dependency on 3rd party modules). * * @package Piwik_Tracker */ @@ -57,6 +38,12 @@ class Piwik_Tracker const COOKIE_INDEX_TIMESTAMP_FIRST_ACTION = 3; const COOKIE_INDEX_ID_VISIT = 4; const COOKIE_INDEX_ID_LAST_ACTION = 5; + const COOKIE_INDEX_REFERER_ID_VISIT = 6; + const COOKIE_INDEX_REFERER_TIMESTAMP = 7; + const COOKIE_INDEX_REFERER_TYPE = 8; + const COOKIE_INDEX_REFERER_NAME = 9; + const COOKIE_INDEX_REFERER_KEYWORD = 10; + const COOKIE_INDEX_VISITOR_RETURNING = 11; public function __construct() {} @@ -68,15 +55,27 @@ class Piwik_Tracker { try { self::connectDatabase(); + $visit = $this->getNewVisitObject(); $visit->handle(); + unset($visit); } catch (PDOException $e) { $this->setState(self::STATE_LOGGING_DISABLE); - } + } } $this->end(); } + /** + * Returns the date in the "Y-m-d H:i:s" PHP format + * @return string + */ + public static function getDatetimeFromTimestamp($timestamp) + { + return date("Y-m-d H:i:s", $timestamp); + } + + protected function init() { $this->loadTrackerPlugins(); @@ -155,7 +154,10 @@ class Piwik_Tracker self::$db = $db; } - public static function getDb() + /** + * @return Piwik_Tracker_Db + */ + public static function getDatabase() { return self::$db; } @@ -188,7 +190,6 @@ class Piwik_Tracker throw new Exception("The Visit object set in the plugin must implement Piwik_Tracker_Visit_Interface"); } - $visit->setDb(self::$db); return $visit; } diff --git a/core/Tracker/Action.php b/core/Tracker/Action.php index a50baa06b4..970f2c0189 100644 --- a/core/Tracker/Action.php +++ b/core/Tracker/Action.php @@ -16,7 +16,7 @@ * @package Piwik_Tracker */ interface Piwik_Tracker_Action_Interface { - public function getActionId(); + public function getIdAction(); public function record( $idVisit, $idRefererAction, $timeSpentRefererAction ); public function setIdSite( $idSite ); } @@ -45,11 +45,12 @@ interface Piwik_Tracker_Action_Interface { */ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface { - private $actionName; private $url; - private $defaultActionName; - private $nameDownloadOutlink; private $idSite; + private $idLinkVisitAction; + private $finalActionName; + private $actionType; + private $idAction = null; /** * 3 types of action, Standard action / Download / Outlink click @@ -58,28 +59,20 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface const TYPE_DOWNLOAD = 3; const TYPE_OUTLINK = 2; + protected function getDefaultActionName() + { + return Piwik_Tracker_Config::getInstance()->Tracker['default_action_name']; + } + /** - * @param Piwik_Tracker_Db Database object to be used + * Returns URL of the page currently being tracked, or the file being downloaded, or the outlink being clicked + * @return string */ - function __construct( $db ) + public function getUrl() { - $this->actionName = Piwik_Common::getRequestVar( 'action_name', '', 'string'); - - $downloadVariableName = Piwik_Tracker_Config::getInstance()->Tracker['download_url_var_name']; - $this->downloadUrl = Piwik_Common::getRequestVar( $downloadVariableName, '', 'string'); - - $outlinkVariableName = Piwik_Tracker_Config::getInstance()->Tracker['outlink_url_var_name']; - $this->outlinkUrl = Piwik_Common::getRequestVar( $outlinkVariableName, '', 'string'); - - $nameVariableName = Piwik_Tracker_Config::getInstance()->Tracker['download_outlink_name_var']; - $this->nameDownloadOutlink = Piwik_Common::getRequestVar( $nameVariableName, '', 'string'); - - $this->url = Piwik_Common::getRequestVar( 'url', '', 'string'); - $this->db = $db; - $this->defaultActionName = Piwik_Tracker_Config::getInstance()->Tracker['default_action_name']; + return $this->url; } - /** * Returns the idaction of the current action name. * This idaction is used in the visitor logging table to link the visit information @@ -91,9 +84,12 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface * * @return int Id action that is associated to this action name in the Actions table lookup */ - function getActionId() + function getIdAction() { - $this->loadActionId(); + if(is_null($this->idAction)) + { + $this->idAction = $this->loadActionId(); + } return $this->idAction; } @@ -116,16 +112,16 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface */ public function record( $idVisit, $idRefererAction, $timeSpentRefererAction) { - $this->db->query("/* SHARDING_ID_SITE = ".$this->idSite." */ INSERT INTO ".$this->db->prefixTable('log_link_visit_action') + Piwik_Tracker::getDatabase()->query("/* SHARDING_ID_SITE = ".$this->idSite." */ INSERT INTO ".Piwik_Tracker::getDatabase()->prefixTable('log_link_visit_action') ." (idvisit, idaction, idaction_ref, time_spent_ref_action) VALUES (?,?,?,?)", - array($idVisit, $this->idAction, $idRefererAction, $timeSpentRefererAction) + array($idVisit, $this->getIdAction(), $idRefererAction, $timeSpentRefererAction) ); - $idLinkVisitAction = $this->db->lastInsertId(); + $this->idLinkVisitAction = Piwik_Tracker::getDatabase()->lastInsertId(); $info = array( 'idSite' => $this->idSite, - 'idLinkVistAction' => $idLinkVisitAction, + 'idLinkVisitAction' => $this->idLinkVisitAction, 'idVisit' => $idVisit, 'idRefererAction' => $idRefererAction, 'timeSpentRefererAction' => $timeSpentRefererAction, @@ -137,6 +133,16 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface Piwik_PostEvent('Tracker.Action.record', $this, $info); } + /** + * 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->finalActionName @@ -146,34 +152,44 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface private function generateInfo() { $actionName = ''; - if(!empty($this->downloadUrl)) + + $downloadVariableName = Piwik_Tracker_Config::getInstance()->Tracker['download_url_var_name']; + $downloadUrl = Piwik_Common::getRequestVar( $downloadVariableName, '', 'string'); + + if(!empty($downloadUrl)) { $this->actionType = self::TYPE_DOWNLOAD; - $url = $this->downloadUrl; - //$actionName = $this->nameDownloadOutlink; + $url = $downloadUrl; $actionName = $url; } - elseif(!empty($this->outlinkUrl)) + else { - $this->actionType = self::TYPE_OUTLINK; - $url = $this->outlinkUrl; - //remove the last '/' character if it's present - if(substr($url,-1) == '/') + $outlinkVariableName = Piwik_Tracker_Config::getInstance()->Tracker['outlink_url_var_name']; + $outlinkUrl = Piwik_Common::getRequestVar( $outlinkVariableName, '', 'string'); + if(!empty($outlinkUrl)) { - $url = substr($url,0,-1); + $this->actionType = self::TYPE_OUTLINK; + $url = $outlinkUrl; + //remove the last '/' character if it's present + if(substr($url,-1) == '/') + { + $url = substr($url,0,-1); + } + $nameVariableName = Piwik_Tracker_Config::getInstance()->Tracker['download_outlink_name_var']; + $actionName = Piwik_Common::getRequestVar( $nameVariableName, '', 'string'); + if( empty($actionName) ) + { + $actionName = $url; + } } - $actionName = $this->nameDownloadOutlink; - if( empty($actionName) ) + else { - $actionName = $url; + $this->actionType = self::TYPE_ACTION; + $url = Piwik_Common::getRequestVar( 'url', '', 'string'); + $actionName = Piwik_Common::getRequestVar( 'action_name', '', 'string'); } } - else - { - $this->actionType = self::TYPE_ACTION; - $url = $this->url; - $actionName = $this->actionName; - } + $this->url = $url; // the ActionName wasn't specified if( empty($actionName) ) @@ -188,7 +204,7 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface && $actionName[strlen($actionName)-1] == '/' ) { - $actionName.=$this->defaultActionName; + $actionName .= $this->getDefaultActionName(); } } @@ -224,7 +240,7 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface if(empty($actionName)) { - $actionName = $this->defaultActionName; + $actionName = $this->getDefaultActionName(); } $this->finalActionName = $actionName; @@ -233,17 +249,17 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface /** * Sets the attribute $idAction based on $finalActionName and $actionType. * - * @see getActionId() + * @see getIdAction() */ private function loadActionId() - { + { $this->generateInfo(); $name = $this->finalActionName; $type = $this->actionType; - $idAction = $this->db->fetch("/* SHARDING_ID_SITE = ".$this->idSite." */ SELECT idaction - FROM ".$this->db->prefixTable('log_action') + $idAction = Piwik_Tracker::getDatabase()->fetch("/* SHARDING_ID_SITE = ".$this->idSite." */ SELECT idaction + FROM ".Piwik_Tracker::getDatabase()->prefixTable('log_action') ." WHERE name = ? AND type = ?", array($name, $type) ); @@ -251,16 +267,18 @@ class Piwik_Tracker_Action implements Piwik_Tracker_Action_Interface // the action name has not been found, create it if($idAction === false) { - $this->db->query("/* SHARDING_ID_SITE = ".$this->idSite." */ INSERT INTO ". $this->db->prefixTable('log_action'). "( name, type ) - VALUES (?,?)",array($name,$type) ); - $idAction = $this->db->lastInsertId(); + Piwik_Tracker::getDatabase()->query("/* SHARDING_ID_SITE = ".$this->idSite." */ + INSERT INTO ". Piwik_Tracker::getDatabase()->prefixTable('log_action'). "( name, type ) + VALUES (?,?)", + array($name,$type) + ); + $idAction = Piwik_Tracker::getDatabase()->lastInsertId(); } else { $idAction = $idAction['idaction']; } - - $this->idAction = $idAction; + return $idAction; } } diff --git a/core/Tracker/Generator.php b/core/Tracker/Generator.php index 0979b02fea..8a11a7028c 100644 --- a/core/Tracker/Generator.php +++ b/core/Tracker/Generator.php @@ -355,7 +355,6 @@ class Piwik_Tracker_Generator for($i = 0; $i < $nbVisitors; $i++) { $nbActions = mt_rand(1, $nbActionsMaxPerVisit); - Piwik_Tracker_Generator_Visit::setTimestampToUse($this->getTimestampToUse()); $this->generateNewVisit(); @@ -364,7 +363,6 @@ class Piwik_Tracker_Generator $this->generateActionVisit(); $this->saveVisit(); } - $nbActionsTotal += $nbActions; } return $nbActionsTotal; @@ -655,6 +653,7 @@ class Piwik_Tracker_Generator $this->setFakeRequest(); $process = new Piwik_Tracker_Generator_Tracker; $process->main(); + unset($process); } } diff --git a/core/Tracker/Generator/Tracker.php b/core/Tracker/Generator/Tracker.php index d4457c33fc..4344e2a515 100644 --- a/core/Tracker/Generator/Tracker.php +++ b/core/Tracker/Generator/Tracker.php @@ -47,7 +47,6 @@ class Piwik_Tracker_Generator_Tracker extends Piwik_Tracker protected function getNewVisitObject() { $visit = new Piwik_Tracker_Generator_Visit(); - $visit->setDb(self::$db); return $visit; } diff --git a/core/Tracker/Generator/Visit.php b/core/Tracker/Generator/Visit.php index 41535f045c..533b0963f5 100644 --- a/core/Tracker/Generator/Visit.php +++ b/core/Tracker/Generator/Visit.php @@ -37,11 +37,6 @@ class Piwik_Tracker_Generator_Visit extends Piwik_Tracker_Visit self::$timestampToUse += mt_rand(4,1840); return self::$timestampToUse; } - - protected function getDatetimeFromTimestamp($timestamp) - { - return date("Y-m-d H:i:s",$timestamp); - } protected function updateCookie() { @@ -49,3 +44,4 @@ class Piwik_Tracker_Generator_Visit extends Piwik_Tracker_Visit } } + diff --git a/core/Tracker/GoalManager.php b/core/Tracker/GoalManager.php new file mode 100644 index 0000000000..477339ad0e --- /dev/null +++ b/core/Tracker/GoalManager.php @@ -0,0 +1,147 @@ +action = $action; + + + } + + function setCookie($cookie) + { + $this->cookie = $cookie; + } + + //TODO goalid should be incrementing on a website basis + // load goal definitions from file + static public function getGoalDefinitions() + { + return array( + 0 => array( 'id' => 1, + 'name' => 'Downloads', + 'default_revenue' => '3', + 'pattern' => '/e/' + ), + 1 => array( 'id' => 5, + 'name' => 'hosted signups', + 'default_revenue' => false, + 'pattern' => '//' + ), + ); + } + + static public function getGoalDefinition( $idGoal ) + { + $goals = self::getGoalDefinitions(); + foreach($goals as $goal) + { + if($goal['id'] == $idGoal) + { + return $goal; + } + } + throw new Exception("The goal id = $idGoal couldn't be found."); + } + + static public function getGoalIds() + { + $goals = self::getGoalDefinitions(); + $goalIds = array(); + foreach($goals as $goal) + { + $goalIds[] = $goal['id']; + } + return $goalIds; + } + + //TODO does this code work for manually triggered goals, with custom revenue? + function detectGoals($idSite) + { + $url = $this->action->getUrl(); + $goals = $this->getGoalDefinitions($idSite); + foreach($goals as $goal) + { + $match = preg_match($goal['pattern'], $url); + if($match === 1) + { + $this->matchedGoals[] = $goal; + } + } + return count($this->matchedGoals) > 0; + } + + function recordGoals($visitorInformation) + { + $location_country = isset($visitorInformation['location_country']) ? $visitorInformation['location_country'] : Piwik_Common::getCountry(Piwik_Common::getBrowserLanguage()); + $location_continent = isset($visitorInformation['location_continent']) ? $visitorInformation['location_continent'] : Piwik_Common::getContinent($location_country); + + $goal = array( + 'idvisit' => $visitorInformation['idvisit'], + 'idsite' => $visitorInformation['idsite'], + 'visitor_idcookie' => $visitorInformation['visitor_idcookie'], + 'server_time' => Piwik_Tracker::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time']), + 'visit_server_date' => $visitorInformation['visit_server_date'], + 'idaction' => $this->action->getIdAction(), + 'idlink_va' => $this->action->getIdLinkVisitAction(), + 'location_country' => $location_country, + 'location_continent'=> $location_continent, + 'url' => $this->action->getUrl(), + 'visitor_returning' => $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_VISITOR_RETURNING ), + ); + + $referer_idvisit = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_REFERER_ID_VISIT ); + if($referer_idvisit !== false) + { + $goal += array( + 'referer_idvisit' => $referer_idvisit, + 'referer_visit_server_date' => date("Y-m-d", $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_REFERER_TIMESTAMP )), + 'referer_type' => htmlspecialchars_decode($this->cookie->get( Piwik_Tracker::COOKIE_INDEX_REFERER_TYPE )), + 'referer_name' => htmlspecialchars_decode($this->cookie->get( Piwik_Tracker::COOKIE_INDEX_REFERER_NAME )), + 'referer_keyword' => htmlspecialchars_decode($this->cookie->get( Piwik_Tracker::COOKIE_INDEX_REFERER_KEYWORD )), + ); + } + + foreach($this->matchedGoals as $matchedGoal) + { + printDebug("- Goal ".$matchedGoal['id'] ." matched. Recording..."); + $newGoal = $goal; + $newGoal['idgoal'] = $matchedGoal['id']; + $newGoal['revenue'] = $matchedGoal['default_revenue']; + printDebug($newGoal); + + $fields = implode(", ", array_keys($newGoal)); + $bindFields = substr(str_repeat( "?,",count($newGoal)),0,-1); + + try { + Piwik_Tracker::getDatabase()->query( + "INSERT INTO " . Piwik_Tracker::getDatabase()->prefixTable('log_conversion') . " ($fields) + VALUES ($bindFields) ", array_values($newGoal) + ); + } catch( Exception $e) { + if(strpos($e->getMessage(), '1062') !== false) + { + // integrity violation when same visit converts to the same goal twice + printDebug("--> Goal already recorded for this (idvisit, idgoal)"); + } + else + { + throw $e; + } + } + //$idlog_goal = Piwik_Tracker::getDatabase()->lastInsertId(); + } + } +} \ No newline at end of file diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php index d7bcc8832d..087bb1a0fc 100644 --- a/core/Tracker/Visit.php +++ b/core/Tracker/Visit.php @@ -12,7 +12,6 @@ interface Piwik_Tracker_Visit_Interface { function handle(); - function setDb($db); } /** @@ -31,11 +30,14 @@ interface Piwik_Tracker_Visit_Interface { class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface { - protected $cookieLog = null; + /** + * @var Piwik_Cookie + */ + protected $cookie = null; protected $visitorInfo = array(); protected $userSettingsInformation = null; - protected $db = null; protected $idsite; + protected $visitorKnown; function __construct() { @@ -48,9 +50,203 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $this->idsite = $idsite; } - public function setDb($db) + /** + * Main algorith to handle the visit. + * + * Once we have the visitor information, we have to define 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() + { + if($this->isExcluded()) + { + return; + } + + // current action + $action = $this->newAction(); + $actionId = $action->getIdAction(); + + // goal matched? + $goalManager = new Piwik_Tracker_GoalManager( $action ); + $someGoalsConverted = false; + if($goalManager->detectGoals($this->idsite)) + { + $someGoalsConverted = true; + } + + // the visitor and session + $this->recognizeTheVisitor(); + if( $this->isVisitorKnown() + && $this->isLastActionInTheSameVisit()) + { + $this->handleKnownVisit($actionId, $someGoalsConverted); + $action->record( $this->visitorInfo['idvisit'], + $this->visitorInfo['visit_exit_idaction'], + $this->visitorInfo['time_spent_ref_action'] + ); + } + else + { + $this->handleNewVisit($actionId, $someGoalsConverted); + $action->record( $this->visitorInfo['idvisit'], 0, 0 ); + } + + // update the cookie with the new visit information + $this->updateCookie(); + + // record the goals if applicable + if($someGoalsConverted) + { + $goalManager->setCookie($this->cookie); + $goalManager->recordGoals($this->visitorInfo); + } + unset($goalManager); + unset($action); + } + + + /** + * In the case of a known visit, we have to do the following actions: + * + * 1) Insert the new action + * + * 2) Update the visit information + */ + protected function handleKnownVisit($actionId, $someGoalsConverted) + { + printDebug("Visit known."); + $serverTime = $this->getCurrentTimestamp(); + $datetimeServer = Piwik_Tracker::getDatetimeFromTimestamp($serverTime); + + $sqlUpdateGoalConverted = ''; + if($someGoalsConverted) + { + $sqlUpdateGoalConverted = " visit_goal_converted = 1,"; + } + + Piwik_Tracker::getDatabase()->query("/* SHARDING_ID_SITE = ". $this->idsite ." */ + UPDATE ". Piwik_Tracker::getDatabase()->prefixTable('log_visit')." + SET visit_last_action_time = ?, + visit_exit_idaction = ?, + visit_total_actions = visit_total_actions + 1, + $sqlUpdateGoalConverted + visit_total_time = UNIX_TIMESTAMP(visit_last_action_time) - UNIX_TIMESTAMP(visit_first_action_time) + WHERE idvisit = ? + LIMIT 1", + array( $datetimeServer, + $actionId, + $this->visitorInfo['idvisit'] ) + ); + $this->visitorInfo['idsite'] = $this->idsite; + $this->visitorInfo['visit_server_date'] = $this->getCurrentDate(); + + // will be updated in cookie + $this->visitorInfo['visit_last_action_time'] = $serverTime; + $this->visitorInfo['visit_exit_idaction'] = $actionId; + $this->visitorInfo['time_spent_ref_action'] = $serverTime - $this->visitorInfo['visit_last_action_time']; + } + + /** + * In the case of a new visit, we have to do the following actions: + * + * 1) Insert the new action + * + * 2) Insert the visit information + */ + protected function handleNewVisit($actionId, $someGoalsConverted) { - $this->db = $db; + printDebug("New Visit."); + + $localTime = Piwik_Common::getRequestVar( 'h', $this->getCurrentDate("H"), 'numeric') + .':'. Piwik_Common::getRequestVar( 'm', $this->getCurrentDate("i"), 'numeric') + .':'. Piwik_Common::getRequestVar( 's', $this->getCurrentDate("s"), 'numeric'); + $serverTime = $this->getCurrentTimestamp(); + $serverDate = $this->getCurrentDate(); + + if($this->isVisitorKnown()) + { + $idcookie = $this->visitorInfo['visitor_idcookie']; + $returningVisitor = 1; + } + else + { + $idcookie = $this->getVisitorUniqueId(); + $returningVisitor = 0; + } + + $defaultTimeOnePageVisit = Piwik_Tracker_Config::getInstance()->Tracker['default_time_one_page_visit']; + + $userInfo = $this->getUserSettingsInformation(); + $country = Piwik_Common::getCountry($userInfo['location_browser_lang']); + $refererInfo = $this->getRefererInformation(); + + /** + * Save the visitor + */ + $this->visitorInfo = array( + 'idsite' => $this->idsite, + 'visitor_localtime' => $localTime, + 'visitor_idcookie' => $idcookie, + 'visitor_returning' => $returningVisitor, + 'visit_first_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($serverTime), + 'visit_last_action_time' => Piwik_Tracker::getDatetimeFromTimestamp($serverTime), + 'visit_server_date' => $serverDate, + 'visit_entry_idaction' => $actionId, + 'visit_exit_idaction' => $actionId, + 'visit_total_actions' => 1, + 'visit_total_time' => $defaultTimeOnePageVisit, + 'visit_goal_converted' => $someGoalsConverted ? 1: 0, + 'referer_type' => $refererInfo['referer_type'], + 'referer_name' => $refererInfo['referer_name'], + 'referer_url' => $refererInfo['referer_url'], + 'referer_keyword' => $refererInfo['referer_keyword'], + 'config_md5config' => $userInfo['config_md5config'], + 'config_os' => $userInfo['config_os'], + 'config_browser_name' => $userInfo['config_browser_name'], + 'config_browser_version' => $userInfo['config_browser_version'], + 'config_resolution' => $userInfo['config_resolution'], + 'config_pdf' => $userInfo['config_pdf'], + 'config_flash' => $userInfo['config_flash'], + 'config_java' => $userInfo['config_java'], + 'config_director' => $userInfo['config_director'], + 'config_quicktime' => $userInfo['config_quicktime'], + 'config_realplayer' => $userInfo['config_realplayer'], + 'config_windowsmedia' => $userInfo['config_windowsmedia'], + 'config_cookie' => $userInfo['config_cookie'], + 'location_ip' => $userInfo['location_ip'], + 'location_browser_lang' => $userInfo['location_browser_lang'], + 'location_country' => $country + ); + + Piwik_PostEvent('Tracker.newVisitorInformation', $this->visitorInfo); + $this->visitorInfo['location_continent'] = Piwik_Common::getContinent( $this->visitorInfo['location_country'] ); + + $fields = implode(", ", array_keys($this->visitorInfo)); + $values = substr(str_repeat( "?,",count($this->visitorInfo)),0,-1); + + Piwik_Tracker::getDatabase()->query( "INSERT INTO ".Piwik_Tracker::getDatabase()->prefixTable('log_visit'). + " ($fields) VALUES ($values)", array_values($this->visitorInfo)); + + $idVisit = Piwik_Tracker::getDatabase()->lastInsertId(); + + $this->visitorInfo['idvisit'] = $idVisit; + $this->visitorInfo['visit_first_action_time'] = $serverTime; + $this->visitorInfo['visit_last_action_time'] = $serverTime; } /** @@ -70,15 +266,6 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface { return time(); } - - /** - * Returns the date in the "Y-m-d H:i:s" PHP format - * @return string - */ - protected function getDatetimeFromTimestamp($timestamp) - { - return date("Y-m-d H:i:s", $timestamp); - } /** * Test if the current visitor is excluded from the statistics. @@ -144,19 +331,18 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface protected function recognizeTheVisitor() { $this->visitorKnown = false; + $this->cookie = new Piwik_Cookie( $this->getCookieName() ); - $this->cookieLog = new Piwik_Cookie( $this->getCookieName() ); /* * Case the visitor has the piwik cookie. * We make sure all the data that should saved in the cookie is available. */ - - if( false !== ($idVisitor = $this->cookieLog->get( Piwik_Tracker::COOKIE_INDEX_IDVISITOR )) ) + if( false !== ($idVisitor = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_IDVISITOR )) ) { - $timestampLastAction = $this->cookieLog->get( Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_LAST_ACTION ); - $timestampFirstAction = $this->cookieLog->get( Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_FIRST_ACTION ); - $idVisit = $this->cookieLog->get( Piwik_Tracker::COOKIE_INDEX_ID_VISIT ); - $idLastAction = $this->cookieLog->get( Piwik_Tracker::COOKIE_INDEX_ID_LAST_ACTION ); + $timestampLastAction = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_LAST_ACTION ); + $timestampFirstAction = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_FIRST_ACTION ); + $idVisit = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_ID_VISIT ); + $idLastAction = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_ID_LAST_ACTION ); if( $timestampLastAction !== false && is_numeric($timestampLastAction) && $timestampFirstAction !== false && is_numeric($timestampFirstAction) @@ -177,21 +363,22 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface } /* - * If the visitor doesn't have the piwik cookie, we look for a visitor that has exactly the same configuration - * and that visited the website today. + * If the visitor doesn't have the piwik cookie, we look for a visitor + * that has exactly the same configuration and that visited the website today. */ - if( !$this->visitorKnown ) + if( !$this->visitorKnown + && Piwik_Tracker_Config::getInstance()->Tracker['enable_detect_unique_visitor_using_settings']) { $userInfo = $this->getUserSettingsInformation(); $md5Config = $userInfo['config_md5config']; - $visitRow = $this->db->fetch( + $visitRow = Piwik_Tracker::getDatabase()->fetch( " SELECT visitor_idcookie, UNIX_TIMESTAMP(visit_last_action_time) as visit_last_action_time, UNIX_TIMESTAMP(visit_first_action_time) as visit_first_action_time, idvisit, visit_exit_idaction - FROM ".$this->db->prefixTable('log_visit'). + FROM ".Piwik_Tracker::getDatabase()->prefixTable('log_visit'). " WHERE visit_server_date = ? AND idsite = ? AND config_md5config = ? @@ -226,8 +413,6 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface { return $this->userSettingsInformation; } - - $plugin_Flash = Piwik_Common::getRequestVar( 'fla', 0, 'int'); $plugin_Director = Piwik_Common::getRequestVar( 'dir', 0, 'int'); $plugin_Quicktime = Piwik_Common::getRequestVar( 'qt', 0, 'int'); @@ -249,7 +434,7 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface $ip = Piwik_Common::getIp(); $ip = ip2long($ip); - $browserLang = substr(Piwik_Common::getBrowserLanguage(), 0, 20); + $browserLang = Piwik_Common::getBrowserLanguage(); $configurationHash = $this->getConfigHash( $os, @@ -305,47 +490,6 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface return $this->visitorKnown === true; } - /** - * Main algorith to handle the visit. - * - * Once we have the visitor information, we have to define 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() - { - if($this->isExcluded()) - { - return; - } - - $this->recognizeTheVisitor(); - if( $this->isVisitorKnown() - && $this->isLastActionInTheSameVisit()) - { - $this->handleKnownVisit(); - } - else - { - $this->handleNewVisit(); - } - - // we update the cookie with the new visit information - $this->updateCookie(); - } /** * Update the cookie information. @@ -354,190 +498,50 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface { printDebug("We manage the cookie..."); + if( isset($this->visitorInfo['referer_type']) + && $this->visitorInfo['referer_type'] != Piwik_Common::REFERER_TYPE_DIRECT_ENTRY) + { + // if the setting is set to use only the first referer, + // we don't update the cookie referer values if they are already set + if( !Piwik_Tracker_Config::getInstance()->Tracker['use_first_referer_to_determine_goal_referer'] + || $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_REFERER_TYPE ) == false) + { + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_REFERER_TYPE, $this->visitorInfo['referer_type']); + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_REFERER_NAME, $this->visitorInfo['referer_name']); + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_REFERER_KEYWORD, $this->visitorInfo['referer_keyword']); + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_REFERER_ID_VISIT, $this->visitorInfo['idvisit']); + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_REFERER_TIMESTAMP, $this->getCurrentTimestamp()) ; + } + } + // idcookie has been generated in handleNewVisit or we simply propagate the old value - $this->cookieLog->set( Piwik_Tracker::COOKIE_INDEX_IDVISITOR, + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_IDVISITOR, $this->visitorInfo['visitor_idcookie'] ); // the last action timestamp is the current timestamp - $this->cookieLog->set( Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_LAST_ACTION, + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_LAST_ACTION, $this->visitorInfo['visit_last_action_time'] ); // the first action timestamp is the timestamp of the first action of the current visit - $this->cookieLog->set( Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_FIRST_ACTION, + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_FIRST_ACTION, $this->visitorInfo['visit_first_action_time'] ); // the idvisit has been generated by mysql in handleNewVisit or simply propagated here - $this->cookieLog->set( Piwik_Tracker::COOKIE_INDEX_ID_VISIT, + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_ID_VISIT, $this->visitorInfo['idvisit'] ); // the last action ID is the current exit idaction - $this->cookieLog->set( Piwik_Tracker::COOKIE_INDEX_ID_LAST_ACTION, + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_ID_LAST_ACTION, $this->visitorInfo['visit_exit_idaction'] ); - - $this->cookieLog->save(); - } - - - /** - * In the case of a known visit, we have to do the following actions: - * - * 1) Insert the new action - * - * 2) Update the visit information - */ - protected function handleKnownVisit() - { - printDebug("Visit known."); - - /** - * Init the action - */ - $action = $this->getActionObject(); - $actionId = $action->getActionId(); - printDebug("idAction = $actionId"); - - $serverTime = $this->getCurrentTimestamp(); - $datetimeServer = $this->getDatetimeFromTimestamp($serverTime); - - $this->db->query("/* SHARDING_ID_SITE = ". $this->idsite ." */ - UPDATE ". $this->db->prefixTable('log_visit')." - SET visit_last_action_time = ?, - visit_exit_idaction = ?, - visit_total_actions = visit_total_actions + 1, - visit_total_time = UNIX_TIMESTAMP(visit_last_action_time) - UNIX_TIMESTAMP(visit_first_action_time) - WHERE idvisit = ? - LIMIT 1", - array( $datetimeServer, - $actionId, - $this->visitorInfo['idvisit'] ) - ); - /** - * Save the action - */ - $timespentLastAction = $serverTime - $this->visitorInfo['visit_last_action_time']; - - $action->record( $this->visitorInfo['idvisit'], - $this->visitorInfo['visit_exit_idaction'], - $timespentLastAction - ); - - - /** - * Cookie fields to be updated - */ - $this->visitorInfo['visit_last_action_time'] = $serverTime; - $this->visitorInfo['visit_exit_idaction'] = $actionId; - - } - - /** - * In the case of a new visit, we have to do the following actions: - * - * 1) Insert the new action - * - * 2) Insert the visit information - */ - protected function handleNewVisit() - { - printDebug("New Visit."); - - /** - * Get the variables from the REQUEST - */ - $localTime = Piwik_Common::getRequestVar( 'h', $this->getCurrentDate("H"), 'numeric') - .':'. Piwik_Common::getRequestVar( 'm', $this->getCurrentDate("i"), 'numeric') - .':'. Piwik_Common::getRequestVar( 's', $this->getCurrentDate("s"), 'numeric'); - - $serverTime = $this->getCurrentTimestamp(); - $serverDate = $this->getCurrentDate(); - - if($this->isVisitorKnown()) - { - $idcookie = $this->visitorInfo['visitor_idcookie']; - $returningVisitor = 1; - } - else + // for a new visit, we flag the visit with visitor_returning + if(isset($this->visitorInfo['visitor_returning'])) { - $idcookie = $this->getVisitorUniqueId(); - $returningVisitor = 0; + $this->cookie->set( Piwik_Tracker::COOKIE_INDEX_VISITOR_RETURNING, + $this->visitorInfo['visitor_returning'] ); } - $defaultTimeOnePageVisit = Piwik_Tracker_Config::getInstance()->Tracker['default_time_one_page_visit']; - - $userInfo = $this->getUserSettingsInformation(); - $country = Piwik_Common::getCountry($userInfo['location_browser_lang']); - - $refererInfo = $this->getRefererInformation(); - - /** - * Init the action - */ - $action = $this->getActionObject(); - $actionId = $action->getActionId(); - - printDebug("idAction = $actionId"); - - - /** - * Save the visitor - */ - $informationToSave = array( - 'idsite' => $this->idsite, - 'visitor_localtime' => $localTime, - 'visitor_idcookie' => $idcookie, - 'visitor_returning' => $returningVisitor, - 'visit_first_action_time' => $this->getDatetimeFromTimestamp($serverTime), - 'visit_last_action_time' => $this->getDatetimeFromTimestamp($serverTime), - 'visit_server_date' => $serverDate, - 'visit_entry_idaction' => $actionId, - 'visit_exit_idaction' => $actionId, - 'visit_total_actions' => 1, - 'visit_total_time' => $defaultTimeOnePageVisit, - 'referer_type' => $refererInfo['referer_type'], - 'referer_name' => $refererInfo['referer_name'], - 'referer_url' => $refererInfo['referer_url'], - 'referer_keyword' => $refererInfo['referer_keyword'], - 'config_md5config' => $userInfo['config_md5config'], - 'config_os' => $userInfo['config_os'], - 'config_browser_name' => $userInfo['config_browser_name'], - 'config_browser_version' => $userInfo['config_browser_version'], - 'config_resolution' => $userInfo['config_resolution'], - 'config_pdf' => $userInfo['config_pdf'], - 'config_flash' => $userInfo['config_flash'], - 'config_java' => $userInfo['config_java'], - 'config_director' => $userInfo['config_director'], - 'config_quicktime' => $userInfo['config_quicktime'], - 'config_realplayer' => $userInfo['config_realplayer'], - 'config_windowsmedia' => $userInfo['config_windowsmedia'], - 'config_cookie' => $userInfo['config_cookie'], - 'location_ip' => $userInfo['location_ip'], - 'location_browser_lang' => $userInfo['location_browser_lang'], - 'location_country' => $country - ); - - Piwik_PostEvent('Tracker.newVisitorInformation', $informationToSave); - $informationToSave['location_continent'] = Piwik_Common::getContinent( $informationToSave['location_country'] ); - - $fields = implode(", ", array_keys($informationToSave)); - $values = substr(str_repeat( "?,",count($informationToSave)),0,-1); - - $this->db->query( "INSERT INTO ".$this->db->prefixTable('log_visit'). - " ($fields) VALUES ($values)", array_values($informationToSave)); - - $idVisit = $this->db->lastInsertId(); - - // Update the visitor information attribute with this information array - $this->visitorInfo = $informationToSave; - $this->visitorInfo['idvisit'] = $idVisit; - - // we have to save timestamp in the object properties, whereas mysql eats some other datetime format - $this->visitorInfo['visit_first_action_time'] = $serverTime; - $this->visitorInfo['visit_last_action_time'] = $serverTime; - - // saves the action - $action->record( $idVisit, 0, 0 ); - + $this->cookie->save(); } /** @@ -546,14 +550,14 @@ class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface * * @return Piwik_Tracker_Action child or fake but with same public interface */ - protected function getActionObject() + protected function newAction() { $action = null; Piwik_PostEvent('Tracker.newAction', $action); if(is_null($action)) { - $action = new Piwik_Tracker_Action( $this->db ); + $action = new Piwik_Tracker_Action(); } elseif(!($action instanceof Piwik_Tracker_Action_Interface)) { diff --git a/core/Updates/0.2.28.php b/core/Updates/0.2.28.php new file mode 100644 index 0000000000..a64b2b2a72 --- /dev/null +++ b/core/Updates/0.2.28.php @@ -0,0 +1,9 @@ +template = $templateFile; $this->smarty = new Piwik_Smarty(); diff --git a/core/ViewDataTable.php b/core/ViewDataTable.php index c91a200e00..cc767a1337 100644 --- a/core/ViewDataTable.php +++ b/core/ViewDataTable.php @@ -93,13 +93,13 @@ abstract class Piwik_ViewDataTable * @see init() * @var string */ - protected $actionToLoadTheSubTable = null; + protected $controllerActionCalledWhenRequestSubTable = null; /** * @see init() * @var string */ - protected $moduleNameAndMethod; + protected $apiMethodToRequestDataTable; /** * This view should be an implementation of the Interface Piwik_iView @@ -197,6 +197,11 @@ abstract class Piwik_ViewDataTable return new Piwik_ViewDataTable_HtmlTable_AllColumns(); break; + case 'tableGoals': + require_once "ViewDataTable/HtmlTable/Goals.php"; + return new Piwik_ViewDataTable_HtmlTable_Goals(); + break; + case 'table': default: require_once "ViewDataTable/HtmlTable.php"; @@ -208,42 +213,45 @@ abstract class Piwik_ViewDataTable /** * Inits the object given the $currentControllerName, $currentControllerAction of * the calling controller action, eg. 'Referers' 'getLongListOfKeywords'. - * The initialization also requires the $moduleNameAndMethod of the API method + * The initialization also requires the $apiMethodToRequestDataTable of the API method * to call in order to get the DataTable, eg. 'Referers.getKeywords'. - * The optional $actionToLoadTheSubTable defines the method name of the API to call when there is a idSubtable. + * 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 $actionToLoadTheSubTable = 'getSearchEnginesFromKeywordId'. + * In this case $controllerActionCalledWhenRequestSubTable = 'getSearchEnginesFromKeywordId'. * The GET request will hit 'Referers.getSearchEnginesFromKeywordId'. * * @param string $currentControllerName eg. 'Referers' * @param string $currentControllerAction eg. 'getKeywords' - * @param string $moduleNameAndMethod eg. 'Referers.getKeywords' - * @param string $actionToLoadTheSubTable eg. 'getSearchEnginesFromKeywordId' + * @param string $apiMethodToRequestDataTable eg. 'Referers.getKeywords' + * @param string $controllerActionCalledWhenRequestSubTable eg. 'getSearchEnginesFromKeywordId' * @return void */ public function init( $currentControllerName, $currentControllerAction, - $moduleNameAndMethod, - $actionToLoadTheSubTable = null) + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable = null) { $this->currentControllerName = $currentControllerName; $this->currentControllerAction = $currentControllerAction; - $this->moduleNameAndMethod = $moduleNameAndMethod; - $this->actionToLoadTheSubTable = $actionToLoadTheSubTable; - $this->method = $moduleNameAndMethod; + $this->apiMethodToRequestDataTable = $apiMethodToRequestDataTable; + $this->controllerActionCalledWhenRequestSubTable = $controllerActionCalledWhenRequestSubTable; $this->idSubtable = Piwik_Common::getRequestVar('idSubtable', false, 'int'); + $this->viewProperties['show_goals'] = false; $this->viewProperties['show_search'] = Piwik_Common::getRequestVar('show_search', true); $this->viewProperties['show_table_all_columns'] = Piwik_Common::getRequestVar('show_table_all_columns', 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_footer'] = Piwik_Common::getRequestVar('show_footer', true); $this->viewProperties['show_footer_icons'] = ($this->idSubtable == false); + $this->viewProperties['apiMethodToRequestDataTable'] = $this->apiMethodToRequestDataTable; + $this->viewProperties['uniqueId'] = $this->getUniqueIdViewDataTable(); } + /** * Forces the View to use a given template. * Usually the template to use is set in the specific ViewDataTable_* @@ -274,6 +282,26 @@ abstract class Piwik_ViewDataTable 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 * @@ -299,7 +327,7 @@ abstract class Piwik_ViewDataTable protected function loadDataTableFromAPI() { // we build the request string (URL) to call the API - $requestString = $this->getRequestString(); + $requestString = $this->getRequestString(); // we make the request to the API $request = new Piwik_API_Request($requestString); @@ -319,7 +347,7 @@ abstract class Piwik_ViewDataTable // 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->moduleNameAndMethod; + $requestString = 'method='.$this->apiMethodToRequestDataTable; $requestString .= '&format=original'; $toSetEventually = array( @@ -384,7 +412,7 @@ abstract class Piwik_ViewDataTable * @see datatable.js * @return string */ - protected function getUniqIdTable() + protected function getUniqueIdViewDataTable() { // 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 @@ -477,13 +505,25 @@ abstract class Piwik_ViewDataTable $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(); - if(!is_null($this->actionToLoadTheSubTable)) + if(!is_null($this->controllerActionCalledWhenRequestSubTable)) { - $javascriptVariablesToSet['actionToLoadTheSubTable'] = $this->actionToLoadTheSubTable; + $javascriptVariablesToSet['controllerActionCalledWhenRequestSubTable'] = $this->controllerActionCalledWhenRequestSubTable; } if($this->dataTable) @@ -496,16 +536,27 @@ abstract class Piwik_ViewDataTable // are loaded with Piwik_Common::getRequestVar() foreach($javascriptVariablesToSet as &$value) { - if(is_array($value)) - { - $value = array_map('addslashes',$value); - } - else - { + 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; } @@ -605,18 +656,27 @@ abstract class Piwik_ViewDataTable $this->viewProperties['show_table_all_columns'] = false; } - /** - * Sets the pattern to look for in the table (only rows with column equal to the pattern will be kept) - * - * @param array $pattern arrays of patterns to look for - * @param string $column to compare the pattern to - * @return void - */ - public function setExactPattern($pattern, $column) - { - $this->variablesDefault['filter_exact_pattern'] = $pattern; - $this->variablesDefault['filter_exact_column'] = $column; - } + /** + * Whether or not to show the "goal" icon + * @return void + */ + public function enableShowGoals() + { + $this->viewProperties['show_goals'] = true; + } + + /** + * Sets the pattern to look for in the table (only rows with column equal to the pattern will be kept) + * + * @param array $pattern arrays of patterns to look for + * @param string $column to compare the pattern to + * @return void + */ + public function setExactPattern($pattern, $column) + { + $this->variablesDefault['filter_exact_pattern'] = $pattern; + $this->variablesDefault['filter_exact_column'] = $column; + } /** * Sets the value to use for the Exclude low population filter. @@ -625,18 +685,12 @@ abstract class Piwik_ViewDataTable * @param string The name of the column for which we compare the value to $minValue * @return void */ - public function setExcludeLowPopulation( $minValue = null, $columnName = null ) + public function setExcludeLowPopulation( $columnName = null, $minValue = null ) { - if( is_null( $minValue) ) - { - throw new Exception("setExcludeLowPopulation() value shouldn't be null"); - } - if(is_null($columnName)) { $columnName = Piwik_Archive::INDEX_NB_VISITS; } - $this->variablesDefault['filter_excludelowpop'] = $columnName; $this->variablesDefault['filter_excludelowpop_value'] = $minValue; } diff --git a/core/ViewDataTable/Cloud.php b/core/ViewDataTable/Cloud.php index 179c8959d2..29cb676eab 100644 --- a/core/ViewDataTable/Cloud.php +++ b/core/ViewDataTable/Cloud.php @@ -33,11 +33,11 @@ class Piwik_ViewDataTable_Cloud extends Piwik_ViewDataTable */ function init($currentControllerName, $currentControllerAction, - $moduleNameAndMethod ) + $apiMethodToRequestDataTable ) { parent::init($currentControllerName, $currentControllerAction, - $moduleNameAndMethod ); + $apiMethodToRequestDataTable ); $this->dataTableTemplate = 'CoreHome/templates/cloud.tpl'; $this->disableOffsetInformation(); $this->disableExcludeLowPopulation(); @@ -99,8 +99,6 @@ class Piwik_ViewDataTable_Cloud extends Piwik_ViewDataTable $view->labelMetadata = $labelMetadata; $view->cloudValues = $cloudValues; - $view->method = $this->method; - $view->id = $this->getUniqIdTable(); $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); $view->properties = $this->getViewProperties(); return $view; diff --git a/core/ViewDataTable/GenerateGraphData/ChartEvolution.php b/core/ViewDataTable/GenerateGraphData/ChartEvolution.php index 93c69b2a49..8b51c394d6 100644 --- a/core/ViewDataTable/GenerateGraphData/ChartEvolution.php +++ b/core/ViewDataTable/GenerateGraphData/ChartEvolution.php @@ -19,198 +19,198 @@ class Piwik_ViewDataTable_GenerateGraphData_ChartEvolution extends Piwik_ViewDat $this->view = new Piwik_Visualization_ChartEvolution; } - var $lineLabels = array(); - var $data = array(); - - private function generateLine( $dataArray, $columns, $schema = "##label## ##column##" ) - { - $data = array(); - - foreach($dataArray as $keyName => $table) - { - $table->applyQueuedFilters(); - - // initialize data (default values for all lines is 0) - $dataRow = array(); - - $rows = $table->getRows(); - - foreach($rows as $row) + var $lineLabels = array(); + var $data = array(); + + private function generateLine( $dataArray, $columns, $schema = "##label## ##column##" ) + { + $data = array(); + + foreach($dataArray as $keyName => $table) + { + $table->applyQueuedFilters(); + + // initialize data (default values for all lines is 0) + $dataRow = array(); + + $rows = $table->getRows(); + + foreach($rows as $row) + { + $rowLabel = $schema; + + if( strpos($rowLabel, "##label##") !== false ) + { + $rowLabel = str_replace("##label##", $row->getColumn('label'), $rowLabel); + } + + foreach($columns as $col) + { + $label = $rowLabel; + + if( strpos($label, "##column##") !== false ) + { + $label = str_replace("##column##", $col, $label); + } + + if( !isset($this->lineLabels[$label]) ) + { + $this->lineLabels[$label] = count($this->lineLabels); + } + $lineNb = $this->lineLabels[$label]; + + $value = $row->getColumn($col); + + $dataRow['value'.$lineNb] = $value; + } + } + $data[] = $dataRow; + } + return $data; + } + + private function generateLabels( $dataArray ) + { + $data = array(); + + foreach($dataArray as $keyName => $table) + { + $table->applyQueuedFilters(); + + $data[] = array('label' => $keyName); + } + + return $data; + } + + private function addArray( &$data, $newData ) + { + for($i = 0; $i < count($newData); $i++) + { + foreach($newData[$i] as $key => $value) + { + $data[$i][$key] = $value; + } + } + } + + private function fillValues( &$data ) + { + $nbLines = count($this->lineLabels); + + for($i = 0; $i < count($data); $i++) + { + for($j = 0; $j < $nbLines; $j++) + { + if( !isset($data[$i]['value'.$j]) ) + { + $data[$i]['value'.$j] = 0; + } + } + } + } + + /* + * generates data for evolution graph from a numeric DataTable (DataTable that has only 'label' and 'value' columns) + */ + protected function generateDataFromNumericDataTable($dataArray, $siteLabel = "") + { + $columnsToDisplay = Piwik_Common::getRequestVar('columns', array(), 'array'); + + // for numeric we want to have only one column name + if( count($columnsToDisplay) != 1 ) + { + $columnsToDisplay = array( 'nb_uniq_visitors' ); + } + + $label = $siteLabel . array_shift($columnsToDisplay); + + $this->addArray($this->data, $this->generateLabels($dataArray)); + $this->addArray($this->data, $this->generateLine($dataArray,array('value'),$label)); + $this->fillValues($this->data); + } + + /* + * generates data for evolution graph from a DataTable that has named columns (i.e. 'nb_hits', 'nb_uniq_visitors') + */ + protected function generateDataFromRegularDataTable($dataArray, $siteLabel = "") + { + // get list of columns to display i.e. array('nb_hits','nb_uniq_visitors') + $columnsToDisplay = Piwik_Common::getRequestVar('columns', array(), 'array'); + + // default column + if( count($columnsToDisplay) == 0 ) + { + $columnsToDisplay = array( 'nb_uniq_visitors' ); + } + + $this->addArray($this->data, $this->generateLabels($dataArray)); + $this->addArray($this->data, $this->generateLine($dataArray, $columnsToDisplay, $siteLabel."##label## ##column##")); + $this->fillValues($this->data); + } + + protected function handleSiteGenerateDataFromDataTable($dataArray, $siteLabel = "") + { + // detect if we got numeric Datatable or regular DataTable + foreach($dataArray as $table) + { + $row = $table->getFirstRow(); + + if( $row != null ) + { + $columns = $row->getColumns(); + + // if we got 2 columns - 'label' and 'value' this is numeric DataTable + if( count($columns) == 2 && isset($columns['label']) && isset($columns['value']) ) + { + $this->generateDataFromNumericDataTable($dataArray, $siteLabel); + } + else + { + $this->generateDataFromRegularDataTable($dataArray, $siteLabel); + } + break; + } + } + } + + public function generateDataFromDataTable() + { + $data = array(); + + if( $this->dataTable->getRowsCount() ) + { + $row = null; + + // find first table with rows + foreach($this->dataTable->getArray() as $idsite => $table) { - $rowLabel = $schema; - - if( strpos($rowLabel, "##label##") !== false ) - { - $rowLabel = str_replace("##label##", $row->getColumn('label'), $rowLabel); - } - - foreach($columns as $col) - { - $label = $rowLabel; - - if( strpos($label, "##column##") !== false ) - { - $label = str_replace("##column##", $col, $label); - } - - if( !isset($this->lineLabels[$label]) ) - { - $this->lineLabels[$label] = count($this->lineLabels); - } - $lineNb = $this->lineLabels[$label]; - - $value = $row->getColumn($col); - - $dataRow['value'.$lineNb] = $value; - } - } - $data[] = $dataRow; - } - return $data; - } - - private function generateLabels( $dataArray ) - { - $data = array(); - - foreach($dataArray as $keyName => $table) - { - $table->applyQueuedFilters(); - - $data[] = array('label' => $keyName); - } - - return $data; - } - - private function addArray( &$data, $newData ) - { - for($i = 0; $i < count($newData); $i++) - { - foreach($newData[$i] as $key => $value) - { - $data[$i][$key] = $value; - } - } - } - - private function fillValues( &$data ) - { - $nbLines = count($this->lineLabels); - - for($i = 0; $i < count($data); $i++) - { - for($j = 0; $j < $nbLines; $j++) - { - if( !isset($data[$i]['value'.$j]) ) - { - $data[$i]['value'.$j] = 0; - } - } - } - } - - /* - * generates data for evolution graph from a numeric DataTable (DataTable that has only 'label' and 'value' columns) - */ - protected function generateDataFromNumericDataTable($dataArray, $siteLabel = "") - { - $columnsToDisplay = Piwik_Common::getRequestVar('columns', array(), 'array'); - - // for numeric we want to have only one column name - if( count($columnsToDisplay) != 1 ) - { - $columnsToDisplay = array( 'nb_uniq_visitors' ); - } - - $label = $siteLabel . array_shift($columnsToDisplay); - - $this->addArray($this->data, $this->generateLabels($dataArray)); - $this->addArray($this->data, $this->generateLine($dataArray,array('value'),$label)); - $this->fillValues($this->data); - } - - /* - * generates data for evolution graph from a DataTable that has named columns (i.e. 'nb_hits', 'nb_uniq_visitors') - */ - protected function generateDataFromRegularDataTable($dataArray, $siteLabel = "") - { - // get list of columns to display i.e. array('nb_hits','nb_uniq_visitors') - $columnsToDisplay = Piwik_Common::getRequestVar('columns', array(), 'array'); - - // default column - if( count($columnsToDisplay) == 0 ) - { - $columnsToDisplay = array( 'nb_uniq_visitors' ); - } - - $this->addArray($this->data, $this->generateLabels($dataArray)); - $this->addArray($this->data, $this->generateLine($dataArray, $columnsToDisplay, $siteLabel."##label## ##column##")); - $this->fillValues($this->data); - } - - protected function handleSiteGenerateDataFromDataTable($dataArray, $siteLabel = "") - { - // detect if we got numeric Datatable or regular DataTable - foreach($dataArray as $table) - { - $row = $table->getFirstRow(); - - if( $row != null ) - { - $columns = $row->getColumns(); - - // if we got 2 columns - 'label' and 'value' this is numeric DataTable - if( count($columns) == 2 && isset($columns['label']) && isset($columns['value']) ) - { - $this->generateDataFromNumericDataTable($dataArray, $siteLabel); - } - else - { - $this->generateDataFromRegularDataTable($dataArray, $siteLabel); - } - break; - } - } - } - - public function generateDataFromDataTable() - { - $data = array(); - - if( $this->dataTable->getRowsCount() ) - { - $row = null; - - // find first table with rows - foreach($this->dataTable->getArray() as $idsite => $table) - { - // detect if we got data from more than one site - if( $table instanceof Piwik_DataTable_Array) - { - // multiple sites - $site = new Piwik_Site($idsite); - - $this->handleSiteGenerateDataFromDataTable($table->getArray(), $site->getName()." "); - } - else if( $table instanceof Piwik_DataTable_Simple && $this->dataTable->getKeyName() == 'idSite') - { - // multiple sites (when numeric DataTable) - $site = new Piwik_Site($idsite); - - $this->handleSiteGenerateDataFromDataTable($table->getFirstRow()->getColumn('value')->getArray(), $site->getName()." "); - } - else - { - // single site - $this->handleSiteGenerateDataFromDataTable($this->dataTable->getArray()); - break; - } - } - - } - array_unshift($this->data, array_keys($this->lineLabels)); - - return $this->data; + // detect if we got data from more than one site + if( $table instanceof Piwik_DataTable_Array) + { + // multiple sites + $site = new Piwik_Site($idsite); + + $this->handleSiteGenerateDataFromDataTable($table->getArray(), $site->getName()." "); + } + else if( $table instanceof Piwik_DataTable_Simple && $this->dataTable->getKeyName() == 'idSite') + { + // multiple sites (when numeric DataTable) + $site = new Piwik_Site($idsite); + + $this->handleSiteGenerateDataFromDataTable($table->getFirstRow()->getColumn('value')->getArray(), $site->getName()." "); + } + else + { + // single site + $this->handleSiteGenerateDataFromDataTable($this->dataTable->getArray()); + break; + } + } + + } + array_unshift($this->data, array_keys($this->lineLabels)); + + return $this->data; } } diff --git a/core/ViewDataTable/GenerateGraphHTML.php b/core/ViewDataTable/GenerateGraphHTML.php index 3204361748..58849049d3 100644 --- a/core/ViewDataTable/GenerateGraphHTML.php +++ b/core/ViewDataTable/GenerateGraphHTML.php @@ -27,11 +27,11 @@ abstract class Piwik_ViewDataTable_GenerateGraphHTML extends Piwik_ViewDataTable */ function init($currentControllerName, $currentControllerAction, - $moduleNameAndMethod ) + $apiMethodToRequestDataTable ) { parent::init($currentControllerName, $currentControllerAction, - $moduleNameAndMethod ); + $apiMethodToRequestDataTable ); $this->dataTableTemplate = 'CoreHome/templates/graph.tpl'; @@ -74,7 +74,7 @@ abstract class Piwik_ViewDataTable_GenerateGraphHTML extends Piwik_ViewDataTable protected function buildView() { $view = new Piwik_View($this->dataTableTemplate); - $this->id = $this->getUniqIdTable(); + $this->uniqueIdViewDataTable = $this->getUniqueIdViewDataTable(); $view->graphType = $this->graphType; $this->parametersToModify['action'] = $this->currentControllerAction; @@ -82,11 +82,9 @@ abstract class Piwik_ViewDataTable_GenerateGraphHTML extends Piwik_ViewDataTable $view->jsInvocationTag = $this->getFlashInvocationCode($url); $view->urlGraphData = $url; - $view->formEmbedId = "formEmbed".$this->id; + $view->formEmbedId = "formEmbed".$this->uniqueIdViewDataTable; $view->graphCodeEmbed = $this->graphCodeEmbed; - $view->id = $this->id; - $view->method = $this->method; $view->javascriptVariablesToSet = $this->getJavascriptVariablesToSet(); $view->properties = $this->getViewProperties(); return $view; @@ -105,8 +103,8 @@ abstract class Piwik_ViewDataTable_GenerateGraphHTML extends Piwik_ViewDataTable // escape the & and stuff: $url = urlencode($url); - $obj_id = $this->id . "Chart"; - $div_name = $this->id . "FlashContent"; + $obj_id = $this->uniqueIdViewDataTable . "Chart"; + $div_name = $this->uniqueIdViewDataTable . "FlashContent"; $return = ''; if( $use_swfobject ) diff --git a/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php b/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php index 0eb29034e3..98fa9323a8 100644 --- a/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php +++ b/core/ViewDataTable/GenerateGraphHTML/ChartEvolution.php @@ -28,24 +28,24 @@ class Piwik_ViewDataTable_GenerateGraphHTML_ChartEvolution extends Piwik_ViewDat function init($currentControllerName, $currentControllerAction, - $moduleNameAndMethod ) + $apiMethodToRequestDataTable ) { parent::init($currentControllerName, $currentControllerAction, - $moduleNameAndMethod ); + $apiMethodToRequestDataTable ); $this->setParametersToModify(array('date' => Piwik_Common::getRequestVar('date', 'last30', 'string'))); $this->doNotShowFooter(); } - - /** - * 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) - { - $this->setParametersToModify( array('columns' => $columnsNames) ); - } + + /** + * 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) + { + $this->setParametersToModify( array('columns' => $columnsNames) ); + } } \ No newline at end of file diff --git a/core/ViewDataTable/HtmlTable.php b/core/ViewDataTable/HtmlTable.php index 9c27447417..3beb82265e 100644 --- a/core/ViewDataTable/HtmlTable.php +++ b/core/ViewDataTable/HtmlTable.php @@ -53,21 +53,15 @@ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable */ function init($currentControllerName, $currentControllerAction, - $moduleNameAndMethod, - $actionToLoadTheSubTable = null ) + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable = null ) { parent::init($currentControllerName, $currentControllerAction, - $moduleNameAndMethod, - $actionToLoadTheSubTable); + $apiMethodToRequestDataTable, + $controllerActionCalledWhenRequestSubTable); $this->dataTableTemplate = 'CoreHome/templates/datatable.tpl'; - $this->variablesDefault['enable_sort'] = '1'; - - // load general columns translations - $this->setColumnTranslation('nb_visits', Piwik_Translate('General_ColumnNbVisits')); - $this->setColumnTranslation('label', Piwik_Translate('General_ColumnLabel')); - $this->setColumnTranslation('nb_uniq_visitors', Piwik_Translate('General_ColumnNbUniqVisitors')); } protected function getViewDataTableId() @@ -97,6 +91,10 @@ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable */ protected function postDataTableLoadedFromAPI() { + // load general columns translations + $this->setColumnTranslation('nb_visits', Piwik_Translate('General_ColumnNbVisits')); + $this->setColumnTranslation('label', Piwik_Translate('General_ColumnLabel')); + $this->setColumnTranslation('nb_uniq_visitors', Piwik_Translate('General_ColumnNbUniqVisitors')); } /** @@ -109,7 +107,6 @@ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable $phpArray = $this->getPHPArrayFromDataTable(); $view->arrayDataTable = $phpArray; - $view->method = $this->method; $columns = $this->getColumnsToDisplay($phpArray); $view->dataTableColumns = $columns; @@ -122,11 +119,24 @@ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable } $view->nbColumns = $nbColumns; - $view->id = $this->getUniqIdTable(); $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 = Piwik_Archive::INDEX_NB_VISITS; + } + $this->setExcludeLowPopulation( $columnToApplyFilter); + } /** * Returns friendly php array from the Piwik_DataTable @@ -155,6 +165,16 @@ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable $this->columnsToDisplay = $columnsNames; } + /** + * Adds a column to the list of columns to be displayed + * + * @param string $columnName + */ + public function addColumnToDisplay( $columnName ) + { + $this->columnsToDisplay[] = $columnName; + } + /** * Sets translation string for given column * @@ -217,6 +237,7 @@ class Piwik_ViewDataTable_HtmlTable extends Piwik_ViewDataTable $columnsToDisplay = $columnsInDataTable; } + $columnsToDisplay = array_unique($columnsToDisplay); foreach($columnsToDisplay as $columnToDisplay) { if(in_array($columnToDisplay, $columnsInDataTable)) diff --git a/core/ViewDataTable/HtmlTable/AllColumns.php b/core/ViewDataTable/HtmlTable/AllColumns.php index 9ea33f5a85..e1c0882301 100644 --- a/core/ViewDataTable/HtmlTable/AllColumns.php +++ b/core/ViewDataTable/HtmlTable/AllColumns.php @@ -3,8 +3,6 @@ require_once "ViewDataTable/HtmlTable.php"; class Piwik_ViewDataTable_HtmlTable_AllColumns extends Piwik_ViewDataTable_HtmlTable { - const LOW_POPULATION_THRESHOLD_PERCENTAGE_VISIT = 0.005; - protected function getViewDataTableId() { return 'tableAllColumns'; @@ -18,22 +16,6 @@ class Piwik_ViewDataTable_HtmlTable_AllColumns extends Piwik_ViewDataTable_HtmlT parent::main(); } - protected function handleLowPopulation() - { - if(Piwik_Common::getRequestVar('filter_excludelowpop', '0', 'string' ) == '0') - { - return; - } - - require_once "VisitsSummary/Controller.php"; - $visits = Piwik_VisitsSummary_Controller::getVisits(); - $visitsThreshold = floor( self::LOW_POPULATION_THRESHOLD_PERCENTAGE_VISIT * $visits); - if($visitsThreshold > 0) - { - $this->setExcludeLowPopulation( $visitsThreshold, Piwik_Archive::INDEX_NB_VISITS ); - } - } - protected function getRequestString() { $requestString = parent::getRequestString(); @@ -42,6 +24,7 @@ class Piwik_ViewDataTable_HtmlTable_AllColumns extends Piwik_ViewDataTable_HtmlT protected function postDataTableLoadedFromAPI() { + parent::postDataTableLoadedFromAPI(); $this->setColumnsToDisplay(array('label', 'nb_visits', 'nb_uniq_visitors', diff --git a/core/ViewDataTable/HtmlTable/Goals.php b/core/ViewDataTable/HtmlTable/Goals.php new file mode 100644 index 0000000000..ebb5d9e800 --- /dev/null +++ b/core/ViewDataTable/HtmlTable/Goals.php @@ -0,0 +1,77 @@ +viewProperties['show_exclude_low_population'] = true; + $this->viewProperties['show_goals'] = true; + $this->setColumnsToDisplay( array( 'label', + 'nb_visits', + 'goals_conversion_rate', + 'goal_%s_conversion_rate', + 'revenue_per_visit', + )); + $this->handleLowPopulation(); + parent::main(); + } + + public function disableSubTableWhenShowGoals() + { + $this->controllerActionCalledWhenRequestSubTable = null; + } + + protected function getRequestString() + { + $requestString = parent::getRequestString(); + return $requestString . '&filter_update_columns_when_show_all_goals=1'; + } + + protected $columnsToPercentageFilter = array(); + + public function setColumnsToDisplay($columnsNames) + { + $newColumnsNames = array(); + foreach($columnsNames as $columnName) + { + if($columnName == 'goal_%s_conversion_rate') + { + require_once "core/Tracker/GoalManager.php"; + $goals = Piwik_Tracker_GoalManager::getGoalDefinitions(); + foreach($goals as $goal) + { + $idgoal = $goal['id']; + $name = $goal['name']; + $columnName = 'goal_'.$idgoal.'_conversion_rate'; + $newColumnsNames[] = $columnName; + $this->setColumnTranslation($columnName, $name); + $this->columnsToPercentageFilter[] = $columnName; + } + } + else + { + $newColumnsNames[] = $columnName; + } + } + parent::setColumnsToDisplay($newColumnsNames); + } + + protected function postDataTableLoadedFromAPI() + { + parent::postDataTableLoadedFromAPI(); + $this->setColumnTranslation('revenue_per_visit', 'Value per Visit'); + $this->setColumnTranslation('goals_conversion_rate', 'Visits with Conversions'); + $this->columnsToPercentageFilter[] = 'goals_conversion_rate'; + foreach($this->columnsToPercentageFilter as $columnName) + { + $filter = new Piwik_DataTable_Filter_ColumnCallbackReplace($this->dataTable, $columnName, create_function('$rate', 'return $rate."%";')); + } + $filter = new Piwik_DataTable_Filter_ColumnCallbackReplace($this->dataTable, 'revenue_per_visit', array("Piwik", "getPrettyMoney")); + } +} diff --git a/core/ViewDataTable/Sparkline.php b/core/ViewDataTable/Sparkline.php index ab6fce4ce7..f9908a50fa 100644 --- a/core/ViewDataTable/Sparkline.php +++ b/core/ViewDataTable/Sparkline.php @@ -23,18 +23,6 @@ class Piwik_ViewDataTable_Sparkline extends Piwik_ViewDataTable return 'sparkline'; } - /** - * @see Piwik_ViewDataTable::init() - */ - function init($currentControllerName, - $currentControllerAction, - $moduleNameAndMethod ) - { - parent::init($currentControllerName, - $currentControllerAction, - $moduleNameAndMethod ); - } - /** * @see Piwik_ViewDataTable::main() */ @@ -63,6 +51,15 @@ class Piwik_ViewDataTable_Sparkline extends Piwik_ViewDataTable $this->view = $graph; } + /** + * Sparkline needs to be fast to load. Currently we only load numeric values + * that do not need filtering, we can disable the filters for more performance. + */ + protected function getRequestString() + { + return parent::getRequestString() . "&disable_generic_filters=1"; + } + /** * Given a Piwik_DataTable_Array made of DataTable_Simple rows, returns a php array with the structure: * array( diff --git a/core/Visualization/ChartEvolution.php b/core/Visualization/ChartEvolution.php index 9160eb8bc2..82f2da4fa4 100644 --- a/core/Visualization/ChartEvolution.php +++ b/core/Visualization/ChartEvolution.php @@ -1,72 +1,72 @@ -prepareData(); - - $colors = array( - "0x3357A0", - "0x9933CC", - "0xCC3399", - "0x80a033", - "0xFD9816", - "0x246AD2", - "0xFD16EA", - "0x49C100", - ); - - // first row in array contains line labels (legend) - $legendLabels = array_shift($this->dataGraph); - - $line = array(); - - // define labels - foreach($legendLabels as $nbLabel => $labelName) - { - $line[$nbLabel] = new line_hollow( 1, 3, $colors[$nbLabel] ); - $line[$nbLabel]->key( $labelName, 10 ); - } - - $maxData = 0; - $xLabels = array(); - $cnt = count($this->dataGraph); - - // loop over data - foreach($this->dataGraph as $values) - { - // add x axis value (label) - array_push($xLabels, $values['label']); - - // loop over values for all lines (y axis values) - for($j = 0; $j < count($legendLabels); $j++) - { - // get the y axis value for line $j - $dotValue = $values['value'.$j]; - - // find maximum y axis value - if( $dotValue > $maxData ) - { - $maxData = $dotValue; - } +prepareData(); + + $colors = array( + "0x3357A0", + "0x9933CC", + "0xCC3399", + "0x80a033", + "0xFD9816", + "0x246AD2", + "0xFD16EA", + "0x49C100", + ); + + // first row in array contains line labels (legend) + $legendLabels = array_shift($this->dataGraph); + + $line = array(); + + // define labels + foreach($legendLabels as $nbLabel => $labelName) + { + $line[$nbLabel] = new line_hollow( 1, 3, $colors[$nbLabel] ); + $line[$nbLabel]->key( $labelName, 10 ); + } + + $maxData = 0; + $xLabels = array(); + $cnt = count($this->dataGraph); + + // loop over data + foreach($this->dataGraph as $values) + { + // add x axis value (label) + array_push($xLabels, $values['label']); + + // loop over values for all lines (y axis values) + for($j = 0; $j < count($legendLabels); $j++) + { + // get the y axis value for line $j + $dotValue = $values['value'.$j]; + + // find maximum y axis value + if( $dotValue > $maxData ) + { + $maxData = $dotValue; + } $link = null; if($this->isLinkEnabled()) @@ -89,12 +89,12 @@ class Piwik_Visualization_ChartEvolution extends Piwik_Visualization_Chart else { $line[$j]->add($dotValue); - } - } - } - $this->data_sets = $line; - $this->set_y_max( $maxData ); - $this->set_x_labels( $xLabels ); + } + } + } + $this->data_sets = $line; + $this->set_y_max( $maxData ); + $this->set_x_labels( $xLabels ); } private function isLinkEnabled() @@ -105,5 +105,5 @@ class Piwik_Visualization_ChartEvolution extends Piwik_Visualization_Chart $linkEnabled = !Piwik_Common::getRequestVar('disableLink', 0, 'int'); } return $linkEnabled; - } + } } diff --git a/misc/TODO b/misc/TODO index e52dab9f68..bd1fc45730 100644 --- a/misc/TODO +++ b/misc/TODO @@ -3,18 +3,21 @@ Following up show all columns new UI element - passe en année, actions puis pages => c'est vide !!! - pages columns: first should be unique page views, sorted by unique pageviews - exclude all population doesn't work for actions +- when used with visitsGenerator, in dowloads export XML, full_url and url is 0 +- add new column "overall visits with conversion" - post 29 There seems not to be a way to export the pages information widget. I wanted to download the entry pages to see where my visitors entered my site from, but there is no option to do so. - -$("td img", domElem).attr("src") is undefined -[Break on this error] var plusDetected = $('td img', domElem).attr('src').indexOf('plus') >= 0; -datatable.js (line 907) -$("td img", domElem).attr("src") is undefined -[Break on this error] var plusDetected = $('td img', domElem).attr('src').indexOf('plus') >= 0; -when clicking MINUS on actions + $("td img", domElem).attr("src") is undefined + [Break on this error] var plusDetected = $('td img', domElem).attr('src').indexOf('plus') >= 0; + datatable.js (line 907) + $("td img", domElem).attr("src") is undefined + [Break on this error] var plusDetected = $('td img', domElem).attr('src').indexOf('plus') >= 0; + when clicking MINUS on actions - icons disappear when showing all columns for browsers - exclude low population doesn't work for providers - ADD forward of non true show_values to forward when specified from URL to ajax - disable show icon in some iframes and/or dashboard +- when exclude low, switch back to normal table, include all is not avail and low are still excluded + ->when switch back to simple table, reset exclude - do proper CSS factoring - clarify 'false' '0' values etc. //convert all JS datatable footer parameter values to '1' or '0' - check state of javascript footer (should not have safe_decode and filter_add_columns blah) @@ -39,26 +42,31 @@ UI fixes + adjustements - write test to check disabled db plugin default - show total size in MB in db plugin - search inside datatable doesnt really work eg. "web analytics" restore regex search + can we find a "binary operator string -> regex" conversion function? - bounce count should be bounce rate - idem in 222 times that a returning visit has bounced (left the site after one page) - show new vs returning more clearly in UI - calendar CSS on API page - WRONG::: 147 sites Internet différents (utilisant 147 différentes adresses) - see also #421 +- add "days since last visit" +- add "visit count until conversion" check analytics books high priority features ==== -- tester clickheat http://www.labsmedia.com/clickheat/piwik-clickheat.tar.gz + add FAQ -- sync update + email install - Goal tracking +- tester clickheat http://www.labsmedia.com/clickheat/piwik-clickheat.tar.gz + add FAQ +- sync update + email install + automatic update - Live! - simple MultiSites report plugin. - new JS tagging API - Exclude webmaster - Loyalty (Most people visited: 1 times) and Recency (Most people last visited: X days ago) - + http://en.wikipedia.org/wiki/RFM + + UI elements: ==== - add link under widgets to report (optional display) @@ -75,9 +83,12 @@ website Bugs - add icon calendar http://developer.yahoo.com/ypatterns/pattern.php?pattern=calendar#Solution - something is wrong in JS calendar when year selected -- I had to chmod 777 tmp/ to pass install -all graphic showing data evolution are labelled with the text "nb_uniq_visitors" no matter if youre watching last visits graph, global visits graph or any other. Its happening in every graph under Visitors subpages and in the Dashboard. -In Referers->Evolution page, in addition, I think its melting titles because I read in spanish "Entrada directa nb_uniq_visitors" for the first graph, "Paginas web nb_uniq_visitors" for the second and so on. +- all graphic showing data evolution are labelled with the text "nb_uniq_visitors" + no matter if youre watching last visits graph, global visits graph or any other. + Its happening in every graph under Visitors subpages and in the Dashboard. +- In Referers->Evolution page, in addition, I think its melting titles because I read + in spanish "Entrada directa nb_uniq_visitors" for the first graph, + "Paginas web nb_uniq_visitors" for the second and so on. To clean - plugins should listen to user delete/ site delete and delete their own user/site related data diff --git a/misc/generateVisits.php b/misc/generateVisits.php index f8984199e8..38774e0d00 100644 --- a/misc/generateVisits.php +++ b/misc/generateVisits.php @@ -3,10 +3,10 @@ * The script can be used to generate huge number of visits and actions * for a given number of days. */ -$minVisitors = 500; -$maxVisitors = 1000; +$minVisitors = 200; +$maxVisitors = 200; $nbActions = 3; -$daysToCompute = 1; +$daysToCompute = 10; //----------------------------------------------------------------------------- error_reporting(E_ALL|E_NOTICE); @@ -46,6 +46,7 @@ require_once "Tracker/Action.php"; require_once "Tracker/Db.php"; require_once "Tracker/Visit.php"; require_once "Tracker/Generator.php"; +require_once "Tracker/GoalManager.php"; //Piwik_PluginsManager::getInstance()->unloadPlugins(); @@ -73,6 +74,7 @@ while($startTime <= time()) $visitors = rand($minVisitors, $maxVisitors); $actions = $nbActions; $generator->setTimestampToUse($startTime); + $nbActionsTotalThisDay = $generator->generate($visitors, $actions); $actionsPerVisit = round($nbActionsTotalThisDay / $visitors); print("Generated $visitors unique visitors and $actionsPerVisit actions per visit for the ".date("Y-m-d", $startTime)."
\n"); diff --git a/piwik.php b/piwik.php index 413ef7d147..b4a8d922b5 100644 --- a/piwik.php +++ b/piwik.php @@ -25,6 +25,7 @@ require_once "Tracker/Action.php"; require_once "Cookie.php"; require_once "Tracker/Db.php"; require_once "Tracker/Visit.php"; +require_once "Tracker/GoalManager.php"; $GLOBALS['DEBUGPIWIK'] = false; diff --git a/plugins/Actions/Actions.php b/plugins/Actions/Actions.php index 0972d00e46..4545762f64 100644 --- a/plugins/Actions/Actions.php +++ b/plugins/Actions/Actions.php @@ -1,21 +1,21 @@ -setCategoryDelimiter( Zend_Registry::get('config')->General->action_category_delimiter); + } + + public function setCategoryDelimiter($delimiter) { - $this->setCategoryDelimiter( Zend_Registry::get('config')->General->action_category_delimiter); - } - - public function setCategoryDelimiter($delimiter) - { - self::$actionCategoryDelimiter = $delimiter; - } - + self::$actionCategoryDelimiter = $delimiter; + } + function addWidgets() { Piwik_AddWidget( 'Actions', 'getActions', Piwik_Translate('Actions_SubmenuPages')); Piwik_AddWidget( 'Actions', 'getDownloads', Piwik_Translate('Actions_SubmenuDownloads')); Piwik_AddWidget( 'Actions', 'getOutlinks', Piwik_Translate('Actions_SubmenuOutlinks')); } - - function addMenus() - { - Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPages', array('module' => 'Actions', 'action' => 'getActions')); - Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuOutlinks', array('module' => 'Actions', 'action' => 'getOutlinks')); - Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuDownloads', array('module' => 'Actions', 'action' => 'getDownloads')); - } - - function archivePeriod( $notification ) - { - $archiveProcessing = $notification->getNotificationObject(); - - $dataTableToSum = array( - 'Actions_actions', - 'Actions_downloads', - 'Actions_outlink', - ); + + function addMenus() + { + Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuPages', array('module' => 'Actions', 'action' => 'getActions')); + Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuOutlinks', array('module' => 'Actions', 'action' => 'getOutlinks')); + Piwik_AddMenu('Actions_Actions', 'Actions_SubmenuDownloads', array('module' => 'Actions', 'action' => 'getDownloads')); + } + + function archivePeriod( $notification ) + { + $archiveProcessing = $notification->getNotificationObject(); + + $dataTableToSum = array( + 'Actions_actions', + 'Actions_downloads', + 'Actions_outlink', + ); $maximumRowsInDataTableLevelZero = 200; - $maximumRowsInSubDataTable = 50; - $archiveProcessing->archiveDataTable($dataTableToSum, $maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable); - } - - /** - * Compute all the actions along with their hierarchies. - * - * For each action we process the "interest statistics" : - * visits, unique visitors, bouce count, sum visit length. - * - * - */ - public function archiveDay( $notification ) - { - $archiveProcessing = $notification->getNotificationObject(); - - require_once "Tracker/Action.php"; - - $this->actionsTablesByType = array( - Piwik_Tracker_Action::TYPE_ACTION => array(), - Piwik_Tracker_Action::TYPE_DOWNLOAD => array(), - Piwik_Tracker_Action::TYPE_OUTLINK => array(), - ); - - // This row is used in the case where an action is know as an exit_action - // but this action was not properly recorded when it was hit in the first place - // so we add this fake row information to make sure there is a nb_hits, etc. column for every action - $this->defaultRow = new Piwik_DataTable_Row(array( - Piwik_DataTable_Row::COLUMNS => array( - 'nb_visits' => 1, - 'nb_uniq_visitors' => 1, - 'nb_hits' => 1, - ))); - - /* - * Actions global information - */ - $query = "SELECT name, - type, - count(distinct t1.idvisit) as nb_visits, - count(distinct visitor_idcookie) as nb_uniq_visitors, - count(*) as nb_hits - FROM (".$archiveProcessing->logTable." as t1 - LEFT JOIN ".$archiveProcessing->logVisitActionTable." as t2 USING (idvisit)) - LEFT JOIN ".$archiveProcessing->logActionTable." as t3 USING (idaction) - WHERE visit_server_date = ? - AND idsite = ? + $maximumRowsInSubDataTable = 50; + $archiveProcessing->archiveDataTable($dataTableToSum, $maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable); + } + + /** + * Compute all the actions along with their hierarchies. + * + * For each action we process the "interest statistics" : + * visits, unique visitors, bouce count, sum visit length. + * + * + */ + public function archiveDay( $notification ) + { + $archiveProcessing = $notification->getNotificationObject(); + + require_once "Tracker/Action.php"; + + $this->actionsTablesByType = array( + Piwik_Tracker_Action::TYPE_ACTION => array(), + Piwik_Tracker_Action::TYPE_DOWNLOAD => array(), + Piwik_Tracker_Action::TYPE_OUTLINK => array(), + ); + + // This row is used in the case where an action is know as an exit_action + // but this action was not properly recorded when it was hit in the first place + // so we add this fake row information to make sure there is a nb_hits, etc. column for every action + $this->defaultRow = new Piwik_DataTable_Row(array( + Piwik_DataTable_Row::COLUMNS => array( + 'nb_visits' => 1, + 'nb_uniq_visitors' => 1, + 'nb_hits' => 1, + ))); + + /* + * Actions global information + */ + $query = "SELECT name, + type, + count(distinct t1.idvisit) as nb_visits, + count(distinct visitor_idcookie) as nb_uniq_visitors, + count(*) as nb_hits + FROM (".$archiveProcessing->logTable." as t1 + LEFT JOIN ".$archiveProcessing->logVisitActionTable." as t2 USING (idvisit)) + LEFT JOIN ".$archiveProcessing->logActionTable." as t3 USING (idaction) + WHERE visit_server_date = ? + AND idsite = ? GROUP BY t3.idaction - ORDER BY nb_hits DESC"; - $query = $archiveProcessing->db->query($query, array( $archiveProcessing->strDateStart, $archiveProcessing->idsite )); - - $modified = $this->updateActionsTableWithRowQuery($query); - - - /* - * Entry actions - */ - $query = "SELECT name, - type, - count(distinct visitor_idcookie) as entry_nb_unique_visitor, - count(*) as entry_nb_visits, - sum(visit_total_actions) as entry_nb_actions, - sum(visit_total_time) as entry_sum_visit_length, - sum(case visit_total_actions when 1 then 1 else 0 end) as entry_bounce_count - FROM ".$archiveProcessing->logTable." - JOIN ".$archiveProcessing->logActionTable." ON (visit_entry_idaction = idaction) - WHERE visit_server_date = ? - AND idsite = ? + ORDER BY nb_hits DESC"; + $query = $archiveProcessing->db->query($query, array( $archiveProcessing->strDateStart, $archiveProcessing->idsite )); + $modified = $this->updateActionsTableWithRowQuery($query); + + + /* + * Entry actions + */ + $query = "SELECT name, + type, + count(distinct visitor_idcookie) as entry_nb_unique_visitor, + count(*) as entry_nb_visits, + sum(visit_total_actions) as entry_nb_actions, + sum(visit_total_time) as entry_sum_visit_length, + sum(case visit_total_actions when 1 then 1 else 0 end) as entry_bounce_count + FROM ".$archiveProcessing->logTable." + JOIN ".$archiveProcessing->logActionTable." ON (visit_entry_idaction = idaction) + WHERE visit_server_date = ? + AND idsite = ? GROUP BY visit_entry_idaction - "; - $query = $archiveProcessing->db->query($query, array( $archiveProcessing->strDateStart, $archiveProcessing->idsite )); - - $modified = $this->updateActionsTableWithRowQuery($query); - - - /* - * Exit actions - */ - $query = "SELECT name, - type, - count(distinct visitor_idcookie) as exit_nb_unique_visitor, - count(*) as exit_nb_visits, - sum(case visit_total_actions when 1 then 1 else 0 end) as exit_bounce_count - - FROM ".$archiveProcessing->logTable." - JOIN ".$archiveProcessing->logActionTable." ON (visit_exit_idaction = idaction) - WHERE visit_server_date = ? - AND idsite = ? + "; + $query = $archiveProcessing->db->query($query, array( $archiveProcessing->strDateStart, $archiveProcessing->idsite )); + $modified = $this->updateActionsTableWithRowQuery($query); + + + /* + * Exit actions + */ + $query = "SELECT name, + type, + count(distinct visitor_idcookie) as exit_nb_unique_visitor, + count(*) as exit_nb_visits, + sum(case visit_total_actions when 1 then 1 else 0 end) as exit_bounce_count + + FROM ".$archiveProcessing->logTable." + JOIN ".$archiveProcessing->logActionTable." ON (visit_exit_idaction = idaction) + WHERE visit_server_date = ? + AND idsite = ? GROUP BY visit_exit_idaction - "; - $query = $archiveProcessing->db->query($query, array( $archiveProcessing->strDateStart, $archiveProcessing->idsite )); - - $modified = $this->updateActionsTableWithRowQuery($query); - - /* - * Time per action - */ - $query = "SELECT name, - type, - sum(time_spent_ref_action) as sum_time_spent - FROM (".$archiveProcessing->logTable." log_visit - JOIN ".$archiveProcessing->logVisitActionTable." log_link_visit_action USING (idvisit)) - JOIN ".$archiveProcessing->logActionTable." log_action ON (log_action.idaction = log_link_visit_action.idaction_ref) - WHERE visit_server_date = ? - AND idsite = ? - GROUP BY idaction_ref - "; - $query = $archiveProcessing->db->query($query, array( $archiveProcessing->strDateStart, $archiveProcessing->idsite )); - - $modified = $this->updateActionsTableWithRowQuery($query); + "; + $query = $archiveProcessing->db->query($query, array( $archiveProcessing->strDateStart, $archiveProcessing->idsite )); + $modified = $this->updateActionsTableWithRowQuery($query); + + /* + * Time per action + */ + $query = "SELECT name, + type, + sum(time_spent_ref_action) as sum_time_spent + FROM (".$archiveProcessing->logTable." log_visit + JOIN ".$archiveProcessing->logVisitActionTable." log_link_visit_action USING (idvisit)) + JOIN ".$archiveProcessing->logActionTable." log_action ON (log_action.idaction = log_link_visit_action.idaction_ref) + WHERE visit_server_date = ? + AND idsite = ? + GROUP BY idaction_ref + "; + $query = $archiveProcessing->db->query($query, array( $archiveProcessing->strDateStart, $archiveProcessing->idsite )); + $modified = $this->updateActionsTableWithRowQuery($query); + $this->archiveDayRecordInDatabase(); + } + + protected function archiveDayRecordInDatabase() + { $maximumRowsInDataTableLevelZero = 200; $maximumRowsInSubDataTable = 50; $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_ACTION]); $s = $dataTable->getSerialized( $maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable ); - $record = new Piwik_ArchiveProcessing_Record_BlobArray('Actions_actions', $s); + $record = new Piwik_ArchiveProcessing_Record_BlobArray('Actions_actions', $s); $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_DOWNLOAD]); $s = $dataTable->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable ); - $record = new Piwik_ArchiveProcessing_Record_BlobArray('Actions_downloads', $s); - - $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_OUTLINK]); - $s = $dataTable->getSerialized( $maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable ); - $record = new Piwik_ArchiveProcessing_Record_BlobArray('Actions_outlink', $s); + $record = new Piwik_ArchiveProcessing_Record_BlobArray('Actions_downloads', $s); + + $dataTable = Piwik_ArchiveProcessing_Day::generateDataTable($this->actionsTablesByType[Piwik_Tracker_Action::TYPE_OUTLINK]); + $s = $dataTable->getSerialized( $maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable ); + $record = new Piwik_ArchiveProcessing_Record_BlobArray('Actions_outlink', $s); - unset($this->actionsTablesByType); - } - - static public function splitUrl($url) - { - $matches = $split_arr = array(); - $n = preg_match("#://[^/]+(/)#",$url, $matches, PREG_OFFSET_CAPTURE); - if( $n ) - { - $host = substr($url, 0, $matches[1][1]); - $split_arr = array($host, $url); - } - else - { - $split_arr = array($url); - } - return $split_arr; - } - - static public function getActionCategoryFromName($name) - { - $isUrl = false; - // case the name is an URL we dont clean the name the same way - if(Piwik_Common::isLookLikeUrl($name) - || preg_match('#^mailto:#',$name)) - { - $split = self::splitUrl($name); - $isUrl = true; - } - else + unset($this->actionsTablesByType); + } + + static public function splitUrl($url) + { + $matches = $split_arr = array(); + $n = preg_match("#://[^/]+(/)#",$url, $matches, PREG_OFFSET_CAPTURE); + if( $n ) + { + $host = substr($url, 0, $matches[1][1]); + $split_arr = array($host, $url); + } + else + { + $split_arr = array($url); + } + return $split_arr; + } + + static public function getActionCategoryFromName($name) + { + $isUrl = false; + // case the name is an URL we dont clean the name the same way + if(Piwik_Common::isLookLikeUrl($name) + || preg_match('#^mailto:#',$name)) + { + $split = self::splitUrl($name); + $isUrl = true; + } + else { if(empty(self::$actionCategoryDelimiter)) { $split = array($name); } else - { + { $split = explode(self::$actionCategoryDelimiter, $name, self::$limitLevelSubCategory); - } - } - return array( $isUrl, $split); - } - - - protected function updateActionsTableWithRowQuery($query) - { - $rowsProcessed = 0; - - while( $row = $query->fetch() ) - { - // split the actions by category - $returned = $this->getActionCategoryFromName($row['name']); - $aActions = $returned[1]; - $isUrl = $returned[0]; - - // we work on the root table of the given TYPE (either ACTION or DOWNLOAD or OUTLINK etc.) - $currentTable =& $this->actionsTablesByType[$row['type']]; - - // go to the level of the subcategory - $end = count($aActions)-1; - for($level = 0 ; $level < $end; $level++) - { - $actionCategory = $aActions[$level]; - $currentTable =& $currentTable[$actionCategory]; - } - $actionName = $aActions[$end]; - - // create a new element in the array for the page - // we are careful to prefix the pageName with some value so that if a page has the same name - // as a category we don't overwrite or do other silly things - - // if the page name is not a URL we add a / before - if( !$isUrl ) - { - // we know that the concatenation of a space and the name of the action - // will always be unique as all the action names have been trimmed before reaching this point - $actionName = '/' . $actionName; - } - else - { - $actionName = ' ' . $actionName; - } - - // currentTable is now the array element corresponding the the action - // at this point we may be for example at the 4th level of depth in the hierarchy - $currentTable =& $currentTable[$actionName]; - - // add the row to the matching sub category subtable - if(!($currentTable instanceof Piwik_DataTable_Row)) - { - $currentTable = new Piwik_DataTable_Row( - array( Piwik_DataTable_Row::COLUMNS => - array( 'label' => (string)$actionName, - 'full_url' => (string)$row['name'], - ) - ) - ); - } - - foreach($row as $name => $value) - { - // we don't add this information as itnot pertinent - // name is already set as the label // and it has been cleaned from the categories and extracted from the initial string - // type is used to partition the different actions type in different table. Adding the info to the row would be a duplicate. - if($name != 'name' && $name != 'type') - { - // in some very rare case, we actually have twice the same action name with 2 different idaction - // this happens when 2 visitors visit the same new page at the same time, there is a SELECT and an INSERT for each new page, - // and in between the two the other visitor comes. - // here we handle the case where there is already a row for this action name, if this is the case we add the value - if(($alreadyValue = $currentTable->getColumn($name)) !== false) - { - $currentTable->setColumn($name, $alreadyValue+$value); - } - else - { - $currentTable->addColumn($name, $value); - } - } - } - - // if the exit_action was not recorded properly in the log_link_visit_action - // there would be an error message when getting the nb_hits column - // we must fake the record and add the columns - if($currentTable->getColumn('nb_hits') === false) - { - // to test this code: delete the entries in log_link_action_visit for - // a given exit_idaction - foreach($this->defaultRow->getColumns() as $name => $value) - { - $currentTable->addColumn($name, $value); - } - } - // simple count - $rowsProcessed++; - } - - // just to make sure php copies the last $currentTable in the $parentTable array - $currentTable =& $this->actionsTablesByType; - - return $rowsProcessed; - } -} - + } + } + return array( $isUrl, $split); + } + + + protected function updateActionsTableWithRowQuery($query) + { + $rowsProcessed = 0; + + while( $row = $query->fetch() ) + { + // split the actions by category + $returned = $this->getActionCategoryFromName($row['name']); + $aActions = $returned[1]; + $isUrl = $returned[0]; + + // we work on the root table of the given TYPE (either ACTION or DOWNLOAD or OUTLINK etc.) + $currentTable =& $this->actionsTablesByType[$row['type']]; + + // go to the level of the subcategory + $end = count($aActions)-1; + for($level = 0 ; $level < $end; $level++) + { + $actionCategory = $aActions[$level]; + $currentTable =& $currentTable[$actionCategory]; + } + $actionName = $aActions[$end]; + + // create a new element in the array for the page + // we are careful to prefix the pageName with some value so that if a page has the same name + // as a category we don't overwrite or do other silly things + + // if the page name is not a URL we add a / before + if( !$isUrl ) + { + // we know that the concatenation of a space and the name of the action + // will always be unique as all the action names have been trimmed before reaching this point + $actionName = '/' . $actionName; + } + else + { + $actionName = ' ' . $actionName; + } + + // currentTable is now the array element corresponding the the action + // at this point we may be for example at the 4th level of depth in the hierarchy + $currentTable =& $currentTable[$actionName]; + + // add the row to the matching sub category subtable + if(!($currentTable instanceof Piwik_DataTable_Row)) + { + $currentTable = new Piwik_DataTable_Row( + array( Piwik_DataTable_Row::COLUMNS => + array( 'label' => (string)$actionName, + 'full_url' => (string)$row['name'], + ) + ) + ); + } + + foreach($row as $name => $value) + { + // we don't add this information as itnot pertinent + // name is already set as the label // and it has been cleaned from the categories and extracted from the initial string + // type is used to partition the different actions type in different table. Adding the info to the row would be a duplicate. + if($name != 'name' && $name != 'type') + { + // in some very rare case, we actually have twice the same action name with 2 different idaction + // this happens when 2 visitors visit the same new page at the same time, there is a SELECT and an INSERT for each new page, + // and in between the two the other visitor comes. + // here we handle the case where there is already a row for this action name, if this is the case we add the value + if(($alreadyValue = $currentTable->getColumn($name)) !== false) + { + $currentTable->setColumn($name, $alreadyValue+$value); + } + else + { + $currentTable->addColumn($name, $value); + } + } + } + + // if the exit_action was not recorded properly in the log_link_visit_action + // there would be an error message when getting the nb_hits column + // we must fake the record and add the columns + if($currentTable->getColumn('nb_hits') === false) + { + // to test this code: delete the entries in log_link_action_visit for + // a given exit_idaction + foreach($this->defaultRow->getColumns() as $name => $value) + { + $currentTable->addColumn($name, $value); + } + } + // simple count + $rowsProcessed++; + } + + // just to make sure php copies the last $currentTable in the $parentTable array + $currentTable =& $this->actionsTablesByType; + + return $rowsProcessed; + } +} + diff --git a/plugins/Actions/Controller.php b/plugins/Actions/Controller.php index 4957756f3c..85d3cf9466 100644 --- a/plugins/Actions/Controller.php +++ b/plugins/Actions/Controller.php @@ -149,7 +149,7 @@ class Piwik_Actions_Controller extends Piwik_Controller // and each of them has 1 or 2 hits... $nbActionsLowPopulationThreshold = min($visitsInfo->getColumn('max_actions')-1, $nbActionsLowPopulationThreshold-1); - $view->setExcludeLowPopulation( $nbActionsLowPopulationThreshold, 'nb_hits' ); + $view->setExcludeLowPopulation( 'nb_hits', $nbActionsLowPopulationThreshold ); $view->main(); diff --git a/plugins/CoreHome/Controller.php b/plugins/CoreHome/Controller.php index 26c0875aa4..185cdfb2c2 100644 --- a/plugins/CoreHome/Controller.php +++ b/plugins/CoreHome/Controller.php @@ -1,26 +1,26 @@ -menu = Piwik_GetMenu(); - } + protected function setGeneralVariablesView($view) + { + parent::setGeneralVariablesView($view); + $view->menu = Piwik_GetMenu(); + } - public function showInContext() - { - $controllerName = Piwik_Common::getRequestVar('moduleToLoad'); - $actionName = Piwik_Common::getRequestVar('actionToLoad', 'index'); - - $view = $this->getDefaultIndexView(); - $view->basicHtmlView = true; - $view->content = Piwik_FrontController::getInstance()->fetchDispatch( $controllerName, $actionName ); - echo $view->render(); - } - - protected function getDefaultIndexView() - { - $view = new Piwik_View('CoreHome/templates/index.tpl'); - $this->setGeneralVariablesView($view); + public function showInContext() + { + $controllerName = Piwik_Common::getRequestVar('moduleToLoad'); + $actionName = Piwik_Common::getRequestVar('actionToLoad', 'index'); + + $view = $this->getDefaultIndexView(); + $view->basicHtmlView = true; + $view->content = Piwik_FrontController::getInstance()->fetchDispatch( $controllerName, $actionName ); + echo $view->render(); + } + + protected function getDefaultIndexView() + { + $view = new Piwik_View('CoreHome/templates/index.tpl'); + $this->setGeneralVariablesView($view); $view->content = ''; - return $view; + return $view; + } + + public function index() + { + $view = $this->getDefaultIndexView(); + echo $view->render(); } - - public function index() - { - $view = $this->getDefaultIndexView(); - echo $view->render(); - } -} +} diff --git a/plugins/CoreHome/templates/cloud.tpl b/plugins/CoreHome/templates/cloud.tpl index 8989e48a85..b3c208ccc0 100644 --- a/plugins/CoreHome/templates/cloud.tpl +++ b/plugins/CoreHome/templates/cloud.tpl @@ -1,4 +1,4 @@ -
+
{literal}