/plugins/Actions/API.php
PHP | 605 lines | 386 code | 61 blank | 158 comment | 32 complexity | 3e9a92f64c70a1334e44eb9f233f79ac MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
- <?php
- /**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
- namespace Piwik\Plugins\Actions;
- use Exception;
- use Piwik\API\Request;
- use Piwik\Archive;
- use Piwik\Common;
- use Piwik\DataTable;
- use Piwik\Date;
- use Piwik\Metrics;
- use Piwik\Piwik;
- use Piwik\Plugins\CustomVariables\API as APICustomVariables;
- use Piwik\Tracker\Action;
- use Piwik\Tracker\ActionSiteSearch;
- use Piwik\Tracker\PageUrl;
- /**
- * The Actions API lets you request reports for all your Visitor Actions: Page URLs, Page titles (Piwik Events),
- * File Downloads and Clicks on external websites.
- *
- * For example, "getPageTitles" will return all your page titles along with standard <a href='http://piwik.org/docs/analytics-api/reference/#toc-metric-definitions' target='_blank'>Actions metrics</a> for each row.
- *
- * It is also possible to request data for a specific Page Title with "getPageTitle"
- * and setting the parameter pageName to the page title you wish to request.
- * Similarly, you can request metrics for a given Page URL via "getPageUrl", a Download file via "getDownload"
- * and an outlink via "getOutlink".
- *
- * Note: pageName, pageUrl, outlinkUrl, downloadUrl parameters must be URL encoded before you call the API.
- * @method static \Piwik\Plugins\Actions\API getInstance()
- */
- class API extends \Piwik\Plugin\API
- {
- /**
- * Returns the list of metrics (pages, downloads, outlinks)
- *
- * @param int $idSite
- * @param string $period
- * @param string $date
- * @param bool|string $segment
- * @param bool|array $columns
- * @return DataTable
- */
- public function get($idSite, $period, $date, $segment = false, $columns = false)
- {
- Piwik::checkUserHasViewAccess($idSite);
- $archive = Archive::build($idSite, $period, $date, $segment);
- $metrics = Archiver::$actionsAggregateMetrics;
- $metrics['Actions_avg_time_generation'] = 'avg_time_generation';
- // get requested columns
- $columns = Piwik::getArrayFromApiParameter($columns);
- if (!empty($columns)) {
- // get the columns that are available and requested
- $columns = array_intersect($columns, array_values($metrics));
- $columns = array_values($columns); // make sure indexes are right
- $nameReplace = array();
- foreach ($columns as $i => $column) {
- $fullColumn = array_search($column, $metrics);
- $columns[$i] = $fullColumn;
- $nameReplace[$fullColumn] = $column;
- }
- if (false !== ($avgGenerationTimeRequested = array_search('Actions_avg_time_generation', $columns))) {
- unset($columns[$avgGenerationTimeRequested]);
- $avgGenerationTimeRequested = true;
- }
- } else {
- // get all columns
- unset($metrics['Actions_avg_time_generation']);
- $columns = array_keys($metrics);
- $nameReplace = & $metrics;
- $avgGenerationTimeRequested = true;
- }
- if ($avgGenerationTimeRequested) {
- $tempColumns[] = Archiver::METRIC_SUM_TIME_RECORD_NAME;
- $tempColumns[] = Archiver::METRIC_HITS_TIMED_RECORD_NAME;
- $columns = array_merge($columns, $tempColumns);
- $columns = array_unique($columns);
- $nameReplace[Archiver::METRIC_SUM_TIME_RECORD_NAME] = 'sum_time_generation';
- $nameReplace[Archiver::METRIC_HITS_TIMED_RECORD_NAME] = 'nb_hits_with_time_generation';
- }
- $table = $archive->getDataTableFromNumeric($columns);
- // replace labels (remove Actions_)
- $table->filter('ReplaceColumnNames', array($nameReplace));
- // compute avg generation time
- if ($avgGenerationTimeRequested) {
- $table->filter('ColumnCallbackAddColumnQuotient', array('avg_time_generation', 'sum_time_generation', 'nb_hits_with_time_generation', 3));
- $table->deleteColumns(array('sum_time_generation', 'nb_hits_with_time_generation'));
- }
- return $table;
- }
- /**
- * @param int $idSite
- * @param string $period
- * @param Date $date
- * @param bool $segment
- * @param bool $expanded
- * @param bool|int $idSubtable
- * @param bool|int $depth
- *
- * @return DataTable|DataTable\Map
- */
- public function getPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false,
- $depth = false)
- {
- $dataTable = $this->getDataTableFromArchive('Actions_actions_url', $idSite, $period, $date, $segment, $expanded, $idSubtable, $depth);
- $this->filterPageDatatable($dataTable);
- $this->filterActionsDataTable($dataTable, $expanded);
- return $dataTable;
- }
- /**
- * @param int $idSite
- * @param string $period
- * @param Date $date
- * @param bool $segment
- * @param bool $expanded
- * @param bool $idSubtable
- *
- * @return DataTable|DataTable\Map
- */
- public function getPageUrlsFollowingSiteSearch($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
- {
- $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable);
- $this->keepPagesFollowingSearch($dataTable);
- return $dataTable;
- }
- /**
- * @param int $idSite
- * @param string $period
- * @param Date $date
- * @param bool $segment
- * @param bool $expanded
- * @param bool $idSubtable
- *
- * @return DataTable|DataTable\Map
- */
- public function getPageTitlesFollowingSiteSearch($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
- {
- $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable);
- $this->keepPagesFollowingSearch($dataTable);
- return $dataTable;
- }
- /**
- * @param DataTable $dataTable
- */
- protected function keepPagesFollowingSearch($dataTable)
- {
- // Keep only pages which are following site search
- $dataTable->filter('ColumnCallbackDeleteRow', array(
- 'nb_hits_following_search',
- function ($value) {
- return $value <= 0;
- }
- ));
- }
- /**
- * Returns a DataTable with analytics information for every unique entry page URL, for
- * the specified site, period & segment.
- */
- public function getEntryPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
- {
- $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable);
- $this->filterNonEntryActions($dataTable);
- return $dataTable;
- }
- /**
- * Returns a DataTable with analytics information for every unique exit page URL, for
- * the specified site, period & segment.
- */
- public function getExitPageUrls($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
- {
- $dataTable = $this->getPageUrls($idSite, $period, $date, $segment, $expanded, $idSubtable);
- $this->filterNonExitActions($dataTable);
- return $dataTable;
- }
- public function getPageUrl($pageUrl, $idSite, $period, $date, $segment = false)
- {
- $callBackParameters = array('Actions_actions_url', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
- $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageUrl, Action::TYPE_PAGE_URL);
- $this->filterPageDatatable($dataTable);
- $this->filterActionsDataTable($dataTable);
- return $dataTable;
- }
- public function getPageTitles($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
- {
- $dataTable = $this->getDataTableFromArchive('Actions_actions', $idSite, $period, $date, $segment, $expanded, $idSubtable);
- $this->filterPageDatatable($dataTable);
- $this->filterActionsDataTable($dataTable, $expanded);
- return $dataTable;
- }
- /**
- * Returns a DataTable with analytics information for every unique entry page title
- * for the given site, time period & segment.
- */
- public function getEntryPageTitles($idSite, $period, $date, $segment = false, $expanded = false,
- $idSubtable = false)
- {
- $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable);
- $this->filterNonEntryActions($dataTable);
- return $dataTable;
- }
- /**
- * Returns a DataTable with analytics information for every unique exit page title
- * for the given site, time period & segment.
- */
- public function getExitPageTitles($idSite, $period, $date, $segment = false, $expanded = false,
- $idSubtable = false)
- {
- $dataTable = $this->getPageTitles($idSite, $period, $date, $segment, $expanded, $idSubtable);
- $this->filterNonExitActions($dataTable);
- return $dataTable;
- }
- public function getPageTitle($pageName, $idSite, $period, $date, $segment = false)
- {
- $callBackParameters = array('Actions_actions', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
- $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $pageName, Action::TYPE_PAGE_TITLE);
- $this->filterPageDatatable($dataTable);
- $this->filterActionsDataTable($dataTable);
- return $dataTable;
- }
- public function getDownloads($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
- {
- $dataTable = $this->getDataTableFromArchive('Actions_downloads', $idSite, $period, $date, $segment, $expanded, $idSubtable);
- $this->filterActionsDataTable($dataTable, $expanded);
- return $dataTable;
- }
- public function getDownload($downloadUrl, $idSite, $period, $date, $segment = false)
- {
- $callBackParameters = array('Actions_downloads', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
- $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $downloadUrl, Action::TYPE_DOWNLOAD);
- $this->filterActionsDataTable($dataTable);
- return $dataTable;
- }
- public function getOutlinks($idSite, $period, $date, $segment = false, $expanded = false, $idSubtable = false)
- {
- $dataTable = $this->getDataTableFromArchive('Actions_outlink', $idSite, $period, $date, $segment, $expanded, $idSubtable);
- $this->filterActionsDataTable($dataTable, $expanded);
- return $dataTable;
- }
- public function getOutlink($outlinkUrl, $idSite, $period, $date, $segment = false)
- {
- $callBackParameters = array('Actions_outlink', $idSite, $period, $date, $segment, $expanded = false, $idSubtable = false);
- $dataTable = $this->getFilterPageDatatableSearch($callBackParameters, $outlinkUrl, Action::TYPE_OUTLINK);
- $this->filterActionsDataTable($dataTable);
- return $dataTable;
- }
- public function getSiteSearchKeywords($idSite, $period, $date, $segment = false)
- {
- $dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment);
- $dataTable->deleteColumn(Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT);
- $this->filterPageDatatable($dataTable);
- $this->filterActionsDataTable($dataTable);
- $this->addPagesPerSearchColumn($dataTable);
- return $dataTable;
- }
- /**
- * Visitors can search, and then click "next" to view more results. This is the average number of search results pages viewed for this keyword.
- *
- * @param DataTable|DataTable\Simple|DataTable\Map $dataTable
- * @param string $columnToRead
- */
- protected function addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_hits')
- {
- $dataTable->filter('ColumnCallbackAddColumnQuotient', array('nb_pages_per_search', $columnToRead, 'nb_visits', $precision = 1));
- }
- protected function getSiteSearchKeywordsRaw($idSite, $period, $date, $segment)
- {
- $dataTable = $this->getDataTableFromArchive('Actions_sitesearch', $idSite, $period, $date, $segment, $expanded = false);
- return $dataTable;
- }
- public function getSiteSearchNoResultKeywords($idSite, $period, $date, $segment = false)
- {
- $dataTable = $this->getSiteSearchKeywordsRaw($idSite, $period, $date, $segment);
- // Delete all rows that have some results
- $dataTable->filter('ColumnCallbackDeleteRow',
- array(
- Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT,
- function ($value) {
- return $value < 1;
- }
- ));
- $dataTable->deleteRow(DataTable::ID_SUMMARY_ROW);
- $dataTable->deleteColumn(Metrics::INDEX_SITE_SEARCH_HAS_NO_RESULT);
- $this->filterPageDatatable($dataTable);
- $this->filterActionsDataTable($dataTable);
- $this->addPagesPerSearchColumn($dataTable);
- return $dataTable;
- }
- /**
- * @param int $idSite
- * @param string $period
- * @param Date $date
- * @param bool $segment
- *
- * @return DataTable|DataTable\Map
- */
- public function getSiteSearchCategories($idSite, $period, $date, $segment = false)
- {
- Actions::checkCustomVariablesPluginEnabled();
- $customVariables = APICustomVariables::getInstance()->getCustomVariables($idSite, $period, $date, $segment, $expanded = false, $_leavePiwikCoreVariables = true);
- $customVarNameToLookFor = ActionSiteSearch::CVAR_KEY_SEARCH_CATEGORY;
- $dataTable = new DataTable();
- // Handle case where date=last30&period=day
- // FIXMEA: this logic should really be refactored somewhere, this is ugly!
- if ($customVariables instanceof DataTable\Map) {
- $dataTable = $customVariables->getEmptyClone();
- $customVariableDatatables = $customVariables->getDataTables();
- $dataTables = $dataTable->getDataTables();
- foreach ($customVariableDatatables as $key => $customVariableTableForDate) {
- // we do not enter the IF, in the case idSite=1,3 AND period=day&date=datefrom,dateto,
- if ($customVariableTableForDate instanceof DataTable
- && $customVariableTableForDate->getMetadata(Archive\DataTableFactory::TABLE_METADATA_PERIOD_INDEX)
- ) {
- $row = $customVariableTableForDate->getRowFromLabel($customVarNameToLookFor);
- if ($row) {
- $dateRewrite = $customVariableTableForDate->getMetadata(Archive\DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getDateStart()->toString();
- $idSubtable = $row->getIdSubDataTable();
- $categories = APICustomVariables::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $dateRewrite, $idSubtable, $segment);
- $dataTable->addTable($categories, $key);
- }
- }
- }
- } elseif ($customVariables instanceof DataTable) {
- $row = $customVariables->getRowFromLabel($customVarNameToLookFor);
- if ($row) {
- $idSubtable = $row->getIdSubDataTable();
- $dataTable = APICustomVariables::getInstance()->getCustomVariablesValuesFromNameId($idSite, $period, $date, $idSubtable, $segment);
- }
- }
- $this->filterActionsDataTable($dataTable);
- $this->addPagesPerSearchColumn($dataTable, $columnToRead = 'nb_actions');
- return $dataTable;
- }
- /**
- * Will search in the DataTable for a Label matching the searched string
- * and return only the matching row, or an empty datatable
- */
- protected function getFilterPageDatatableSearch($callBackParameters, $search, $actionType, $table = false,
- $searchTree = false)
- {
- if ($searchTree === false) {
- // build the query parts that are searched inside the tree
- if ($actionType == Action::TYPE_PAGE_TITLE) {
- $searchedString = Common::unsanitizeInputValue($search);
- } else {
- $idSite = $callBackParameters[1];
- try {
- $searchedString = PageUrl::excludeQueryParametersFromUrl($search, $idSite);
- } catch (Exception $e) {
- $searchedString = $search;
- }
- }
- ArchivingHelper::reloadConfig();
- $searchTree = ArchivingHelper::getActionExplodedNames($searchedString, $actionType);
- }
- if ($table === false) {
- // fetch the data table
- $table = call_user_func_array(array($this, 'getDataTableFromArchive'), $callBackParameters);
- if ($table instanceof DataTable\Map) {
- // search an array of tables, e.g. when using date=last30
- // note that if the root is an array, we filter all children
- // if an array occurs inside the nested table, we only look for the first match (see below)
- $dataTableMap = $table->getEmptyClone();
- foreach ($table->getDataTables() as $label => $subTable) {
- $newSubTable = $this->doFilterPageDatatableSearch($callBackParameters, $subTable, $searchTree);
- $dataTableMap->addTable($newSubTable, $label);
- }
- return $dataTableMap;
- }
- }
- return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree);
- }
- /**
- * This looks very similar to LabelFilter.php should it be refactored somehow? FIXME
- */
- protected function doFilterPageDatatableSearch($callBackParameters, $table, $searchTree)
- {
- // filter a data table array
- if ($table instanceof DataTable\Map) {
- foreach ($table->getDataTables() as $subTable) {
- $filteredSubTable = $this->doFilterPageDatatableSearch($callBackParameters, $subTable, $searchTree);
- if ($filteredSubTable->getRowsCount() > 0) {
- // match found in a sub table, return and stop searching the others
- return $filteredSubTable;
- }
- }
- // nothing found in all sub tables
- return new DataTable;
- }
- // filter regular data table
- if ($table instanceof DataTable) {
- // search for the first part of the tree search
- $search = array_shift($searchTree);
- $row = $table->getRowFromLabel($search);
- if ($row === false) {
- // not found
- $result = new DataTable;
- $result->setAllTableMetadata($table->getAllTableMetadata());
- return $result;
- }
- // end of tree search reached
- if (count($searchTree) == 0) {
- $result = new DataTable();
- $result->addRow($row);
- $result->setAllTableMetadata($table->getAllTableMetadata());
- return $result;
- }
- // match found on this level and more levels remaining: go deeper
- $idSubTable = $row->getIdSubDataTable();
- $callBackParameters[6] = $idSubTable;
- /**
- * @var \Piwik\Period $period
- */
- $period = $table->getMetadata('period');
- if (!empty($period)) {
- $callBackParameters[3] = $period->getDateStart() . ',' . $period->getDateEnd();
- }
- $table = call_user_func_array(array($this, 'getDataTableFromArchive'), $callBackParameters);
- return $this->doFilterPageDatatableSearch($callBackParameters, $table, $searchTree);
- }
- throw new Exception("For this API function, DataTable " . get_class($table) . " is not supported");
- }
- /**
- * Common filters for Page URLs and Page Titles
- *
- * @param DataTable|DataTable\Simple|DataTable\Map $dataTable
- */
- protected function filterPageDatatable($dataTable)
- {
- $columnsToRemove = array('bounce_rate');
- $dataTable->queueFilter('ColumnDelete', array($columnsToRemove));
- // Average time on page = total time on page / number visits on that page
- $dataTable->queueFilter('ColumnCallbackAddColumnQuotient',
- array('avg_time_on_page',
- 'sum_time_spent',
- 'nb_visits',
- 0)
- );
- // Bounce rate = single page visits on this page / visits started on this page
- $dataTable->queueFilter('ColumnCallbackAddColumnPercentage',
- array('bounce_rate',
- 'entry_bounce_count',
- 'entry_nb_visits',
- 0));
- // % Exit = Number of visits that finished on this page / visits on this page
- $dataTable->queueFilter('ColumnCallbackAddColumnPercentage',
- array('exit_rate',
- 'exit_nb_visits',
- 'nb_visits',
- 0)
- );
- // Handle performance analytics
- $hasTimeGeneration = (array_sum($dataTable->getColumn(Metrics::INDEX_PAGE_SUM_TIME_GENERATION)) > 0);
- if ($hasTimeGeneration) {
- // Average generation time = total generation time / number of pageviews
- $precisionAvgTimeGeneration = 3;
- $dataTable->queueFilter('ColumnCallbackAddColumnQuotient',
- array('avg_time_generation',
- 'sum_time_generation',
- 'nb_hits_with_time_generation',
- $precisionAvgTimeGeneration)
- );
- $dataTable->queueFilter('ColumnDelete', array(array('sum_time_generation')));
- } else {
- // No generation time: remove it from the API output and add it to empty_columns metadata, so that
- // the columns can also be removed from the view
- $dataTable->filter('ColumnDelete', array(array(
- Metrics::INDEX_PAGE_SUM_TIME_GENERATION,
- Metrics::INDEX_PAGE_NB_HITS_WITH_TIME_GENERATION,
- Metrics::INDEX_PAGE_MIN_TIME_GENERATION,
- Metrics::INDEX_PAGE_MAX_TIME_GENERATION
- )));
- if ($dataTable instanceof DataTable) {
- $emptyColumns = $dataTable->getMetadata(DataTable::EMPTY_COLUMNS_METADATA_NAME);
- if (!is_array($emptyColumns)) {
- $emptyColumns = array();
- }
- $emptyColumns[] = 'sum_time_generation';
- $emptyColumns[] = 'avg_time_generation';
- $emptyColumns[] = 'min_time_generation';
- $emptyColumns[] = 'max_time_generation';
- $dataTable->setMetadata(DataTable::EMPTY_COLUMNS_METADATA_NAME, $emptyColumns);
- }
- }
- }
- /**
- * Common filters for all Actions API
- *
- * @param DataTable|DataTable\Simple|DataTable\Map $dataTable
- * @param bool $expanded
- */
- protected function filterActionsDataTable($dataTable, $expanded = false)
- {
- // Must be applied before Sort in this case, since the DataTable can contain both int and strings indexes
- // (in the transition period between pre 1.2 and post 1.2 datatable structure)
- $dataTable->filter('ReplaceColumnNames');
- $dataTable->filter('Sort', array('nb_visits', 'desc', $naturalSort = false, $expanded));
- $dataTable->queueFilter('ReplaceSummaryRowLabel');
- }
- /**
- * Removes DataTable rows referencing actions that were never the first action of a visit.
- *
- * @param DataTable $dataTable
- */
- private function filterNonEntryActions($dataTable)
- {
- $dataTable->filter('ColumnCallbackDeleteRow',
- array('entry_nb_visits',
- function ($visits) {
- return !strlen($visits);
- }
- )
- );
- }
- /**
- * Removes DataTable rows referencing actions that were never the last action of a visit.
- *
- * @param DataTable $dataTable
- */
- private function filterNonExitActions($dataTable)
- {
- $dataTable->filter('ColumnCallbackDeleteRow',
- array('exit_nb_visits',
- function ($visits) {
- return !strlen($visits);
- })
- );
- }
- protected function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded = false, $idSubtable = null, $depth = null)
- {
- $skipAggregationOfSubTables = false;
- if ($period == 'range'
- && empty($idSubtable)
- && empty($expanded)
- && !Request::shouldLoadFlatten()
- ) {
- $skipAggregationOfSubTables = false;
- }
- return Archive::getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable, $skipAggregationOfSubTables, $depth);
- }
- }