add piwik installation
This commit is contained in:
parent
90aa4ef157
commit
8c5d4f0c31
3197 changed files with 563902 additions and 0 deletions
81
www/analytics/core/DataTable/BaseFilter.php
Normal file
81
www/analytics/core/DataTable/BaseFilter.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
|
||||
/**
|
||||
* A filter is set of logic that manipulates a DataTable. Existing filters do things like,
|
||||
*
|
||||
* - remove rows
|
||||
* - change column values (change string to lowercase, truncate, etc.)
|
||||
* - add/remove columns or metadata (compute percentage values, add an 'icon' metadata based on the label, etc.)
|
||||
* - add/remove/edit subtable associated with rows
|
||||
* - etc.
|
||||
*
|
||||
* Filters are called with a DataTable instance and extra parameters that are specified
|
||||
* in {@link Piwik\DataTable::filter()} and {@link Piwik\DataTable::queueFilter()}.
|
||||
*
|
||||
* To see examples of Filters look at the existing ones in the Piwik\DataTable\BaseFilter
|
||||
* namespace.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
abstract class BaseFilter
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $enableRecursive = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function __construct(DataTable $table)
|
||||
{
|
||||
// empty
|
||||
}
|
||||
|
||||
/**
|
||||
* Manipulates a {@link DataTable} in some way.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
abstract public function filter($table);
|
||||
|
||||
/**
|
||||
* Enables/Disables recursive filtering. Whether this property is actually used
|
||||
* is up to the derived BaseFilter class.
|
||||
*
|
||||
* @param bool $enable
|
||||
*/
|
||||
public function enableRecursive($enable)
|
||||
{
|
||||
$this->enableRecursive = (bool)$enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a row's subtable, if one exists and is loaded in memory.
|
||||
*
|
||||
* @param Row $row The row whose subtable should be filter.
|
||||
*/
|
||||
public function filterSubTable(Row $row)
|
||||
{
|
||||
if (!$this->enableRecursive) {
|
||||
return;
|
||||
}
|
||||
if ($row->isSubtableLoaded()) {
|
||||
$subTable = Manager::getInstance()->getTable($row->getIdSubDataTable());
|
||||
$this->filter($subTable);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
www/analytics/core/DataTable/Bridges.php
Normal file
28
www/analytics/core/DataTable/Bridges.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* This contains the bridge classes which were used prior to Piwik 2.0
|
||||
* The serialized reports contains these classes below, which were not using namespaces yet
|
||||
*/
|
||||
namespace {
|
||||
|
||||
use Piwik\DataTable\Row\DataTableSummaryRow;
|
||||
use Piwik\DataTable\Row;
|
||||
|
||||
class Piwik_DataTable_Row_DataTableSummary extends DataTableSummaryRow
|
||||
{
|
||||
}
|
||||
|
||||
class Piwik_DataTable_Row extends Row
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
30
www/analytics/core/DataTable/DataTableInterface.php
Normal file
30
www/analytics/core/DataTable/DataTableInterface.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable;
|
||||
|
||||
/**
|
||||
* The DataTable Interface
|
||||
*
|
||||
*/
|
||||
interface DataTableInterface
|
||||
{
|
||||
public function getRowsCount();
|
||||
public function queueFilter($className, $parameters = array());
|
||||
public function applyQueuedFilters();
|
||||
public function filter($className, $parameters = array());
|
||||
public function getFirstRow();
|
||||
public function __toString();
|
||||
public function enableRecursiveSort();
|
||||
public function renameColumn($oldName, $newName);
|
||||
public function deleteColumns($columns, $deleteRecursiveInSubtables = false);
|
||||
public function deleteRow($id);
|
||||
public function deleteColumn($name);
|
||||
public function getColumn($name);
|
||||
public function getColumns();
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
use Piwik\DataTable\Row;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Metrics;
|
||||
|
||||
/**
|
||||
* Adds processed metrics columns to a {@link DataTable} using metrics that already exist.
|
||||
*
|
||||
* Columns added are:
|
||||
*
|
||||
* - **conversion_rate**: percent value of `nb_visits_converted / nb_visits
|
||||
* - **nb_actions_per_visit**: `nb_actions / nb_visits`
|
||||
* - **avg_time_on_site**: in number of seconds, `round(visit_length / nb_visits)`. Not
|
||||
* pretty formatted.
|
||||
* - **bounce_rate**: percent value of `bounce_count / nb_visits`
|
||||
*
|
||||
* Adding the **filter_add_columns_when_show_all_columns** query parameter to
|
||||
* an API request will trigger the execution of this Filter.
|
||||
*
|
||||
* _Note: This filter must be called before {@link ReplaceColumnNames} is called._
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $dataTable->filter('AddColumnsProcessedMetrics');
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class AddColumnsProcessedMetrics extends BaseFilter
|
||||
{
|
||||
protected $invalidDivision = 0;
|
||||
protected $roundPrecision = 2;
|
||||
protected $deleteRowsWithNoVisit = true;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The table to eventually filter.
|
||||
* @param bool $deleteRowsWithNoVisit Whether to delete rows with no visits or not.
|
||||
*/
|
||||
public function __construct($table, $deleteRowsWithNoVisit = true)
|
||||
{
|
||||
$this->deleteRowsWithNoVisit = $deleteRowsWithNoVisit;
|
||||
parent::__construct($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the processed metrics. See {@link AddColumnsProcessedMetrics} for
|
||||
* more information.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
$rowsIdToDelete = array();
|
||||
foreach ($table->getRows() as $key => $row) {
|
||||
$nbVisits = $this->getColumn($row, Metrics::INDEX_NB_VISITS);
|
||||
$nbActions = $this->getColumn($row, Metrics::INDEX_NB_ACTIONS);
|
||||
if ($nbVisits == 0
|
||||
&& $nbActions == 0
|
||||
&& $this->deleteRowsWithNoVisit
|
||||
) {
|
||||
// case of keyword/website/campaign with a conversion for this day,
|
||||
// but no visit, we don't show it
|
||||
$rowsIdToDelete[] = $key;
|
||||
continue;
|
||||
}
|
||||
|
||||
$nbVisitsConverted = (int)$this->getColumn($row, Metrics::INDEX_NB_VISITS_CONVERTED);
|
||||
if ($nbVisitsConverted > 0) {
|
||||
$conversionRate = round(100 * $nbVisitsConverted / $nbVisits, $this->roundPrecision);
|
||||
try {
|
||||
$row->addColumn('conversion_rate', $conversionRate . "%");
|
||||
} catch (\Exception $e) {
|
||||
// conversion_rate can be defined upstream apparently? FIXME
|
||||
}
|
||||
}
|
||||
|
||||
if ($nbVisits == 0) {
|
||||
$actionsPerVisit = $averageTimeOnSite = $bounceRate = $this->invalidDivision;
|
||||
} else {
|
||||
// nb_actions / nb_visits => Actions/visit
|
||||
// sum_visit_length / nb_visits => Avg. Time on Site
|
||||
// bounce_count / nb_visits => Bounce Rate
|
||||
$actionsPerVisit = round($nbActions / $nbVisits, $this->roundPrecision);
|
||||
$visitLength = $this->getColumn($row, Metrics::INDEX_SUM_VISIT_LENGTH);
|
||||
$averageTimeOnSite = round($visitLength / $nbVisits, $rounding = 0);
|
||||
$bounceRate = round(100 * $this->getColumn($row, Metrics::INDEX_BOUNCE_COUNT) / $nbVisits, $this->roundPrecision);
|
||||
}
|
||||
try {
|
||||
$row->addColumn('nb_actions_per_visit', $actionsPerVisit);
|
||||
$row->addColumn('avg_time_on_site', $averageTimeOnSite);
|
||||
// It could be useful for API users to have raw sum length value.
|
||||
//$row->addMetadata('sum_visit_length', $visitLength);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
try {
|
||||
$row->addColumn('bounce_rate', $bounceRate . "%");
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
$this->filterSubTable($row);
|
||||
}
|
||||
$table->deleteRows($rowsIdToDelete);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns column from a given row.
|
||||
* Will work with 2 types of datatable
|
||||
* - raw datatables coming from the archive DB, which columns are int indexed
|
||||
* - datatables processed resulting of API calls, which columns have human readable english names
|
||||
*
|
||||
* @param Row|array $row
|
||||
* @param int $columnIdRaw see consts in Archive::
|
||||
* @param bool|array $mappingIdToName
|
||||
* @return mixed Value of column, false if not found
|
||||
*/
|
||||
protected function getColumn($row, $columnIdRaw, $mappingIdToName = false)
|
||||
{
|
||||
if (empty($mappingIdToName)) {
|
||||
$mappingIdToName = Metrics::$mappingFromIdToName;
|
||||
}
|
||||
$columnIdReadable = $mappingIdToName[$columnIdRaw];
|
||||
if ($row instanceof Row) {
|
||||
$raw = $row->getColumn($columnIdRaw);
|
||||
if ($raw !== false) {
|
||||
return $raw;
|
||||
}
|
||||
return $row->getColumn($columnIdReadable);
|
||||
}
|
||||
if (isset($row[$columnIdRaw])) {
|
||||
return $row[$columnIdRaw];
|
||||
}
|
||||
if (isset($row[$columnIdReadable])) {
|
||||
return $row[$columnIdReadable];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Exception;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Metrics;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Tracker\GoalManager;
|
||||
|
||||
/**
|
||||
* Adds goal related metrics to a {@link DataTable} using metrics that already exist.
|
||||
*
|
||||
* Metrics added are:
|
||||
* - **revenue_per_visit**: total goal and ecommerce revenue / nb_visits
|
||||
* - **goal_%idGoal%_conversion_rate**: the conversion rate. There will be one of
|
||||
* these columns for each goal that exists
|
||||
* for the site.
|
||||
* - **goal_%idGoal%_nb_conversions**: the number of conversions. There will be one of
|
||||
* these columns for each goal that exists
|
||||
* for the site.
|
||||
* - **goal_%idGoal%_revenue_per_visit**: goal revenue / nb_visits. There will be one of
|
||||
* these columns for each goal that exists
|
||||
* for the site.
|
||||
* - **goal_%idGoal%_revenue**: goal revenue. There will be one of
|
||||
* these columns for each goal that exists
|
||||
* for the site.
|
||||
* - **goal_%idGoal%_avg_order_revenue**: goal revenue / number of orders or abandoned
|
||||
* carts. Only for ecommerce order and abandoned cart
|
||||
* reports.
|
||||
* - **goal_%idGoal%_items**: number of items. Only for ecommerce order and abandoned cart
|
||||
* reports.
|
||||
*
|
||||
* Adding the **filter_update_columns_when_show_all_goals** query parameter to
|
||||
* an API request will trigger the execution of this Filter.
|
||||
*
|
||||
* _Note: This filter must be called before {@link ReplaceColumnNames} is called._
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $dataTable->filter('AddColumnsProcessedMetricsGoal',
|
||||
* array($enable = true, $idGoal = Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class AddColumnsProcessedMetricsGoal extends AddColumnsProcessedMetrics
|
||||
{
|
||||
/**
|
||||
* Process main goal metrics: conversion rate, revenue per visit
|
||||
*/
|
||||
const GOALS_MINIMAL_REPORT = -2;
|
||||
|
||||
/**
|
||||
* Process main goal metrics, and conversion rate per goal
|
||||
*/
|
||||
const GOALS_OVERVIEW = -1;
|
||||
|
||||
/**
|
||||
* Process all goal and per-goal metrics
|
||||
*/
|
||||
const GOALS_FULL_TABLE = 0;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The table that will eventually filtered.
|
||||
* @param bool $enable Always set to true.
|
||||
* @param string $processOnlyIdGoal Defines what metrics to add (don't process metrics when you don't display them).
|
||||
* If self::GOALS_FULL_TABLE, all Goal metrics (and per goal metrics) will be processed.
|
||||
* If self::GOALS_OVERVIEW, only the main goal metrics will be added.
|
||||
* If an int > 0, then will process only metrics for this specific Goal.
|
||||
*/
|
||||
public function __construct($table, $enable = true, $processOnlyIdGoal)
|
||||
{
|
||||
$this->processOnlyIdGoal = $processOnlyIdGoal;
|
||||
$this->isEcommerce = $this->processOnlyIdGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER || $this->processOnlyIdGoal == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART;
|
||||
parent::__construct($table);
|
||||
// Ensure that all rows with no visit but conversions will be displayed
|
||||
$this->deleteRowsWithNoVisit = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the processed metrics. See {@link AddColumnsProcessedMetrics} for
|
||||
* more information.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
// Add standard processed metrics
|
||||
parent::filter($table);
|
||||
$roundingPrecision = GoalManager::REVENUE_PRECISION;
|
||||
$expectedColumns = array();
|
||||
foreach ($table->getRows() as $key => $row) {
|
||||
$currentColumns = $row->getColumns();
|
||||
$newColumns = array();
|
||||
|
||||
// visits could be undefined when there is a conversion but no visit
|
||||
$nbVisits = (int)$this->getColumn($row, Metrics::INDEX_NB_VISITS);
|
||||
$conversions = (int)$this->getColumn($row, Metrics::INDEX_NB_CONVERSIONS);
|
||||
$goals = $this->getColumn($currentColumns, Metrics::INDEX_GOALS);
|
||||
if ($goals) {
|
||||
$revenue = 0;
|
||||
foreach ($goals as $goalId => $goalMetrics) {
|
||||
if ($goalId == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART) {
|
||||
continue;
|
||||
}
|
||||
if ($goalId >= GoalManager::IDGOAL_ORDER
|
||||
|| $goalId == Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER
|
||||
) {
|
||||
$revenue += (int)$this->getColumn($goalMetrics, Metrics::INDEX_GOAL_REVENUE, Metrics::$mappingFromIdToNameGoal);
|
||||
}
|
||||
}
|
||||
|
||||
if ($revenue == 0) {
|
||||
$revenue = (int)$this->getColumn($currentColumns, Metrics::INDEX_REVENUE);
|
||||
}
|
||||
if (!isset($currentColumns['revenue_per_visit'])) {
|
||||
// If no visit for this metric, but some conversions, we still want to display some kind of "revenue per visit"
|
||||
// even though it will actually be in this edge case "Revenue per conversion"
|
||||
$revenuePerVisit = $this->invalidDivision;
|
||||
if ($nbVisits > 0
|
||||
|| $conversions > 0
|
||||
) {
|
||||
$revenuePerVisit = round($revenue / ($nbVisits == 0 ? $conversions : $nbVisits), $roundingPrecision);
|
||||
}
|
||||
$newColumns['revenue_per_visit'] = $revenuePerVisit;
|
||||
}
|
||||
if ($this->processOnlyIdGoal == self::GOALS_MINIMAL_REPORT) {
|
||||
$row->addColumns($newColumns);
|
||||
continue;
|
||||
}
|
||||
// Display per goal metrics
|
||||
// - conversion rate
|
||||
// - conversions
|
||||
// - revenue per visit
|
||||
foreach ($goals as $goalId => $goalMetrics) {
|
||||
$goalId = str_replace("idgoal=", "", $goalId);
|
||||
if (($this->processOnlyIdGoal > self::GOALS_FULL_TABLE
|
||||
|| $this->isEcommerce)
|
||||
&& $this->processOnlyIdGoal != $goalId
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
$conversions = (int)$this->getColumn($goalMetrics, Metrics::INDEX_GOAL_NB_CONVERSIONS, Metrics::$mappingFromIdToNameGoal);
|
||||
|
||||
// Goal Conversion rate
|
||||
$name = 'goal_' . $goalId . '_conversion_rate';
|
||||
if ($nbVisits == 0) {
|
||||
$value = $this->invalidDivision;
|
||||
} else {
|
||||
$value = round(100 * $conversions / $nbVisits, $roundingPrecision);
|
||||
}
|
||||
$newColumns[$name] = $value . "%";
|
||||
$expectedColumns[$name] = true;
|
||||
|
||||
// When the table is displayed by clicking on the flag icon, we only display the columns
|
||||
// Visits, Conversions, Per goal conversion rate, Revenue
|
||||
if ($this->processOnlyIdGoal == self::GOALS_OVERVIEW) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Goal Conversions
|
||||
$name = 'goal_' . $goalId . '_nb_conversions';
|
||||
$newColumns[$name] = $conversions;
|
||||
$expectedColumns[$name] = true;
|
||||
|
||||
// Goal Revenue per visit
|
||||
$name = 'goal_' . $goalId . '_revenue_per_visit';
|
||||
// See comment above for $revenuePerVisit
|
||||
$goalRevenue = (float)$this->getColumn($goalMetrics, Metrics::INDEX_GOAL_REVENUE, Metrics::$mappingFromIdToNameGoal);
|
||||
$revenuePerVisit = round($goalRevenue / ($nbVisits == 0 ? $conversions : $nbVisits), $roundingPrecision);
|
||||
$newColumns[$name] = $revenuePerVisit;
|
||||
$expectedColumns[$name] = true;
|
||||
|
||||
// Total revenue
|
||||
$name = 'goal_' . $goalId . '_revenue';
|
||||
$newColumns[$name] = $goalRevenue;
|
||||
$expectedColumns[$name] = true;
|
||||
|
||||
if ($this->isEcommerce) {
|
||||
|
||||
// AOV Average Order Value
|
||||
$name = 'goal_' . $goalId . '_avg_order_revenue';
|
||||
$newColumns[$name] = $goalRevenue / $conversions;
|
||||
$expectedColumns[$name] = true;
|
||||
|
||||
// Items qty
|
||||
$name = 'goal_' . $goalId . '_items';
|
||||
$newColumns[$name] = $this->getColumn($goalMetrics, Metrics::INDEX_GOAL_ECOMMERCE_ITEMS, Metrics::$mappingFromIdToNameGoal);
|
||||
$expectedColumns[$name] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// conversion_rate can be defined upstream apparently? FIXME
|
||||
try {
|
||||
$row->addColumns($newColumns);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
$expectedColumns['revenue_per_visit'] = true;
|
||||
|
||||
// make sure all goals values are set, 0 by default
|
||||
// if no value then sorting would put at the end
|
||||
$expectedColumns = array_keys($expectedColumns);
|
||||
$rows = $table->getRows();
|
||||
foreach ($rows as &$row) {
|
||||
foreach ($expectedColumns as $name) {
|
||||
if (false === $row->getColumn($name)) {
|
||||
$value = 0;
|
||||
if (strpos($name, 'conversion_rate') !== false) {
|
||||
$value = '0%';
|
||||
}
|
||||
$row->addColumn($name, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
www/analytics/core/DataTable/Filter/AddSummaryRow.php
Normal file
52
www/analytics/core/DataTable/Filter/AddSummaryRow.php
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row\DataTableSummaryRow;
|
||||
|
||||
/**
|
||||
* Adds a summary row to {@link DataTable}s that contains the sum of all other table rows.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $dataTable->filter('AddSummaryRow');
|
||||
*
|
||||
* // use a human readable label for the summary row (instead of '-1')
|
||||
* $dataTable->filter('AddSummaryRow', array($labelSummaryRow = Piwik::translate('General_Total')));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class AddSummaryRow extends BaseFilter
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The table that will be filtered.
|
||||
* @param int $labelSummaryRow The value of the label column for the new row.
|
||||
*/
|
||||
public function __construct($table, $labelSummaryRow = DataTable::LABEL_SUMMARY_ROW)
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->labelSummaryRow = $labelSummaryRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the filter. See {@link AddSummaryRow}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
$row = new DataTableSummaryRow($table);
|
||||
$row->setColumn('label', $this->labelSummaryRow);
|
||||
$table->addSummaryRow($row);
|
||||
}
|
||||
}
|
||||
168
www/analytics/core/DataTable/Filter/BeautifyRangeLabels.php
Normal file
168
www/analytics/core/DataTable/Filter/BeautifyRangeLabels.php
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Piwik;
|
||||
|
||||
/**
|
||||
* A {@link DataTable} filter that replaces range label columns with prettier,
|
||||
* human-friendlier versions.
|
||||
*
|
||||
* When reports that summarize data over a set of ranges (such as the
|
||||
* reports in the **VisitorInterest** plugin) are archived, they are
|
||||
* archived with labels that read as: '$min-$max' or '$min+'. These labels
|
||||
* have no units and can look like '1-1'.
|
||||
*
|
||||
* This filter can be used to clean up and add units to those range labels. To
|
||||
* do this, you supply a string to use when the range specifies only
|
||||
* one unit (ie '1-1') and another format string when the range specifies
|
||||
* more than one unit (ie '2-2', '3-5' or '6+').
|
||||
*
|
||||
* This filter can be extended to vary exactly how ranges are prettified based
|
||||
* on the range values found in the DataTable. To see an example of this,
|
||||
* take a look at the {@link BeautifyTimeRangeLabels} filter.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $dataTable->queueFilter('BeautifyRangeLabels', array("1 visit", "%s visits"));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class BeautifyRangeLabels extends ColumnCallbackReplace
|
||||
{
|
||||
/**
|
||||
* The string to use when the range being beautified is between 1-1 units.
|
||||
* @var string
|
||||
*/
|
||||
protected $labelSingular;
|
||||
|
||||
/**
|
||||
* The format string to use when the range being beautified references more than
|
||||
* one unit.
|
||||
* @var string
|
||||
*/
|
||||
protected $labelPlural;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable that will be filtered.
|
||||
* @param string $labelSingular The string to use when the range being beautified
|
||||
* is equal to '1-1 units', eg `"1 visit"`.
|
||||
* @param string $labelPlural The string to use when the range being beautified
|
||||
* references more than one unit. This must be a format
|
||||
* string that takes one string parameter, eg, `"%s visits"`.
|
||||
*/
|
||||
public function __construct($table, $labelSingular, $labelPlural)
|
||||
{
|
||||
parent::__construct($table, 'label', array($this, 'beautify'), array());
|
||||
|
||||
$this->labelSingular = $labelSingular;
|
||||
$this->labelPlural = $labelPlural;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautifies a range label and returns the pretty result. See {@link BeautifyRangeLabels}.
|
||||
*
|
||||
* @param string $value The range string. This must be in either a '$min-$max' format
|
||||
* a '$min+' format.
|
||||
* @return string The pretty range label.
|
||||
*/
|
||||
public function beautify($value)
|
||||
{
|
||||
// if there's more than one element, handle as a range w/ an upper bound
|
||||
if (strpos($value, "-") !== false) {
|
||||
// get the range
|
||||
sscanf($value, "%d - %d", $lowerBound, $upperBound);
|
||||
|
||||
// if the lower bound is the same as the upper bound make sure the singular label
|
||||
// is used
|
||||
if ($lowerBound == $upperBound) {
|
||||
return $this->getSingleUnitLabel($value, $lowerBound);
|
||||
} else {
|
||||
return $this->getRangeLabel($value, $lowerBound, $upperBound);
|
||||
}
|
||||
} // if there's one element, handle as a range w/ no upper bound
|
||||
else {
|
||||
// get the lower bound
|
||||
sscanf($value, "%d", $lowerBound);
|
||||
|
||||
if ($lowerBound !== null) {
|
||||
$plusEncoded = urlencode('+');
|
||||
$plusLen = strlen($plusEncoded);
|
||||
$len = strlen($value);
|
||||
|
||||
// if the label doesn't end with a '+', append it
|
||||
if ($len < $plusLen || substr($value, $len - $plusLen) != $plusEncoded) {
|
||||
$value .= $plusEncoded;
|
||||
}
|
||||
|
||||
return $this->getUnboundedLabel($value, $lowerBound);
|
||||
} else {
|
||||
// if no lower bound can be found, this isn't a valid range. in this case
|
||||
// we assume its a translation key and try to translate it.
|
||||
return Piwik::translate(trim($value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautifies and returns a range label whose range spans over one unit, ie
|
||||
* 1-1, 2-2 or 3-3.
|
||||
*
|
||||
* This function can be overridden in derived types to customize beautifcation
|
||||
* behavior based on the range values.
|
||||
*
|
||||
* @param string $oldLabel The original label value.
|
||||
* @param int $lowerBound The lower bound of the range.
|
||||
* @return string The pretty range label.
|
||||
*/
|
||||
public function getSingleUnitLabel($oldLabel, $lowerBound)
|
||||
{
|
||||
if ($lowerBound == 1) {
|
||||
return $this->labelSingular;
|
||||
} else {
|
||||
return sprintf($this->labelPlural, $lowerBound);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautifies and returns a range label whose range is bounded and spans over
|
||||
* more than one unit, ie 1-5, 5-10 but NOT 11+.
|
||||
*
|
||||
* This function can be overridden in derived types to customize beautifcation
|
||||
* behavior based on the range values.
|
||||
*
|
||||
* @param string $oldLabel The original label value.
|
||||
* @param int $lowerBound The lower bound of the range.
|
||||
* @param int $upperBound The upper bound of the range.
|
||||
* @return string The pretty range label.
|
||||
*/
|
||||
public function getRangeLabel($oldLabel, $lowerBound, $upperBound)
|
||||
{
|
||||
return sprintf($this->labelPlural, $oldLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautifies and returns a range label whose range is unbounded, ie
|
||||
* 5+, 10+, etc.
|
||||
*
|
||||
* This function can be overridden in derived types to customize beautifcation
|
||||
* behavior based on the range values.
|
||||
*
|
||||
* @param string $oldLabel The original label value.
|
||||
* @param int $lowerBound The lower bound of the range.
|
||||
* @return string The pretty range label.
|
||||
*/
|
||||
public function getUnboundedLabel($oldLabel, $lowerBound)
|
||||
{
|
||||
return sprintf($this->labelPlural, $oldLabel);
|
||||
}
|
||||
}
|
||||
121
www/analytics/core/DataTable/Filter/BeautifyTimeRangeLabels.php
Normal file
121
www/analytics/core/DataTable/Filter/BeautifyTimeRangeLabels.php
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
|
||||
/**
|
||||
* A {@link DataTable} filter that replaces range labels whose values are in seconds with
|
||||
* prettier, human-friendlier versions.
|
||||
*
|
||||
* This filter customizes the behavior of the {@link BeautifyRangeLabels} filter
|
||||
* so range values that are less than one minute are displayed in seconds but
|
||||
* other ranges are displayed in minutes.
|
||||
*
|
||||
* **Basic usage**
|
||||
*
|
||||
* $dataTable->filter('BeautifyTimeRangeLabels', array("%1$s-%2$s min", "1 min", "%s min"));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class BeautifyTimeRangeLabels extends BeautifyRangeLabels
|
||||
{
|
||||
/**
|
||||
* A format string used to create pretty range labels when the range's
|
||||
* lower bound is between 0 and 60.
|
||||
*
|
||||
* This format string must take two numeric parameters, one for each
|
||||
* range bound.
|
||||
*/
|
||||
protected $labelSecondsPlural;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable this filter will run over.
|
||||
* @param string $labelSecondsPlural A string to use when beautifying range labels
|
||||
* whose lower bound is between 0 and 60. Must be
|
||||
* a format string that takes two numeric params.
|
||||
* @param string $labelMinutesSingular A string to use when replacing a range that
|
||||
* equals 60-60 (or 1 minute - 1 minute).
|
||||
* @param string $labelMinutesPlural A string to use when replacing a range that
|
||||
* spans multiple minutes. This must be a
|
||||
* format string that takes one string parameter.
|
||||
*/
|
||||
public function __construct($table, $labelSecondsPlural, $labelMinutesSingular, $labelMinutesPlural)
|
||||
{
|
||||
parent::__construct($table, $labelMinutesSingular, $labelMinutesPlural);
|
||||
|
||||
$this->labelSecondsPlural = $labelSecondsPlural;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautifies and returns a range label whose range spans over one unit, ie
|
||||
* 1-1, 2-2 or 3-3.
|
||||
*
|
||||
* If the lower bound of the range is less than 60 the pretty range label
|
||||
* will be in seconds. Otherwise, it will be in minutes.
|
||||
*
|
||||
* @param string $oldLabel The original label value.
|
||||
* @param int $lowerBound The lower bound of the range.
|
||||
* @return string The pretty range label.
|
||||
*/
|
||||
public function getSingleUnitLabel($oldLabel, $lowerBound)
|
||||
{
|
||||
if ($lowerBound < 60) {
|
||||
return sprintf($this->labelSecondsPlural, $lowerBound, $lowerBound);
|
||||
} else if ($lowerBound == 60) {
|
||||
return $this->labelSingular;
|
||||
} else {
|
||||
return sprintf($this->labelPlural, ceil($lowerBound / 60));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautifies and returns a range label whose range is bounded and spans over
|
||||
* more than one unit, ie 1-5, 5-10 but NOT 11+.
|
||||
*
|
||||
* If the lower bound of the range is less than 60 the pretty range label
|
||||
* will be in seconds. Otherwise, it will be in minutes.
|
||||
*
|
||||
* @param string $oldLabel The original label value.
|
||||
* @param int $lowerBound The lower bound of the range.
|
||||
* @param int $upperBound The upper bound of the range.
|
||||
* @return string The pretty range label.
|
||||
*/
|
||||
public function getRangeLabel($oldLabel, $lowerBound, $upperBound)
|
||||
{
|
||||
if ($lowerBound < 60) {
|
||||
return sprintf($this->labelSecondsPlural, $lowerBound, $upperBound);
|
||||
} else {
|
||||
return sprintf($this->labelPlural, ceil($lowerBound / 60) . "-" . ceil($upperBound / 60));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Beautifies and returns a range label whose range is unbounded, ie
|
||||
* 5+, 10+, etc.
|
||||
*
|
||||
* If the lower bound of the range is less than 60 the pretty range label
|
||||
* will be in seconds. Otherwise, it will be in minutes.
|
||||
*
|
||||
* @param string $oldLabel The original label value.
|
||||
* @param int $lowerBound The lower bound of the range.
|
||||
* @return string The pretty range label.
|
||||
*/
|
||||
public function getUnboundedLabel($oldLabel, $lowerBound)
|
||||
{
|
||||
if ($lowerBound < 60) {
|
||||
return sprintf($this->labelSecondsPlural, $lowerBound);
|
||||
} else {
|
||||
// since we're using minutes, we use floor so 1801s+ will be 30m+ and not 31m+
|
||||
return sprintf($this->labelPlural, "" . floor($lowerBound / 60) . urlencode('+'));
|
||||
}
|
||||
}
|
||||
}
|
||||
187
www/analytics/core/DataTable/Filter/CalculateEvolutionFilter.php
Executable file
187
www/analytics/core/DataTable/Filter/CalculateEvolutionFilter.php
Executable file
|
|
@ -0,0 +1,187 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
use Piwik\Site;
|
||||
|
||||
/**
|
||||
* A {@link DataTable} filter that calculates the evolution of a metric and adds
|
||||
* it to each row as a percentage.
|
||||
*
|
||||
* **This filter cannot be used as an argument to {@link Piwik\DataTable::filter()}** since
|
||||
* it requires corresponding data from another DataTable. Instead,
|
||||
* you must manually perform a binary filter (see the **MultiSites** API for an
|
||||
* example).
|
||||
*
|
||||
* The evolution metric is calculated as:
|
||||
*
|
||||
* ((currentValue - pastValue) / pastValue) * 100
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class CalculateEvolutionFilter extends ColumnCallbackAddColumnPercentage
|
||||
{
|
||||
/**
|
||||
* The the DataTable that contains past data.
|
||||
*
|
||||
* @var DataTable
|
||||
*/
|
||||
protected $pastDataTable;
|
||||
|
||||
/**
|
||||
* Tells if column being added is the revenue evolution column.
|
||||
*/
|
||||
protected $isRevenueEvolution = null;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable being filtered.
|
||||
* @param DataTable $pastDataTable The DataTable containing data for the period in the past.
|
||||
* @param string $columnToAdd The column to add evolution data to, eg, `'visits_evolution'`.
|
||||
* @param string $columnToRead The column to use to calculate evolution data, eg, `'nb_visits'`.
|
||||
* @param int $quotientPrecision The precision to use when rounding the evolution value.
|
||||
*/
|
||||
public function __construct($table, $pastDataTable, $columnToAdd, $columnToRead, $quotientPrecision = 0)
|
||||
{
|
||||
parent::__construct(
|
||||
$table, $columnToAdd, $columnToRead, $columnToRead, $quotientPrecision, $shouldSkipRows = true);
|
||||
|
||||
$this->pastDataTable = $pastDataTable;
|
||||
|
||||
$this->isRevenueEvolution = $columnToAdd == 'revenue_evolution';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the difference between the column in the specific row and its
|
||||
* sister column in the past DataTable.
|
||||
*
|
||||
* @param Row $row
|
||||
* @return int|float
|
||||
*/
|
||||
protected function getDividend($row)
|
||||
{
|
||||
$currentValue = $row->getColumn($this->columnValueToRead);
|
||||
|
||||
// if the site this is for doesn't support ecommerce & this is for the revenue_evolution column,
|
||||
// we don't add the new column
|
||||
if ($currentValue === false
|
||||
&& $this->isRevenueEvolution
|
||||
&& !Site::isEcommerceEnabledFor($row->getColumn('label'))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pastRow = $this->getPastRowFromCurrent($row);
|
||||
if ($pastRow) {
|
||||
$pastValue = $pastRow->getColumn($this->columnValueToRead);
|
||||
} else {
|
||||
$pastValue = 0;
|
||||
}
|
||||
|
||||
return $currentValue - $pastValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the column in $row's sister row in the past
|
||||
* DataTable.
|
||||
*
|
||||
* @param Row $row
|
||||
* @return int|float
|
||||
*/
|
||||
protected function getDivisor($row)
|
||||
{
|
||||
$pastRow = $this->getPastRowFromCurrent($row);
|
||||
if (!$pastRow) return 0;
|
||||
|
||||
return $pastRow->getColumn($this->columnNameUsedAsDivisor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates and formats a quotient based on a divisor and dividend.
|
||||
*
|
||||
* Unlike ColumnCallbackAddColumnPercentage's,
|
||||
* version of this method, this method will return 100% if the past
|
||||
* value of a metric is 0, and the current value is not 0. For a
|
||||
* value representative of an evolution, this makes sense.
|
||||
*
|
||||
* @param int|float $value The dividend.
|
||||
* @param int|float $divisor
|
||||
* @return string
|
||||
*/
|
||||
protected function formatValue($value, $divisor)
|
||||
{
|
||||
$value = self::getPercentageValue($value, $divisor, $this->quotientPrecision);
|
||||
$value = self::appendPercentSign($value);
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function. Returns the current row in the past DataTable.
|
||||
*
|
||||
* @param Row $row The row in the 'current' DataTable.
|
||||
* @return bool|Row
|
||||
*/
|
||||
protected function getPastRowFromCurrent($row)
|
||||
{
|
||||
return $this->pastDataTable->getRowFromLabel($row->getColumn('label'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the evolution percentage for two arbitrary values.
|
||||
*
|
||||
* @param float|int $currentValue The current metric value.
|
||||
* @param float|int $pastValue The value of the metric in the past. We measure the % change
|
||||
* from this value to $currentValue.
|
||||
* @param float|int $quotientPrecision The quotient precision to round to.
|
||||
* @param bool $appendPercentSign Whether to append a '%' sign to the end of the number or not.
|
||||
*
|
||||
* @return string The evolution percent, eg `'15%'`.
|
||||
*/
|
||||
public static function calculate($currentValue, $pastValue, $quotientPrecision = 0, $appendPercentSign = true)
|
||||
{
|
||||
$number = self::getPercentageValue($currentValue - $pastValue, $pastValue, $quotientPrecision);
|
||||
if ($appendPercentSign) {
|
||||
$number = self::appendPercentSign($number);
|
||||
}
|
||||
return $number;
|
||||
}
|
||||
|
||||
public static function appendPercentSign($number)
|
||||
{
|
||||
return $number . '%';
|
||||
}
|
||||
|
||||
public static function prependPlusSignToNumber($number)
|
||||
{
|
||||
if ($number > 0) {
|
||||
$number = '+' . $number;
|
||||
}
|
||||
return $number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an evolution percent based on a value & divisor.
|
||||
*/
|
||||
private static function getPercentageValue($value, $divisor, $quotientPrecision)
|
||||
{
|
||||
if ($value == 0) {
|
||||
$evolution = 0;
|
||||
} elseif ($divisor == 0) {
|
||||
$evolution = 100;
|
||||
} else {
|
||||
$evolution = ($value / $divisor) * 100;
|
||||
}
|
||||
|
||||
$evolution = round($evolution, $quotientPrecision);
|
||||
return $evolution;
|
||||
}
|
||||
}
|
||||
96
www/analytics/core/DataTable/Filter/ColumnCallbackAddColumn.php
Executable file
96
www/analytics/core/DataTable/Filter/ColumnCallbackAddColumn.php
Executable file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* Adds a new column to every row of a {@link DataTable} based on the result of callback.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $callback = function ($visits, $timeSpent) {
|
||||
* return round($timeSpent / $visits, 2);
|
||||
* };
|
||||
*
|
||||
* $dataTable->filter('ColumnCallbackAddColumn', array(array('nb_visits', 'sum_time_spent'), 'avg_time_on_site', $callback));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class ColumnCallbackAddColumn extends BaseFilter
|
||||
{
|
||||
/**
|
||||
* The names of the columns to pass to the callback.
|
||||
*/
|
||||
private $columns;
|
||||
|
||||
/**
|
||||
* The name of the column to add.
|
||||
*/
|
||||
private $columnToAdd;
|
||||
|
||||
/**
|
||||
* The callback to apply to each row of the DataTable. The result is added as
|
||||
* the value of a new column.
|
||||
*/
|
||||
private $functionToApply;
|
||||
|
||||
/**
|
||||
* Extra parameters to pass to the callback.
|
||||
*/
|
||||
private $functionParameters;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable that will be filtered.
|
||||
* @param array|string $columns The names of the columns to pass to the callback.
|
||||
* @param string $columnToAdd The name of the column to add.
|
||||
* @param callable $functionToApply The callback to apply to each row of a DataTable. The columns
|
||||
* specified in `$columns` are passed to this callback.
|
||||
* @param array $functionParameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
|
||||
* instead.
|
||||
*/
|
||||
public function __construct($table, $columns, $columnToAdd, $functionToApply, $functionParameters = array())
|
||||
{
|
||||
parent::__construct($table);
|
||||
|
||||
if (!is_array($columns)) {
|
||||
$columns = array($columns);
|
||||
}
|
||||
|
||||
$this->columns = $columns;
|
||||
$this->columnToAdd = $columnToAdd;
|
||||
$this->functionToApply = $functionToApply;
|
||||
$this->functionParameters = $functionParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link ColumnCallbackAddColumn}.
|
||||
*
|
||||
* @param DataTable $table The table to filter.
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
foreach ($table->getRows() as $row) {
|
||||
$columnValues = array();
|
||||
foreach ($this->columns as $column) {
|
||||
$columnValues[] = $row->getColumn($column);
|
||||
}
|
||||
|
||||
$parameters = array_merge($columnValues, $this->functionParameters);
|
||||
$value = call_user_func_array($this->functionToApply, $parameters);
|
||||
|
||||
$row->setColumn($this->columnToAdd, $value);
|
||||
|
||||
$this->filterSubTable($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\Piwik;
|
||||
|
||||
/**
|
||||
* Calculates a percentage value for each row of a {@link DataTable} and adds the result
|
||||
* to each row.
|
||||
*
|
||||
* See {@link ColumnCallbackAddColumnQuotient} for more information.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $nbVisits = // ... get the visits for a period ...
|
||||
* $dataTable->queueFilter('ColumnCallbackAddColumnPercentage', array('nb_visits', 'nb_visits_percentage', $nbVisits, 1));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class ColumnCallbackAddColumnPercentage extends ColumnCallbackAddColumnQuotient
|
||||
{
|
||||
/**
|
||||
* Formats the given value as a percentage.
|
||||
*
|
||||
* @param number $value
|
||||
* @param number $divisor
|
||||
* @return string
|
||||
*/
|
||||
protected function formatValue($value, $divisor)
|
||||
{
|
||||
return Piwik::getPercentageSafe($value, $divisor, $this->quotientPrecision) . '%';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
|
||||
/**
|
||||
* Calculates the quotient of two columns and adds the result as a new column
|
||||
* for each row of a DataTable.
|
||||
*
|
||||
* This filter is used to calculate rate values (eg, `'bounce_rate'`), averages
|
||||
* (eg, `'avg_time_on_page'`) and other types of values.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $dataTable->queueFilter('ColumnCallbackAddColumnQuotient', array('bounce_rate', 'bounce_count', 'nb_visits', $precision = 2));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class ColumnCallbackAddColumnQuotient extends BaseFilter
|
||||
{
|
||||
protected $table;
|
||||
protected $columnValueToRead;
|
||||
protected $columnNameToAdd;
|
||||
protected $columnNameUsedAsDivisor;
|
||||
protected $totalValueUsedAsDivisor;
|
||||
protected $quotientPrecision;
|
||||
protected $shouldSkipRows;
|
||||
protected $getDivisorFromSummaryRow;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable that will eventually be filtered.
|
||||
* @param string $columnNameToAdd The name of the column to add the quotient value to.
|
||||
* @param string $columnValueToRead The name of the column that holds the dividend.
|
||||
* @param number|string $divisorValueOrDivisorColumnName
|
||||
* Either numeric value to use as the divisor for every row,
|
||||
* or the name of the column whose value should be used as the
|
||||
* divisor.
|
||||
* @param int $quotientPrecision The precision to use when rounding the quotient.
|
||||
* @param bool|number $shouldSkipRows Whether rows w/o the column to read should be skipped or not.
|
||||
* @param bool $getDivisorFromSummaryRow Whether to get the divisor from the summary row or the current
|
||||
* row iteration.
|
||||
*/
|
||||
public function __construct($table, $columnNameToAdd, $columnValueToRead, $divisorValueOrDivisorColumnName,
|
||||
$quotientPrecision = 0, $shouldSkipRows = false, $getDivisorFromSummaryRow = false)
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->table = $table;
|
||||
$this->columnValueToRead = $columnValueToRead;
|
||||
$this->columnNameToAdd = $columnNameToAdd;
|
||||
if (is_numeric($divisorValueOrDivisorColumnName)) {
|
||||
$this->totalValueUsedAsDivisor = $divisorValueOrDivisorColumnName;
|
||||
} else {
|
||||
$this->columnNameUsedAsDivisor = $divisorValueOrDivisorColumnName;
|
||||
}
|
||||
$this->quotientPrecision = $quotientPrecision;
|
||||
$this->shouldSkipRows = $shouldSkipRows;
|
||||
$this->getDivisorFromSummaryRow = $getDivisorFromSummaryRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link ColumnCallbackAddColumnQuotient}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
foreach ($table->getRows() as $key => $row) {
|
||||
$value = $this->getDividend($row);
|
||||
if ($value === false && $this->shouldSkipRows) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Delete existing column if it exists
|
||||
$existingValue = $row->getColumn($this->columnNameToAdd);
|
||||
if ($existingValue !== false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$divisor = $this->getDivisor($row);
|
||||
|
||||
$formattedValue = $this->formatValue($value, $divisor);
|
||||
$row->addColumn($this->columnNameToAdd, $formattedValue);
|
||||
|
||||
$this->filterSubTable($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the given value
|
||||
*
|
||||
* @param number $value
|
||||
* @param number $divisor
|
||||
* @return float|int
|
||||
*/
|
||||
protected function formatValue($value, $divisor)
|
||||
{
|
||||
$quotient = 0;
|
||||
if ($divisor > 0 && $value > 0) {
|
||||
$quotient = round($value / $divisor, $this->quotientPrecision);
|
||||
}
|
||||
return $quotient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dividend to use when calculating the new column value. Can
|
||||
* be overridden by descendent classes to customize behavior.
|
||||
*
|
||||
* @param Row $row The row being modified.
|
||||
* @return int|float
|
||||
*/
|
||||
protected function getDividend($row)
|
||||
{
|
||||
return $row->getColumn($this->columnValueToRead);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the divisor to use when calculating the new column value. Can
|
||||
* be overridden by descendent classes to customize behavior.
|
||||
*
|
||||
* @param Row $row The row being modified.
|
||||
* @return int|float
|
||||
*/
|
||||
protected function getDivisor($row)
|
||||
{
|
||||
if (!is_null($this->totalValueUsedAsDivisor)) {
|
||||
return $this->totalValueUsedAsDivisor;
|
||||
} else if ($this->getDivisorFromSummaryRow) {
|
||||
$summaryRow = $this->table->getRowFromId(DataTable::ID_SUMMARY_ROW);
|
||||
return $summaryRow->getColumn($this->columnNameUsedAsDivisor);
|
||||
} else {
|
||||
return $row->getColumn($this->columnNameUsedAsDivisor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* Executes a callback for each row of a {@link DataTable} and adds the result as a new
|
||||
* row metadata value.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $dataTable->filter('ColumnCallbackAddMetadata', array('label', 'logo', 'Piwik\Plugins\MyPlugin\getLogoFromLabel'));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class ColumnCallbackAddMetadata extends BaseFilter
|
||||
{
|
||||
private $columnsToRead;
|
||||
private $functionToApply;
|
||||
private $functionParameters;
|
||||
private $metadataToAdd;
|
||||
private $applyToSummaryRow;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable instance that will be filtered.
|
||||
* @param string|array $columnsToRead The columns to read from each row and pass on to the callback.
|
||||
* @param string $metadataToAdd The name of the metadata field that will be added to each row.
|
||||
* @param callable $functionToApply The callback to apply for each row.
|
||||
* @param array $functionParameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
|
||||
* instead.
|
||||
* @param bool $applyToSummaryRow Whether the callback should be applied to the summary row or not.
|
||||
*/
|
||||
public function __construct($table, $columnsToRead, $metadataToAdd, $functionToApply = null,
|
||||
$functionParameters = null, $applyToSummaryRow = true)
|
||||
{
|
||||
parent::__construct($table);
|
||||
|
||||
if (!is_array($columnsToRead)) {
|
||||
$columnsToRead = array($columnsToRead);
|
||||
}
|
||||
$this->columnsToRead = $columnsToRead;
|
||||
|
||||
$this->functionToApply = $functionToApply;
|
||||
$this->functionParameters = $functionParameters;
|
||||
$this->metadataToAdd = $metadataToAdd;
|
||||
$this->applyToSummaryRow = $applyToSummaryRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link ColumnCallbackAddMetadata}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
foreach ($table->getRows() as $key => $row) {
|
||||
if (!$this->applyToSummaryRow && $key == DataTable::ID_SUMMARY_ROW) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$parameters = array();
|
||||
foreach ($this->columnsToRead as $columnsToRead) {
|
||||
$parameters[] = $row->getColumn($columnsToRead);
|
||||
}
|
||||
|
||||
if (!is_null($this->functionParameters)) {
|
||||
$parameters = array_merge($parameters, $this->functionParameters);
|
||||
}
|
||||
if (!is_null($this->functionToApply)) {
|
||||
$newValue = call_user_func_array($this->functionToApply, $parameters);
|
||||
} else {
|
||||
$newValue = $parameters[0];
|
||||
}
|
||||
if ($newValue !== false) {
|
||||
$row->addMetadata($this->metadataToAdd, $newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* Deletes all rows for which a callback returns true.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $labelsToRemove = array('label1', 'label2', 'label2');
|
||||
* $dataTable->filter('ColumnCallbackDeleteRow', array('label', function ($label) use ($labelsToRemove) {
|
||||
* return in_array($label, $labelsToRemove);
|
||||
* }));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class ColumnCallbackDeleteRow extends BaseFilter
|
||||
{
|
||||
private $columnToFilter;
|
||||
private $function;
|
||||
private $functionParams;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable that will be filtered eventually.
|
||||
* @param array|string $columnsToFilter The column or array of columns that should be
|
||||
* passed to the callback.
|
||||
* @param callback $function The callback that determines whether a row should be deleted
|
||||
* or not. Should return `true` if the row should be deleted.
|
||||
* @param array $functionParams deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
|
||||
* instead.
|
||||
*/
|
||||
public function __construct($table, $columnsToFilter, $function, $functionParams = array())
|
||||
{
|
||||
parent::__construct($table);
|
||||
|
||||
if (!is_array($functionParams)) {
|
||||
$functionParams = array($functionParams);
|
||||
}
|
||||
|
||||
if (!is_array($columnsToFilter)) {
|
||||
$columnsToFilter = array($columnsToFilter);
|
||||
}
|
||||
|
||||
$this->function = $function;
|
||||
$this->columnsToFilter = $columnsToFilter;
|
||||
$this->functionParams = $functionParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the given data table
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
foreach ($table->getRows() as $key => $row) {
|
||||
$params = array();
|
||||
foreach ($this->columnsToFilter as $column) {
|
||||
$params[] = $row->getColumn($column);
|
||||
}
|
||||
|
||||
$params = array_merge($params, $this->functionParams);
|
||||
if (call_user_func_array($this->function, $params) === true) {
|
||||
$table->deleteRow($key);
|
||||
}
|
||||
|
||||
$this->filterSubTable($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
122
www/analytics/core/DataTable/Filter/ColumnCallbackReplace.php
Normal file
122
www/analytics/core/DataTable/Filter/ColumnCallbackReplace.php
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
|
||||
/**
|
||||
* Replaces one or more column values in each row of a DataTable with the results
|
||||
* of a callback.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $truncateString = function ($value, $truncateLength) {
|
||||
* if (strlen($value) > $truncateLength) {
|
||||
* return substr(0, $truncateLength);
|
||||
* } else {
|
||||
* return $value;
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* // label, url and truncate_length are columns in $dataTable
|
||||
* $dataTable->filter('ColumnCallbackReplace', array('label', 'url'), $truncateString, null, array('truncate_length'));
|
||||
*
|
||||
*/
|
||||
class ColumnCallbackReplace extends BaseFilter
|
||||
{
|
||||
private $columnsToFilter;
|
||||
private $functionToApply;
|
||||
private $functionParameters;
|
||||
private $extraColumnParameters;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable to filter.
|
||||
* @param array|string $columnsToFilter The columns whose values should be passed to the callback
|
||||
* and then replaced with the callback's result.
|
||||
* @param callable $functionToApply The function to execute. Must take the column value as a parameter
|
||||
* and return a value that will be used to replace the original.
|
||||
* @param array|null $functionParameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
|
||||
* instead.
|
||||
* @param array $extraColumnParameters Extra column values that should be passed to the callback, but
|
||||
* shouldn't be replaced.
|
||||
*/
|
||||
public function __construct($table, $columnsToFilter, $functionToApply, $functionParameters = null,
|
||||
$extraColumnParameters = array())
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->functionToApply = $functionToApply;
|
||||
$this->functionParameters = $functionParameters;
|
||||
|
||||
if (!is_array($columnsToFilter)) {
|
||||
$columnsToFilter = array($columnsToFilter);
|
||||
}
|
||||
|
||||
$this->columnsToFilter = $columnsToFilter;
|
||||
$this->extraColumnParameters = $extraColumnParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link ColumnCallbackReplace}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
foreach ($table->getRows() as $key => $row) {
|
||||
$extraColumnParameters = array();
|
||||
foreach ($this->extraColumnParameters as $columnName) {
|
||||
$extraColumnParameters[] = $row->getColumn($columnName);
|
||||
}
|
||||
|
||||
foreach ($this->columnsToFilter as $column) {
|
||||
// when a value is not defined, we set it to zero by default (rather than displaying '-')
|
||||
$value = $this->getElementToReplace($row, $column);
|
||||
if ($value === false) {
|
||||
$value = 0;
|
||||
}
|
||||
|
||||
$parameters = array_merge(array($value), $extraColumnParameters);
|
||||
if (!is_null($this->functionParameters)) {
|
||||
$parameters = array_merge($parameters, $this->functionParameters);
|
||||
}
|
||||
$newValue = call_user_func_array($this->functionToApply, $parameters);
|
||||
$this->setElementToReplace($row, $column, $newValue);
|
||||
$this->filterSubTable($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the given column within given row with the given value
|
||||
*
|
||||
* @param Row $row
|
||||
* @param string $columnToFilter
|
||||
* @param mixed $newValue
|
||||
*/
|
||||
protected function setElementToReplace($row, $columnToFilter, $newValue)
|
||||
{
|
||||
$row->setColumn($columnToFilter, $newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element that should be replaced
|
||||
*
|
||||
* @param Row $row
|
||||
* @param string $columnToFilter
|
||||
* @return mixed
|
||||
*/
|
||||
protected function getElementToReplace($row, $columnToFilter)
|
||||
{
|
||||
return $row->getColumn($columnToFilter);
|
||||
}
|
||||
}
|
||||
150
www/analytics/core/DataTable/Filter/ColumnDelete.php
Normal file
150
www/analytics/core/DataTable/Filter/ColumnDelete.php
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* Filter that will remove columns from a {@link DataTable} using either a blacklist,
|
||||
* whitelist or both.
|
||||
*
|
||||
* This filter is used to handle the **hideColumn** and **showColumn** query parameters.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $columnsToRemove = array('nb_hits', 'nb_pageviews');
|
||||
* $dataTable->filter('ColumnDelete', array($columnsToRemove));
|
||||
*
|
||||
* $columnsToKeep = array('nb_visits');
|
||||
* $dataTable->filter('ColumnDelete', array(array(), $columnsToKeep));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class ColumnDelete extends BaseFilter
|
||||
{
|
||||
/**
|
||||
* The columns that should be removed from DataTable rows.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $columnsToRemove;
|
||||
|
||||
/**
|
||||
* The columns that should be kept in DataTable rows. All other columns will be
|
||||
* removed. If a column is in $columnsToRemove and this variable, it will NOT be kept.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $columnsToKeep;
|
||||
|
||||
/**
|
||||
* Hack: when specifying "showColumns", sometimes we'd like to also keep columns that "look" like a given column,
|
||||
* without manually specifying all these columns (which may not be possible if column names are generated dynamically)
|
||||
*
|
||||
* Column will be kept, if they match any name in the $columnsToKeep, or if they look like anyColumnToKeep__anythingHere
|
||||
*/
|
||||
const APPEND_TO_COLUMN_NAME_TO_KEEP = '__';
|
||||
|
||||
/**
|
||||
* Delete the column, only if the value was zero
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $deleteIfZeroOnly;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable instance that will eventually be filtered.
|
||||
* @param array|string $columnsToRemove An array of column names or a comma-separated list of
|
||||
* column names. These columns will be removed.
|
||||
* @param array|string $columnsToKeep An array of column names that should be kept or a
|
||||
* comma-separated list of column names. Columns not in
|
||||
* this list will be removed.
|
||||
* @param bool $deleteIfZeroOnly If true, columns will be removed only if their value is 0.
|
||||
*/
|
||||
public function __construct($table, $columnsToRemove, $columnsToKeep = array(), $deleteIfZeroOnly = false)
|
||||
{
|
||||
parent::__construct($table);
|
||||
|
||||
if (is_string($columnsToRemove)) {
|
||||
$columnsToRemove = $columnsToRemove == '' ? array() : explode(',', $columnsToRemove);
|
||||
}
|
||||
|
||||
if (is_string($columnsToKeep)) {
|
||||
$columnsToKeep = $columnsToKeep == '' ? array() : explode(',', $columnsToKeep);
|
||||
}
|
||||
|
||||
$this->columnsToRemove = $columnsToRemove;
|
||||
$this->columnsToKeep = array_flip($columnsToKeep); // flip so we can use isset instead of in_array
|
||||
$this->deleteIfZeroOnly = $deleteIfZeroOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link ColumnDelete}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
// always do recursive filter
|
||||
$this->enableRecursive(true);
|
||||
$recurse = false; // only recurse if there are columns to remove/keep
|
||||
|
||||
// remove columns specified in $this->columnsToRemove
|
||||
if (!empty($this->columnsToRemove)) {
|
||||
foreach ($table->getRows() as $row) {
|
||||
foreach ($this->columnsToRemove as $column) {
|
||||
if ($this->deleteIfZeroOnly) {
|
||||
$value = $row->getColumn($column);
|
||||
if ($value === false || !empty($value)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$row->deleteColumn($column);
|
||||
}
|
||||
}
|
||||
|
||||
$recurse = true;
|
||||
}
|
||||
|
||||
// remove columns not specified in $columnsToKeep
|
||||
if (!empty($this->columnsToKeep)) {
|
||||
foreach ($table->getRows() as $row) {
|
||||
foreach ($row->getColumns() as $name => $value) {
|
||||
|
||||
$keep = false;
|
||||
// @see self::APPEND_TO_COLUMN_NAME_TO_KEEP
|
||||
foreach ($this->columnsToKeep as $nameKeep => $true) {
|
||||
if (strpos($name, $nameKeep . self::APPEND_TO_COLUMN_NAME_TO_KEEP) === 0) {
|
||||
$keep = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$keep
|
||||
&& $name != 'label' // label cannot be removed via whitelisting
|
||||
&& !isset($this->columnsToKeep[$name])
|
||||
) {
|
||||
$row->deleteColumn($name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$recurse = true;
|
||||
}
|
||||
|
||||
// recurse
|
||||
if ($recurse) {
|
||||
foreach ($table->getRows() as $row) {
|
||||
$this->filterSubTable($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
www/analytics/core/DataTable/Filter/ExcludeLowPopulation.php
Normal file
90
www/analytics/core/DataTable/Filter/ExcludeLowPopulation.php
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* Deletes all rows for which a specific column has a value that is lower than
|
||||
* specified minimum threshold value.
|
||||
*
|
||||
* **Basic usage examples**
|
||||
*
|
||||
* // remove all countries from UserCountry.getCountry that have less than 3 visits
|
||||
* $dataTable = // ... get a DataTable whose queued filters have been run ...
|
||||
* $dataTable->filter('ExcludeLowPopulation', array('nb_visits', 3));
|
||||
*
|
||||
* // remove all countries from UserCountry.getCountry whose percent of total visits is less than 5%
|
||||
* $dataTable = // ... get a DataTable whose queued filters have been run ...
|
||||
* $dataTable->filter('ExcludeLowPopulation', array('nb_visits', false, 0.05));
|
||||
*
|
||||
* // remove all countries from UserCountry.getCountry whose bounce rate is less than 10%
|
||||
* $dataTable = // ... get a DataTable that has a numerical bounce_rate column ...
|
||||
* $dataTable->filter('ExcludeLowPopulation', array('bounce_rate', 0.10));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class ExcludeLowPopulation extends BaseFilter
|
||||
{
|
||||
const MINIMUM_SIGNIFICANT_PERCENTAGE_THRESHOLD = 0.02;
|
||||
|
||||
/**
|
||||
* The minimum value to enforce in a datatable for a specified column. Rows found with
|
||||
* a value less than this are removed.
|
||||
*
|
||||
* @var number
|
||||
*/
|
||||
private $minimumValue;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable that will be filtered eventually.
|
||||
* @param string $columnToFilter The name of the column whose value will determine whether
|
||||
* a row is deleted or not.
|
||||
* @param number|false $minimumValue The minimum column value. Rows with column values <
|
||||
* this number will be deleted. If false,
|
||||
* `$minimumPercentageThreshold` is used.
|
||||
* @param bool|float $minimumPercentageThreshold If supplied, column values must be a greater
|
||||
* percentage of the sum of all column values than
|
||||
* this precentage.
|
||||
*/
|
||||
public function __construct($table, $columnToFilter, $minimumValue, $minimumPercentageThreshold = false)
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->columnToFilter = $columnToFilter;
|
||||
|
||||
if ($minimumValue == 0) {
|
||||
if ($minimumPercentageThreshold === false) {
|
||||
$minimumPercentageThreshold = self::MINIMUM_SIGNIFICANT_PERCENTAGE_THRESHOLD;
|
||||
}
|
||||
$allValues = $table->getColumn($this->columnToFilter);
|
||||
$sumValues = array_sum($allValues);
|
||||
$minimumValue = $sumValues * $minimumPercentageThreshold;
|
||||
}
|
||||
|
||||
$this->minimumValue = $minimumValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link ExcludeLowPopulation}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
$minimumValue = $this->minimumValue;
|
||||
$isValueLowPopulation = function ($value) use ($minimumValue) {
|
||||
return $value < $minimumValue;
|
||||
};
|
||||
|
||||
$table->filter('ColumnCallbackDeleteRow', array($this->columnToFilter, $isValueLowPopulation));
|
||||
}
|
||||
}
|
||||
104
www/analytics/core/DataTable/Filter/GroupBy.php
Executable file
104
www/analytics/core/DataTable/Filter/GroupBy.php
Executable file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* DataTable filter that will group {@link DataTable} rows together based on the results
|
||||
* of a reduce function. Rows with the same reduce result will be summed and merged.
|
||||
*
|
||||
* _NOTE: This filter should never be queued, it must be applied directly on a {@link DataTable}._
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* // group URLs by host
|
||||
* $dataTable->filter('GroupBy', array('label', function ($labelUrl) {
|
||||
* return parse_url($labelUrl, PHP_URL_HOST);
|
||||
* }));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class GroupBy extends BaseFilter
|
||||
{
|
||||
/**
|
||||
* The name of the columns to reduce.
|
||||
* @var string
|
||||
*/
|
||||
private $groupByColumn;
|
||||
|
||||
/**
|
||||
* A callback that modifies the $groupByColumn of each row in some way. Rows with
|
||||
* the same reduction result will be added together.
|
||||
*/
|
||||
private $reduceFunction;
|
||||
|
||||
/**
|
||||
* Extra parameters to pass to the reduce function.
|
||||
*/
|
||||
private $parameters;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable to filter.
|
||||
* @param string $groupByColumn The column name to reduce.
|
||||
* @param callable $reduceFunction The reduce function. This must alter the `$groupByColumn`
|
||||
* columng in some way.
|
||||
* @param array $parameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
|
||||
* instead.
|
||||
*/
|
||||
public function __construct($table, $groupByColumn, $reduceFunction, $parameters = array())
|
||||
{
|
||||
parent::__construct($table);
|
||||
|
||||
$this->groupByColumn = $groupByColumn;
|
||||
$this->reduceFunction = $reduceFunction;
|
||||
$this->parameters = $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link GroupBy}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
$groupByRows = array();
|
||||
$nonGroupByRowIds = array();
|
||||
|
||||
foreach ($table->getRows() as $rowId => $row) {
|
||||
// skip the summary row
|
||||
if ($rowId == DataTable::ID_SUMMARY_ROW) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// reduce the group by column of this row
|
||||
$groupByColumnValue = $row->getColumn($this->groupByColumn);
|
||||
$parameters = array_merge(array($groupByColumnValue), $this->parameters);
|
||||
$groupByValue = call_user_func_array($this->reduceFunction, $parameters);
|
||||
|
||||
if (!isset($groupByRows[$groupByValue])) {
|
||||
// if we haven't encountered this group by value before, we mark this row as a
|
||||
// row to keep, and change the group by column to the reduced value.
|
||||
$groupByRows[$groupByValue] = $row;
|
||||
$row->setColumn($this->groupByColumn, $groupByValue);
|
||||
} else {
|
||||
// if we have already encountered this group by value, we add this row to the
|
||||
// row that will be kept, and mark this one for deletion
|
||||
$groupByRows[$groupByValue]->sumRow($row, $copyMeta = true, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME));
|
||||
$nonGroupByRowIds[] = $rowId;
|
||||
}
|
||||
}
|
||||
|
||||
// delete the unneeded rows.
|
||||
$table->deleteRows($nonGroupByRowIds);
|
||||
}
|
||||
}
|
||||
69
www/analytics/core/DataTable/Filter/Limit.php
Normal file
69
www/analytics/core/DataTable/Filter/Limit.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* Delete all rows from the table that are not in the given [offset, offset+limit) range.
|
||||
*
|
||||
* **Basic example usage**
|
||||
*
|
||||
* // delete all rows from 5 -> 15
|
||||
* $dataTable->filter('Limit', array(5, 10));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class Limit extends BaseFilter
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable that will be filtered eventually.
|
||||
* @param int $offset The starting row index to keep.
|
||||
* @param int $limit Number of rows to keep (specify -1 to keep all rows).
|
||||
* @param bool $keepSummaryRow Whether to keep the summary row or not.
|
||||
*/
|
||||
public function __construct($table, $offset, $limit = -1, $keepSummaryRow = false)
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->offset = $offset;
|
||||
|
||||
$this->limit = $limit;
|
||||
$this->keepSummaryRow = $keepSummaryRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Limit}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
$table->setMetadata(DataTable::TOTAL_ROWS_BEFORE_LIMIT_METADATA_NAME, $table->getRowsCount());
|
||||
|
||||
if ($this->keepSummaryRow) {
|
||||
$summaryRow = $table->getRowFromId(DataTable::ID_SUMMARY_ROW);
|
||||
}
|
||||
|
||||
// we delete from 0 to offset
|
||||
if ($this->offset > 0) {
|
||||
$table->deleteRowsOffset(0, $this->offset);
|
||||
}
|
||||
// at this point the array has offset less elements. We delete from limit to the end
|
||||
if ($this->limit >= 0) {
|
||||
$table->deleteRowsOffset($this->limit);
|
||||
}
|
||||
|
||||
if ($this->keepSummaryRow && !empty($summaryRow)) {
|
||||
$table->addSummaryRow($summaryRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* Executes a callback for each row of a {@link DataTable} and adds the result to the
|
||||
* row as a metadata value. Only metadata values are passed to the callback.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* // add a logo metadata based on the url metadata
|
||||
* $dataTable->filter('MetadataCallbackAddMetadata', array('url', 'logo', 'Piwik\Plugins\MyPlugin\getLogoFromUrl'));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class MetadataCallbackAddMetadata extends BaseFilter
|
||||
{
|
||||
private $metadataToRead;
|
||||
private $functionToApply;
|
||||
private $metadataToAdd;
|
||||
private $applyToSummaryRow;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable that will eventually be filtered.
|
||||
* @param string|array $metadataToRead The metadata to read from each row and pass to the callback.
|
||||
* @param string $metadataToAdd The name of the metadata to add.
|
||||
* @param callable $functionToApply The callback to execute for each row. The result will be
|
||||
* added as metadata with the name `$metadataToAdd`.
|
||||
* @param bool $applyToSummaryRow True if the callback should be applied to the summary row, false
|
||||
* if otherwise.
|
||||
*/
|
||||
public function __construct($table, $metadataToRead, $metadataToAdd, $functionToApply,
|
||||
$applyToSummaryRow = true)
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->functionToApply = $functionToApply;
|
||||
|
||||
if (!is_array($metadataToRead)) {
|
||||
$metadataToRead = array($metadataToRead);
|
||||
}
|
||||
|
||||
$this->metadataToRead = $metadataToRead;
|
||||
$this->metadataToAdd = $metadataToAdd;
|
||||
$this->applyToSummaryRow = $applyToSummaryRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link MetadataCallbackAddMetadata}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
foreach ($table->getRows() as $key => $row) {
|
||||
if (!$this->applyToSummaryRow && $key == DataTable::ID_SUMMARY_ROW) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$params = array();
|
||||
foreach ($this->metadataToRead as $name) {
|
||||
$params[] = $row->getMetadata($name);
|
||||
}
|
||||
|
||||
$newValue = call_user_func_array($this->functionToApply, $params);
|
||||
if ($newValue !== false) {
|
||||
$row->addMetadata($this->metadataToAdd, $newValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
|
||||
/**
|
||||
* Execute a callback for each row of a {@link DataTable} passing certain column values and metadata
|
||||
* as metadata, and replaces row metadata with the callback result.
|
||||
*
|
||||
* **Basic usage example**
|
||||
*
|
||||
* $dataTable->filter('MetadataCallbackReplace', array('url', function ($url) {
|
||||
* return $url . '#index';
|
||||
* }));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class MetadataCallbackReplace extends ColumnCallbackReplace
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The DataTable that will eventually be filtered.
|
||||
* @param array|string $metadataToFilter The metadata whose values should be passed to the callback
|
||||
* and then replaced with the callback's result.
|
||||
* @param callable $functionToApply The function to execute. Must take the metadata value as a parameter
|
||||
* and return a value that will be used to replace the original.
|
||||
* @param array|null $functionParameters deprecated - use an [anonymous function](http://php.net/manual/en/functions.anonymous.php)
|
||||
* instead.
|
||||
* @param array $extraColumnParameters Extra column values that should be passed to the callback, but
|
||||
* shouldn't be replaced.
|
||||
*/
|
||||
public function __construct($table, $metadataToFilter, $functionToApply, $functionParameters = null,
|
||||
$extraColumnParameters = array())
|
||||
{
|
||||
parent::__construct($table, $metadataToFilter, $functionToApply, $functionParameters, $extraColumnParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Row $row
|
||||
* @param string $metadataToFilter
|
||||
* @param mixed $newValue
|
||||
*/
|
||||
protected function setElementToReplace($row, $metadataToFilter, $newValue)
|
||||
{
|
||||
$row->setMetadata($metadataToFilter, $newValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Row $row
|
||||
* @param string $metadataToFilter
|
||||
* @return array|bool|mixed
|
||||
*/
|
||||
protected function getElementToReplace($row, $metadataToFilter)
|
||||
{
|
||||
return $row->getMetadata($metadataToFilter);
|
||||
}
|
||||
}
|
||||
96
www/analytics/core/DataTable/Filter/Pattern.php
Normal file
96
www/analytics/core/DataTable/Filter/Pattern.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* Deletes every row for which a specific column does not match a supplied regex pattern.
|
||||
*
|
||||
* **Example**
|
||||
*
|
||||
* // filter out all rows whose labels doesn't start with piwik
|
||||
* $dataTable->filter('Pattern', array('label', '^piwik'));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class Pattern extends BaseFilter
|
||||
{
|
||||
private $columnToFilter;
|
||||
private $patternToSearch;
|
||||
private $patternToSearchQuoted;
|
||||
private $invertedMatch;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The table to eventually filter.
|
||||
* @param string $columnToFilter The column to match with the `$patternToSearch` pattern.
|
||||
* @param string $patternToSearch The regex pattern to use.
|
||||
* @param bool $invertedMatch Whether to invert the pattern or not. If true, will remove
|
||||
* rows if they match the pattern.
|
||||
*/
|
||||
public function __construct($table, $columnToFilter, $patternToSearch, $invertedMatch = false)
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->patternToSearch = $patternToSearch;
|
||||
$this->patternToSearchQuoted = self::getPatternQuoted($patternToSearch);
|
||||
$this->columnToFilter = $columnToFilter;
|
||||
$this->invertedMatch = $invertedMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to return the given pattern quoted
|
||||
*
|
||||
* @param string $pattern
|
||||
* @return string
|
||||
* @ignore
|
||||
*/
|
||||
static public function getPatternQuoted($pattern)
|
||||
{
|
||||
return '/' . str_replace('/', '\/', $pattern) . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs case insensitive match
|
||||
*
|
||||
* @param string $patternQuoted
|
||||
* @param string $string
|
||||
* @param bool $invertedMatch
|
||||
* @return int
|
||||
* @ignore
|
||||
*/
|
||||
static public function match($patternQuoted, $string, $invertedMatch = false)
|
||||
{
|
||||
return preg_match($patternQuoted . "i", $string) == 1 ^ $invertedMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Pattern}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
foreach ($table->getRows() as $key => $row) {
|
||||
//instead search must handle
|
||||
// - negative search with -piwik
|
||||
// - exact match with ""
|
||||
// see (?!pattern) A subexpression that performs a negative lookahead search, which matches the search string at any point where a string not matching pattern begins.
|
||||
$value = $row->getColumn($this->columnToFilter);
|
||||
if ($value === false) {
|
||||
$value = $row->getMetadata($this->columnToFilter);
|
||||
}
|
||||
if (!self::match($this->patternToSearchQuoted, $value, $this->invertedMatch)) {
|
||||
$table->deleteRow($key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
88
www/analytics/core/DataTable/Filter/PatternRecursive.php
Normal file
88
www/analytics/core/DataTable/Filter/PatternRecursive.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Exception;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Manager;
|
||||
|
||||
/**
|
||||
* Deletes rows that do not contain a column that matches a regex pattern and do not contain a
|
||||
* subtable that contains a column that matches a regex pattern.
|
||||
*
|
||||
* **Example**
|
||||
*
|
||||
* // only display index pageviews in Actions.getPageUrls
|
||||
* $dataTable->filter('PatternRecursive', array('label', 'index'));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class PatternRecursive extends BaseFilter
|
||||
{
|
||||
private $columnToFilter;
|
||||
private $patternToSearch;
|
||||
private $patternToSearchQuoted;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The table to eventually filter.
|
||||
* @param string $columnToFilter The column to match with the `$patternToSearch` pattern.
|
||||
* @param string $patternToSearch The regex pattern to use.
|
||||
*/
|
||||
public function __construct($table, $columnToFilter, $patternToSearch)
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->patternToSearch = $patternToSearch;
|
||||
$this->patternToSearchQuoted = Pattern::getPatternQuoted($patternToSearch);
|
||||
$this->patternToSearch = $patternToSearch; //preg_quote($patternToSearch);
|
||||
$this->columnToFilter = $columnToFilter;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link PatternRecursive}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
* @return int The number of deleted rows.
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
$rows = $table->getRows();
|
||||
|
||||
foreach ($rows as $key => $row) {
|
||||
// A row is deleted if
|
||||
// 1 - its label doesnt contain the pattern
|
||||
// AND 2 - the label is not found in the children
|
||||
$patternNotFoundInChildren = false;
|
||||
|
||||
try {
|
||||
$idSubTable = $row->getIdSubDataTable();
|
||||
$subTable = Manager::getInstance()->getTable($idSubTable);
|
||||
|
||||
// we delete the row if we couldn't find the pattern in any row in the
|
||||
// children hierarchy
|
||||
if ($this->filter($subTable) == 0) {
|
||||
$patternNotFoundInChildren = true;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// there is no subtable loaded for example
|
||||
$patternNotFoundInChildren = true;
|
||||
}
|
||||
|
||||
if ($patternNotFoundInChildren
|
||||
&& !Pattern::match($this->patternToSearchQuoted, $row->getColumn($this->columnToFilter), $invertedMatch = false)
|
||||
) {
|
||||
$table->deleteRow($key);
|
||||
}
|
||||
}
|
||||
|
||||
return $table->getRowsCount();
|
||||
}
|
||||
}
|
||||
59
www/analytics/core/DataTable/Filter/RangeCheck.php
Normal file
59
www/analytics/core/DataTable/Filter/RangeCheck.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* Check range
|
||||
*
|
||||
*/
|
||||
class RangeCheck extends BaseFilter
|
||||
{
|
||||
static public $minimumValue = 0.00;
|
||||
static public $maximumValue = 100.0;
|
||||
|
||||
/**
|
||||
* @param DataTable $table
|
||||
* @param string $columnToFilter name of the column to filter
|
||||
* @param float $minimumValue minimum value for range
|
||||
* @param float $maximumValue maximum value for range
|
||||
*/
|
||||
public function __construct($table, $columnToFilter, $minimumValue = 0.00, $maximumValue = 100.0)
|
||||
{
|
||||
parent::__construct($table);
|
||||
|
||||
$this->columnToFilter = $columnToFilter;
|
||||
|
||||
if ($minimumValue < $maximumValue) {
|
||||
self::$minimumValue = $minimumValue;
|
||||
self::$maximumValue = $maximumValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the filter an adjusts all columns to fit the defined range
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
foreach ($table->getRows() as $row) {
|
||||
$value = $row->getColumn($this->columnToFilter);
|
||||
if ($value !== false) {
|
||||
if ($value < self::$minimumValue) {
|
||||
$row->setColumn($this->columnToFilter, self::$minimumValue);
|
||||
} elseif ($value > self::$maximumValue) {
|
||||
$row->setColumn($this->columnToFilter, self::$maximumValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
171
www/analytics/core/DataTable/Filter/ReplaceColumnNames.php
Normal file
171
www/analytics/core/DataTable/Filter/ReplaceColumnNames.php
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
use Piwik\DataTable\Simple;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Metrics;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\Tracker\GoalManager;
|
||||
|
||||
/**
|
||||
* Replaces column names in each row of a table using an array that maps old column
|
||||
* names new ones.
|
||||
*
|
||||
* If no mapping is provided, this column will use one that maps index metric names
|
||||
* (which are integers) with their string column names. In the database, reports are
|
||||
* stored with integer metric names because it results in blobs that take up less space.
|
||||
* When loading the reports, the column names must be replaced, which is handled by this
|
||||
* class. (See {@link Piwik\Metrics} for more information about integer metric names.)
|
||||
*
|
||||
* **Basic example**
|
||||
*
|
||||
* // filter use in a plugin's API method
|
||||
* public function getMyReport($idSite, $period, $date, $segment = false, $expanded = false)
|
||||
* {
|
||||
* $dataTable = Archive::getDataTableFromArchive('MyPlugin_MyReport', $idSite, $period, $date, $segment, $expanded);
|
||||
* $dataTable->queueFilter('ReplaceColumnNames');
|
||||
* return $dataTable;
|
||||
* }
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class ReplaceColumnNames extends BaseFilter
|
||||
{
|
||||
protected $mappingToApply;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The table that will be eventually filtered.
|
||||
* @param array|null $mappingToApply The name mapping to apply. Must map old column names
|
||||
* with new ones, eg,
|
||||
*
|
||||
* array('OLD_COLUMN_NAME' => 'NEW_COLUMN NAME',
|
||||
* 'OLD_COLUMN_NAME2' => 'NEW_COLUMN NAME2')
|
||||
*
|
||||
* If null, {@link Piwik\Metrics::$mappingFromIdToName} is used.
|
||||
*/
|
||||
public function __construct($table, $mappingToApply = null)
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->mappingToApply = Metrics::$mappingFromIdToName;
|
||||
if (!is_null($mappingToApply)) {
|
||||
$this->mappingToApply = $mappingToApply;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link ReplaceColumnNames}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
if ($table instanceof Simple) {
|
||||
$this->filterSimple($table);
|
||||
} else {
|
||||
$this->filterTable($table);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTable $table
|
||||
*/
|
||||
protected function filterTable($table)
|
||||
{
|
||||
foreach ($table->getRows() as $key => $row) {
|
||||
$oldColumns = $row->getColumns();
|
||||
$newColumns = $this->getRenamedColumns($oldColumns);
|
||||
$row->setColumns($newColumns);
|
||||
$this->filterSubTable($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Simple $table
|
||||
*/
|
||||
protected function filterSimple(Simple $table)
|
||||
{
|
||||
foreach ($table->getRows() as $row) {
|
||||
$columns = array_keys($row->getColumns());
|
||||
foreach ($columns as $column) {
|
||||
$newName = $this->getRenamedColumn($column);
|
||||
if ($newName) {
|
||||
$row->renameColumn($column, $newName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function getRenamedColumn($column)
|
||||
{
|
||||
$newName = false;
|
||||
if (isset($this->mappingToApply[$column])
|
||||
&& $this->mappingToApply[$column] != $column
|
||||
) {
|
||||
$newName = $this->mappingToApply[$column];
|
||||
}
|
||||
return $newName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given columns and renames them if required
|
||||
*
|
||||
* @param array $columns
|
||||
* @return array
|
||||
*/
|
||||
protected function getRenamedColumns($columns)
|
||||
{
|
||||
$newColumns = array();
|
||||
foreach ($columns as $columnName => $columnValue) {
|
||||
$renamedColumn = $this->getRenamedColumn($columnName);
|
||||
if ($renamedColumn) {
|
||||
if ($renamedColumn == 'goals') {
|
||||
$columnValue = $this->flattenGoalColumns($columnValue);
|
||||
}
|
||||
// If we happen to rename a column to a name that already exists,
|
||||
// sum both values in the column. This should really not happen, but
|
||||
// we introduced in 1.1 a new dataTable indexing scheme for Actions table, and
|
||||
// could end up with both strings and their int indexes counterpart in a monthly/yearly dataTable
|
||||
// built from DataTable with both formats
|
||||
if (isset($newColumns[$renamedColumn])) {
|
||||
$columnValue += $newColumns[$renamedColumn];
|
||||
}
|
||||
|
||||
$columnName = $renamedColumn;
|
||||
}
|
||||
$newColumns[$columnName] = $columnValue;
|
||||
}
|
||||
return $newColumns;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $columnValue
|
||||
* @return array
|
||||
*/
|
||||
protected function flattenGoalColumns($columnValue)
|
||||
{
|
||||
$newSubColumns = array();
|
||||
foreach ($columnValue as $idGoal => $goalValues) {
|
||||
$mapping = Metrics::$mappingFromIdToNameGoal;
|
||||
if ($idGoal == GoalManager::IDGOAL_CART) {
|
||||
$idGoal = Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_CART;
|
||||
} elseif ($idGoal == GoalManager::IDGOAL_ORDER) {
|
||||
$idGoal = Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER;
|
||||
}
|
||||
foreach ($goalValues as $id => $goalValue) {
|
||||
$subColumnName = $mapping[$id];
|
||||
$newSubColumns['idgoal=' . $idGoal][$subColumnName] = $goalValue;
|
||||
}
|
||||
}
|
||||
return $newSubColumns;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Manager;
|
||||
use Piwik\Piwik;
|
||||
|
||||
/**
|
||||
* Replaces the label of the summary row with a supplied label.
|
||||
*
|
||||
* This filter is only used to prettify the summary row label and so it should
|
||||
* always be queued on a {@link DataTable}.
|
||||
*
|
||||
* This filter always recurses. In other words, this filter will always apply itself to
|
||||
* all subtables in the given {@link DataTable}'s table hierarchy.
|
||||
*
|
||||
* **Basic example**
|
||||
*
|
||||
* $dataTable->queueFilter('ReplaceSummaryRowLabel', array(Piwik::translate('General_Others')));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class ReplaceSummaryRowLabel extends BaseFilter
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The table that will eventually be filtered.
|
||||
* @param string|null $newLabel The new label for summary row. If null, defaults to
|
||||
* `Piwik::translate('General_Others')`.
|
||||
*/
|
||||
public function __construct($table, $newLabel = null)
|
||||
{
|
||||
parent::__construct($table);
|
||||
if (is_null($newLabel)) {
|
||||
$newLabel = Piwik::translate('General_Others');
|
||||
}
|
||||
$this->newLabel = $newLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link ReplaceSummaryRowLabel}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
$rows = $table->getRows();
|
||||
foreach ($rows as $id => $row) {
|
||||
if ($row->getColumn('label') == DataTable::LABEL_SUMMARY_ROW
|
||||
|| $id == DataTable::ID_SUMMARY_ROW
|
||||
) {
|
||||
$row->setColumn('label', $this->newLabel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// recurse
|
||||
foreach ($rows as $row) {
|
||||
if ($row->isSubtableLoaded()) {
|
||||
$subTable = Manager::getInstance()->getTable($row->getIdSubDataTable());
|
||||
$this->filter($subTable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
www/analytics/core/DataTable/Filter/SafeDecodeLabel.php
Normal file
72
www/analytics/core/DataTable/Filter/SafeDecodeLabel.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
|
||||
/**
|
||||
* Sanitizes DataTable labels as an extra precaution. Called internally by Piwik.
|
||||
*
|
||||
*/
|
||||
class SafeDecodeLabel extends BaseFilter
|
||||
{
|
||||
private $columnToDecode;
|
||||
|
||||
/**
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function __construct($table)
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->columnToDecode = 'label';
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes the given value
|
||||
*
|
||||
* @param string $value
|
||||
* @return mixed|string
|
||||
*/
|
||||
public static function decodeLabelSafe($value)
|
||||
{
|
||||
if (empty($value)) {
|
||||
return $value;
|
||||
}
|
||||
$raw = urldecode($value);
|
||||
$value = htmlspecialchars_decode($raw, ENT_QUOTES);
|
||||
|
||||
// ENT_IGNORE so that if utf8 string has some errors, we simply discard invalid code unit sequences
|
||||
$style = ENT_QUOTES | ENT_IGNORE;
|
||||
|
||||
// See changes in 5.4: http://nikic.github.com/2012/01/28/htmlspecialchars-improvements-in-PHP-5-4.html
|
||||
// Note: at some point we should change ENT_IGNORE to ENT_SUBSTITUTE
|
||||
$value = htmlspecialchars($value, $style, 'UTF-8');
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes all columns of the given data table
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
foreach ($table->getRows() as $row) {
|
||||
$value = $row->getColumn($this->columnToDecode);
|
||||
if ($value !== false) {
|
||||
$value = self::decodeLabelSafe($value);
|
||||
$row->setColumn($this->columnToDecode, $value);
|
||||
|
||||
$this->filterSubTable($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
221
www/analytics/core/DataTable/Filter/Sort.php
Normal file
221
www/analytics/core/DataTable/Filter/Sort.php
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
use Piwik\DataTable\Row;
|
||||
use Piwik\DataTable\Simple;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Metrics;
|
||||
|
||||
/**
|
||||
* Sorts a {@link DataTable} based on the value of a specific column.
|
||||
*
|
||||
* It is possible to specify a natural sorting (see [php.net/natsort](http://php.net/natsort) for details).
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class Sort extends BaseFilter
|
||||
{
|
||||
protected $columnToSort;
|
||||
protected $order;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The table to eventually filter.
|
||||
* @param string $columnToSort The name of the column to sort by.
|
||||
* @param string $order order `'asc'` or `'desc'`.
|
||||
* @param bool $naturalSort Whether to use a natural sort or not (see {@link http://php.net/natsort}).
|
||||
* @param bool $recursiveSort Whether to sort all subtables or not.
|
||||
*/
|
||||
public function __construct($table, $columnToSort, $order = 'desc', $naturalSort = true, $recursiveSort = false)
|
||||
{
|
||||
parent::__construct($table);
|
||||
if ($recursiveSort) {
|
||||
$table->enableRecursiveSort();
|
||||
}
|
||||
$this->columnToSort = $columnToSort;
|
||||
$this->naturalSort = $naturalSort;
|
||||
$this->setOrder($order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the order
|
||||
*
|
||||
* @param string $order asc|desc
|
||||
*/
|
||||
public function setOrder($order)
|
||||
{
|
||||
if ($order == 'asc') {
|
||||
$this->order = 'asc';
|
||||
$this->sign = 1;
|
||||
} else {
|
||||
$this->order = 'desc';
|
||||
$this->sign = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting method used for sorting numbers
|
||||
*
|
||||
* @param number $a
|
||||
* @param number $b
|
||||
* @return int
|
||||
*/
|
||||
public function numberSort($a, $b)
|
||||
{
|
||||
return !isset($a->c[Row::COLUMNS][$this->columnToSort])
|
||||
&& !isset($b->c[Row::COLUMNS][$this->columnToSort])
|
||||
|
||||
? 0
|
||||
: (
|
||||
!isset($a->c[Row::COLUMNS][$this->columnToSort])
|
||||
? 1
|
||||
: (
|
||||
!isset($b->c[Row::COLUMNS][$this->columnToSort])
|
||||
? -1
|
||||
: (($a->c[Row::COLUMNS][$this->columnToSort] != $b->c[Row::COLUMNS][$this->columnToSort]
|
||||
|| !isset($a->c[Row::COLUMNS]['label']))
|
||||
? ($this->sign * (
|
||||
$a->c[Row::COLUMNS][$this->columnToSort]
|
||||
< $b->c[Row::COLUMNS][$this->columnToSort]
|
||||
? -1
|
||||
: 1)
|
||||
)
|
||||
: -1 * $this->sign * strnatcasecmp(
|
||||
$a->c[Row::COLUMNS]['label'],
|
||||
$b->c[Row::COLUMNS]['label'])
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting method used for sorting values natural
|
||||
*
|
||||
* @param mixed $a
|
||||
* @param mixed $b
|
||||
* @return int
|
||||
*/
|
||||
function naturalSort($a, $b)
|
||||
{
|
||||
return !isset($a->c[Row::COLUMNS][$this->columnToSort])
|
||||
&& !isset($b->c[Row::COLUMNS][$this->columnToSort])
|
||||
? 0
|
||||
: (!isset($a->c[Row::COLUMNS][$this->columnToSort])
|
||||
? 1
|
||||
: (!isset($b->c[Row::COLUMNS][$this->columnToSort])
|
||||
? -1
|
||||
: $this->sign * strnatcasecmp(
|
||||
$a->c[Row::COLUMNS][$this->columnToSort],
|
||||
$b->c[Row::COLUMNS][$this->columnToSort]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting method used for sorting values
|
||||
*
|
||||
* @param mixed $a
|
||||
* @param mixed $b
|
||||
* @return int
|
||||
*/
|
||||
function sortString($a, $b)
|
||||
{
|
||||
return !isset($a->c[Row::COLUMNS][$this->columnToSort])
|
||||
&& !isset($b->c[Row::COLUMNS][$this->columnToSort])
|
||||
? 0
|
||||
: (!isset($a->c[Row::COLUMNS][$this->columnToSort])
|
||||
? 1
|
||||
: (!isset($b->c[Row::COLUMNS][$this->columnToSort])
|
||||
? -1
|
||||
: $this->sign *
|
||||
strcasecmp($a->c[Row::COLUMNS][$this->columnToSort],
|
||||
$b->c[Row::COLUMNS][$this->columnToSort]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column to be used for sorting
|
||||
*
|
||||
* @param Row $row
|
||||
* @return int
|
||||
*/
|
||||
protected function selectColumnToSort($row)
|
||||
{
|
||||
$value = $row->getColumn($this->columnToSort);
|
||||
if ($value !== false) {
|
||||
return $this->columnToSort;
|
||||
}
|
||||
|
||||
$columnIdToName = Metrics::getMappingFromIdToName();
|
||||
// sorting by "nb_visits" but the index is Metrics::INDEX_NB_VISITS in the table
|
||||
if (isset($columnIdToName[$this->columnToSort])) {
|
||||
$column = $columnIdToName[$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 = Metrics::INDEX_NB_VISITS;
|
||||
$value = $row->getColumn($column);
|
||||
if ($value !== false) {
|
||||
return $column;
|
||||
}
|
||||
|
||||
// even though this column is not set properly in the table,
|
||||
// we select it for the sort, so that the table's internal state is set properly
|
||||
return $this->columnToSort;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link Sort}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
* @return mixed
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
if ($table instanceof Simple) {
|
||||
return;
|
||||
}
|
||||
if (empty($this->columnToSort)) {
|
||||
return;
|
||||
}
|
||||
$rows = $table->getRows();
|
||||
if (count($rows) == 0) {
|
||||
return;
|
||||
}
|
||||
$row = current($rows);
|
||||
if ($row === false) {
|
||||
return;
|
||||
}
|
||||
$this->columnToSort = $this->selectColumnToSort($row);
|
||||
|
||||
$value = $row->getColumn($this->columnToSort);
|
||||
if (is_numeric($value)) {
|
||||
$methodToUse = "numberSort";
|
||||
} else {
|
||||
if ($this->naturalSort) {
|
||||
$methodToUse = "naturalSort";
|
||||
} else {
|
||||
$methodToUse = "sortString";
|
||||
}
|
||||
}
|
||||
$table->sort(array($this, $methodToUse), $this->columnToSort);
|
||||
}
|
||||
}
|
||||
113
www/analytics/core/DataTable/Filter/Truncate.php
Normal file
113
www/analytics/core/DataTable/Filter/Truncate.php
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Filter;
|
||||
|
||||
use Piwik\DataTable\BaseFilter;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
use Piwik\Piwik;
|
||||
|
||||
/**
|
||||
* Truncates a {@link DataTable} by merging all rows after a certain index into a new summary
|
||||
* row. If the count of rows is less than the index, nothing happens.
|
||||
*
|
||||
* The {@link ReplaceSummaryRowLabel} filter will be queued after the table is truncated.
|
||||
*
|
||||
* ### Examples
|
||||
*
|
||||
* **Basic usage**
|
||||
*
|
||||
* $dataTable->filter('Truncate', array($truncateAfter = 500));
|
||||
*
|
||||
* **Using a custom summary row label**
|
||||
*
|
||||
* $dataTable->filter('Truncate', array($truncateAfter = 500, $summaryRowLabel = Piwik::translate('General_Total')));
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class Truncate extends BaseFilter
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable $table The table that will be filtered eventually.
|
||||
* @param int $truncateAfter The row index to truncate at. All rows passed this index will
|
||||
* be removed.
|
||||
* @param string $labelSummaryRow The label to use for the summary row. Defaults to
|
||||
* `Piwik::translate('General_Others')`.
|
||||
* @param string $columnToSortByBeforeTruncating The column to sort by before truncation, eg,
|
||||
* `'nb_visits'`.
|
||||
* @param bool $filterRecursive If true executes this filter on all subtables descending from
|
||||
* `$table`.
|
||||
*/
|
||||
public function __construct($table,
|
||||
$truncateAfter,
|
||||
$labelSummaryRow = null,
|
||||
$columnToSortByBeforeTruncating = null,
|
||||
$filterRecursive = true)
|
||||
{
|
||||
parent::__construct($table);
|
||||
$this->truncateAfter = $truncateAfter;
|
||||
if ($labelSummaryRow === null) {
|
||||
$labelSummaryRow = Piwik::translate('General_Others');
|
||||
}
|
||||
$this->labelSummaryRow = $labelSummaryRow;
|
||||
$this->columnToSortByBeforeTruncating = $columnToSortByBeforeTruncating;
|
||||
$this->filterRecursive = $filterRecursive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the filter, see {@link Truncate}.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
public function filter($table)
|
||||
{
|
||||
$this->addSummaryRow($table);
|
||||
$table->queueFilter('ReplaceSummaryRowLabel', array($this->labelSummaryRow));
|
||||
|
||||
if ($this->filterRecursive) {
|
||||
foreach ($table->getRows() as $row) {
|
||||
if ($row->isSubtableLoaded()) {
|
||||
$this->filter($row->getSubtable());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function addSummaryRow($table)
|
||||
{
|
||||
$table->filter('Sort', array($this->columnToSortByBeforeTruncating, 'desc'));
|
||||
|
||||
if ($table->getRowsCount() <= $this->truncateAfter + 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$rows = $table->getRows();
|
||||
$count = $table->getRowsCount();
|
||||
$newRow = new Row(array(Row::COLUMNS => array('label' => DataTable::LABEL_SUMMARY_ROW)));
|
||||
for ($i = $this->truncateAfter; $i < $count; $i++) {
|
||||
if (!isset($rows[$i])) {
|
||||
// case when the last row is a summary row, it is not indexed by $cout but by DataTable::ID_SUMMARY_ROW
|
||||
$summaryRow = $table->getRowFromId(DataTable::ID_SUMMARY_ROW);
|
||||
|
||||
//FIXME: I'm not sure why it could return false, but it was reported in: http://forum.piwik.org/read.php?2,89324,page=1#msg-89442
|
||||
if ($summaryRow) {
|
||||
$newRow->sumRow($summaryRow, $enableCopyMetadata = false, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME));
|
||||
}
|
||||
} else {
|
||||
$newRow->sumRow($rows[$i], $enableCopyMetadata = false, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME));
|
||||
}
|
||||
}
|
||||
|
||||
$table->filter('Limit', array(0, $this->truncateAfter));
|
||||
$table->addSummaryRow($newRow);
|
||||
unset($rows);
|
||||
}
|
||||
}
|
||||
156
www/analytics/core/DataTable/Manager.php
Normal file
156
www/analytics/core/DataTable/Manager.php
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Piwik\DataTable;
|
||||
|
||||
use Exception;
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Singleton;
|
||||
|
||||
/**
|
||||
* The DataTable_Manager registers all the instanciated DataTable and provides an
|
||||
* easy way to access them. This is used to store all the DataTable during the archiving process.
|
||||
* At the end of archiving, the ArchiveProcessor will read the stored datatable and record them in the DB.
|
||||
*
|
||||
* @method static \Piwik\DataTable\Manager getInstance()
|
||||
*/
|
||||
class Manager extends Singleton
|
||||
{
|
||||
/**
|
||||
* Array used to store the DataTable
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $tables = array();
|
||||
|
||||
/**
|
||||
* Id of the next inserted table id in the Manager
|
||||
* @var int
|
||||
*/
|
||||
protected $nextTableId = 1;
|
||||
|
||||
/**
|
||||
* Add a DataTable to the registry
|
||||
*
|
||||
* @param DataTable $table
|
||||
* @return int Index of the table in the manager array
|
||||
*/
|
||||
public function addTable($table)
|
||||
{
|
||||
$this->tables[$this->nextTableId] = $table;
|
||||
$this->nextTableId++;
|
||||
return $this->nextTableId - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the DataTable associated to the ID $idTable.
|
||||
* NB: The datatable has to have been instanciated before!
|
||||
* This method will not fetch the DataTable from the DB.
|
||||
*
|
||||
* @param int $idTable
|
||||
* @throws Exception If the table can't be found
|
||||
* @return DataTable The table
|
||||
*/
|
||||
public function getTable($idTable)
|
||||
{
|
||||
if (!isset($this->tables[$idTable])) {
|
||||
throw new TableNotFoundException(sprintf("This report has been reprocessed since your last click. To see this error less often, please increase the timeout value in seconds in Settings > General Settings. (error: id %s not found).", $idTable));
|
||||
}
|
||||
return $this->tables[$idTable];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the latest used table ID
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getMostRecentTableId()
|
||||
{
|
||||
return $this->nextTableId - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all the registered DataTables from the manager
|
||||
*/
|
||||
public function deleteAll($deleteWhenIdTableGreaterThan = 0)
|
||||
{
|
||||
foreach ($this->tables as $id => $table) {
|
||||
if ($id > $deleteWhenIdTableGreaterThan) {
|
||||
$this->deleteTable($id);
|
||||
}
|
||||
}
|
||||
if ($deleteWhenIdTableGreaterThan == 0) {
|
||||
$this->tables = array();
|
||||
$this->nextTableId = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes (unsets) the datatable given its id and removes it from the manager
|
||||
* Subsequent get for this table will fail
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function deleteTable($id)
|
||||
{
|
||||
if (isset($this->tables[$id])) {
|
||||
Common::destroy($this->tables[$id]);
|
||||
$this->setTableDeleted($id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all tables starting from the $firstTableId to the most recent table id except the ones that are
|
||||
* supposed to be ignored.
|
||||
*
|
||||
* @param int[] $idsToBeIgnored
|
||||
* @param int $firstTableId
|
||||
*/
|
||||
public function deleteTablesExceptIgnored($idsToBeIgnored, $firstTableId = 0)
|
||||
{
|
||||
$lastTableId = $this->getMostRecentTableId();
|
||||
|
||||
for ($index = $firstTableId; $index <= $lastTableId; $index++) {
|
||||
if (!in_array($index, $idsToBeIgnored)) {
|
||||
$this->deleteTable($index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the table from the manager (table has already been unset)
|
||||
*
|
||||
* @param int $id
|
||||
*/
|
||||
public function setTableDeleted($id)
|
||||
{
|
||||
$this->tables[$id] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug only. Dumps all tables currently registered in the Manager
|
||||
*/
|
||||
public function dumpAllTables()
|
||||
{
|
||||
echo "<hr />Manager->dumpAllTables()<br />";
|
||||
foreach ($this->tables as $id => $table) {
|
||||
if (!($table instanceof DataTable)) {
|
||||
echo "Error table $id is not instance of datatable<br />";
|
||||
var_export($table);
|
||||
} else {
|
||||
echo "<hr />";
|
||||
echo "Table (index=$id) TableId = " . $table->getId() . "<br />";
|
||||
echo $table;
|
||||
echo "<br />";
|
||||
}
|
||||
}
|
||||
echo "<br />-- End Manager->dumpAllTables()<hr />";
|
||||
}
|
||||
}
|
||||
445
www/analytics/core/DataTable/Map.php
Normal file
445
www/analytics/core/DataTable/Map.php
Normal file
|
|
@ -0,0 +1,445 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable;
|
||||
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Renderer\Console;
|
||||
|
||||
/**
|
||||
* Stores an array of {@link DataTable}s indexed by one type of {@link DataTable} metadata (such as site ID
|
||||
* or period).
|
||||
*
|
||||
* DataTable Maps are returned on all queries that involve multiple sites and/or multiple
|
||||
* periods. The Maps will contain a {@link DataTable} for each site and period combination.
|
||||
*
|
||||
* The Map implements some {@link DataTable} such as {@link queueFilter()} and {@link getRowsCount}.
|
||||
*
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class Map implements DataTableInterface
|
||||
{
|
||||
/**
|
||||
* Array containing the DataTable withing this Set
|
||||
*
|
||||
* @var DataTable[]
|
||||
*/
|
||||
protected $array = array();
|
||||
|
||||
/**
|
||||
* @see self::getKeyName()
|
||||
* @var string
|
||||
*/
|
||||
protected $keyName = 'defaultKeyName';
|
||||
|
||||
/**
|
||||
* Returns a string description of the data used to index the DataTables.
|
||||
*
|
||||
* This label is used by DataTable Renderers (it becomes a column name or the XML description tag).
|
||||
*
|
||||
* @return string eg, `'idSite'`, `'period'`
|
||||
*/
|
||||
public function getKeyName()
|
||||
{
|
||||
return $this->keyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the name of they metadata used to index {@link DataTable}s. See {@link getKeyName()}.
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function setKeyName($name)
|
||||
{
|
||||
$this->keyName = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of {@link DataTable}s in this DataTable\Map.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getRowsCount()
|
||||
{
|
||||
return count($this->getDataTables());
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a filter to {@link DataTable} child of contained by this instance.
|
||||
*
|
||||
* See {@link Piwik\DataTable::queueFilter()} for more information..
|
||||
*
|
||||
* @param string|Closure $className Filter name, eg. `'Limit'` or a Closure.
|
||||
* @param array $parameters Filter parameters, eg. `array(50, 10)`.
|
||||
*/
|
||||
public function queueFilter($className, $parameters = array())
|
||||
{
|
||||
foreach ($this->getDataTables() as $table) {
|
||||
$table->queueFilter($className, $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the filters previously queued to each DataTable contained by this DataTable\Map.
|
||||
*/
|
||||
public function applyQueuedFilters()
|
||||
{
|
||||
foreach ($this->getDataTables() as $table) {
|
||||
$table->applyQueuedFilters();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a filter to all tables contained by this instance.
|
||||
*
|
||||
* @param string|Closure $className Name of filter class or a Closure.
|
||||
* @param array $parameters Parameters to pass to the filter.
|
||||
*/
|
||||
public function filter($className, $parameters = array())
|
||||
{
|
||||
foreach ($this->getDataTables() as $id => $table) {
|
||||
$table->filter($className, $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of DataTables contained by this class.
|
||||
*
|
||||
* @return DataTable[]|Map[]
|
||||
*/
|
||||
public function getDataTables()
|
||||
{
|
||||
return $this->array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table with the specific label.
|
||||
*
|
||||
* @param string $label
|
||||
* @return DataTable|Map
|
||||
*/
|
||||
public function getTable($label)
|
||||
{
|
||||
return $this->array[$label];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first element in the Map's array.
|
||||
*
|
||||
* @return DataTable|Map|false
|
||||
*/
|
||||
public function getFirstRow()
|
||||
{
|
||||
return reset($this->array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last element in the Map's array.
|
||||
*
|
||||
* @return DataTable|Map|false
|
||||
*/
|
||||
public function getLastRow()
|
||||
{
|
||||
return end($this->array);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new {@link DataTable} or Map instance to this DataTable\Map.
|
||||
*
|
||||
* @param DataTable|Map $table
|
||||
* @param string $label Label used to index this table in the array.
|
||||
*/
|
||||
public function addTable($table, $label)
|
||||
{
|
||||
$this->array[$label] = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string output of this DataTable\Map (applying the default renderer to every {@link DataTable}
|
||||
* of this DataTable\Map).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$renderer = new Console();
|
||||
$renderer->setTable($this);
|
||||
return (string)$renderer;
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link DataTable::enableRecursiveSort()}.
|
||||
*/
|
||||
public function enableRecursiveSort()
|
||||
{
|
||||
foreach ($this->getDataTables() as $table) {
|
||||
$table->enableRecursiveSort();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the given column in each contained {@link DataTable}.
|
||||
*
|
||||
* See {@link DataTable::renameColumn()}.
|
||||
*
|
||||
* @param string $oldName
|
||||
* @param string $newName
|
||||
*/
|
||||
public function renameColumn($oldName, $newName)
|
||||
{
|
||||
foreach ($this->getDataTables() as $table) {
|
||||
$table->renameColumn($oldName, $newName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the specified columns in each contained {@link DataTable}.
|
||||
*
|
||||
* See {@link DataTable::deleteColumns()}.
|
||||
*
|
||||
* @param array $columns The columns to delete.
|
||||
* @param bool $deleteRecursiveInSubtables This param is currently not used.
|
||||
*/
|
||||
public function deleteColumns($columns, $deleteRecursiveInSubtables = false)
|
||||
{
|
||||
foreach ($this->getDataTables() as $table) {
|
||||
$table->deleteColumns($columns);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a table from the array of DataTables.
|
||||
*
|
||||
* @param string $id The label associated with {@link DataTable}.
|
||||
*/
|
||||
public function deleteRow($id)
|
||||
{
|
||||
unset($this->array[$id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given column in every contained {@link DataTable}.
|
||||
*
|
||||
* @see DataTable::deleteColumn
|
||||
* @param string $name
|
||||
*/
|
||||
public function deleteColumn($name)
|
||||
{
|
||||
foreach ($this->getDataTables() as $table) {
|
||||
$table->deleteColumn($name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array containing all column values in all contained {@link DataTable}s for the requested column.
|
||||
*
|
||||
* @param string $name The column name.
|
||||
* @return array
|
||||
*/
|
||||
public function getColumn($name)
|
||||
{
|
||||
$values = array();
|
||||
foreach ($this->getDataTables() as $table) {
|
||||
$moreValues = $table->getColumn($name);
|
||||
foreach ($moreValues as &$value) {
|
||||
$values[] = $value;
|
||||
}
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the rows of every child {@link DataTable} into a new one and
|
||||
* returns it. This function will also set the label of the merged rows
|
||||
* to the label of the {@link DataTable} they were originally from.
|
||||
*
|
||||
* The result of this function is determined by the type of DataTable
|
||||
* this instance holds. If this DataTable\Map instance holds an array
|
||||
* of DataTables, this function will transform it from:
|
||||
*
|
||||
* Label 0:
|
||||
* DataTable(row1)
|
||||
* Label 1:
|
||||
* DataTable(row2)
|
||||
*
|
||||
* to:
|
||||
*
|
||||
* DataTable(row1[label = 'Label 0'], row2[label = 'Label 1'])
|
||||
*
|
||||
* If this instance holds an array of DataTable\Maps, this function will
|
||||
* transform it from:
|
||||
*
|
||||
* Outer Label 0: // the outer DataTable\Map
|
||||
* Inner Label 0: // one of the inner DataTable\Maps
|
||||
* DataTable(row1)
|
||||
* Inner Label 1:
|
||||
* DataTable(row2)
|
||||
* Outer Label 1:
|
||||
* Inner Label 0:
|
||||
* DataTable(row3)
|
||||
* Inner Label 1:
|
||||
* DataTable(row4)
|
||||
*
|
||||
* to:
|
||||
*
|
||||
* Inner Label 0:
|
||||
* DataTable(row1[label = 'Outer Label 0'], row3[label = 'Outer Label 1'])
|
||||
* Inner Label 1:
|
||||
* DataTable(row2[label = 'Outer Label 0'], row4[label = 'Outer Label 1'])
|
||||
*
|
||||
* If this instance holds an array of DataTable\Maps, the
|
||||
* metadata of the first child is used as the metadata of the result.
|
||||
*
|
||||
* This function can be used, for example, to smoosh IndexedBySite archive
|
||||
* query results into one DataTable w/ different rows differentiated by site ID.
|
||||
*
|
||||
* Note: This DataTable/Map will be destroyed and will be no longer usable after the tables have been merged into
|
||||
* the new dataTable to reduce memory usage. Destroying all DataTables witihn the Map also seems to fix a
|
||||
* Segmentation Fault that occurred in the AllWebsitesDashboard when having > 16k sites.
|
||||
*
|
||||
* @return DataTable|Map
|
||||
*/
|
||||
public function mergeChildren()
|
||||
{
|
||||
$firstChild = reset($this->array);
|
||||
|
||||
if ($firstChild instanceof Map) {
|
||||
$result = $firstChild->getEmptyClone();
|
||||
|
||||
/** @var $subDataTableMap Map */
|
||||
foreach ($this->getDataTables() as $label => $subDataTableMap) {
|
||||
foreach ($subDataTableMap->getDataTables() as $innerLabel => $subTable) {
|
||||
if (!isset($result->array[$innerLabel])) {
|
||||
$dataTable = new DataTable();
|
||||
$dataTable->setMetadataValues($subTable->getAllTableMetadata());
|
||||
|
||||
$result->addTable($dataTable, $innerLabel);
|
||||
}
|
||||
|
||||
$this->copyRowsAndSetLabel($result->array[$innerLabel], $subTable, $label);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$result = new DataTable();
|
||||
|
||||
foreach ($this->getDataTables() as $label => $subTable) {
|
||||
$this->copyRowsAndSetLabel($result, $subTable, $label);
|
||||
Common::destroy($subTable);
|
||||
}
|
||||
|
||||
$this->array = array();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function used by mergeChildren. Copies the rows from one table,
|
||||
* sets their 'label' columns to a value and adds them to another table.
|
||||
*
|
||||
* @param DataTable $toTable The table to copy rows to.
|
||||
* @param DataTable $fromTable The table to copy rows from.
|
||||
* @param string $label The value to set the 'label' column of every copied row.
|
||||
*/
|
||||
private function copyRowsAndSetLabel($toTable, $fromTable, $label)
|
||||
{
|
||||
foreach ($fromTable->getRows() as $fromRow) {
|
||||
$oldColumns = $fromRow->getColumns();
|
||||
unset($oldColumns['label']);
|
||||
|
||||
$columns = array_merge(array('label' => $label), $oldColumns);
|
||||
$row = new Row(array(
|
||||
Row::COLUMNS => $columns,
|
||||
Row::METADATA => $fromRow->getMetadata(),
|
||||
Row::DATATABLE_ASSOCIATED => $fromRow->getIdSubDataTable()
|
||||
));
|
||||
$toTable->addRow($row);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums a DataTable to all the tables in this array.
|
||||
*
|
||||
* _Note: Will only add `$tableToSum` if the childTable has some rows._
|
||||
*
|
||||
* See {@link Piwik\DataTable::addDataTable()}.
|
||||
*
|
||||
* @param DataTable $tableToSum
|
||||
*/
|
||||
public function addDataTable(DataTable $tableToSum)
|
||||
{
|
||||
foreach ($this->getDataTables() as $childTable) {
|
||||
$childTable->addDataTable($tableToSum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new DataTable\Map w/ child tables that have had their
|
||||
* subtables merged.
|
||||
*
|
||||
* See {@link DataTable::mergeSubtables()}.
|
||||
*
|
||||
* @return Map
|
||||
*/
|
||||
public function mergeSubtables()
|
||||
{
|
||||
$result = $this->getEmptyClone();
|
||||
foreach ($this->getDataTables() as $label => $childTable) {
|
||||
$result->addTable($childTable->mergeSubtables(), $label);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new DataTable\Map w/o any child DataTables, but with
|
||||
* the same key name as this instance.
|
||||
*
|
||||
* @return Map
|
||||
*/
|
||||
public function getEmptyClone()
|
||||
{
|
||||
$dataTableMap = new Map;
|
||||
$dataTableMap->setKeyName($this->getKeyName());
|
||||
return $dataTableMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the intersection of children's metadata arrays (what they all have in common).
|
||||
*
|
||||
* @param string $name The metadata name.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMetadataIntersectArray($name)
|
||||
{
|
||||
$data = array();
|
||||
foreach ($this->getDataTables() as $childTable) {
|
||||
$childData = $childTable->getMetadata($name);
|
||||
if (is_array($childData)) {
|
||||
$data = array_intersect($data, $childData);
|
||||
}
|
||||
}
|
||||
return array_values($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link DataTable::getColumns()}.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getColumns()
|
||||
{
|
||||
foreach ($this->getDataTables() as $childTable) {
|
||||
if ($childTable->getRowsCount() > 0) {
|
||||
return $childTable->getColumns();
|
||||
}
|
||||
}
|
||||
return array();
|
||||
}
|
||||
}
|
||||
417
www/analytics/core/DataTable/Renderer.php
Normal file
417
www/analytics/core/DataTable/Renderer.php
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable;
|
||||
|
||||
use Exception;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Loader;
|
||||
use Piwik\Metrics;
|
||||
use Piwik\Piwik;
|
||||
|
||||
/**
|
||||
* A DataTable Renderer can produce an output given a DataTable object.
|
||||
* All new Renderers must be copied in DataTable/Renderer and added to the factory() method.
|
||||
* To use a renderer, simply do:
|
||||
* $render = new Xml();
|
||||
* $render->setTable($dataTable);
|
||||
* echo $render;
|
||||
*/
|
||||
abstract class Renderer
|
||||
{
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* @var Exception
|
||||
*/
|
||||
protected $exception;
|
||||
protected $renderSubTables = false;
|
||||
protected $hideIdSubDatatable = false;
|
||||
|
||||
/**
|
||||
* Whether to translate column names (i.e. metric names) or not
|
||||
* @var bool
|
||||
*/
|
||||
public $translateColumnNames = false;
|
||||
|
||||
/**
|
||||
* Column translations
|
||||
* @var array
|
||||
*/
|
||||
private $columnTranslations = false;
|
||||
|
||||
/**
|
||||
* The API method that has returned the data that should be rendered
|
||||
* @var string
|
||||
*/
|
||||
public $apiMethod = false;
|
||||
|
||||
/**
|
||||
* API metadata for the current report
|
||||
* @var array
|
||||
*/
|
||||
private $apiMetaData = null;
|
||||
|
||||
/**
|
||||
* The current idSite
|
||||
* @var int
|
||||
*/
|
||||
public $idSite = 'all';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to render subtables or not
|
||||
*
|
||||
* @param bool $enableRenderSubTable
|
||||
*/
|
||||
public function setRenderSubTables($enableRenderSubTable)
|
||||
{
|
||||
$this->renderSubTables = (bool)$enableRenderSubTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $bool
|
||||
*/
|
||||
public function setHideIdSubDatableFromResponse($bool)
|
||||
{
|
||||
$this->hideIdSubDatatable = (bool)$bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether to render subtables or not
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isRenderSubtables()
|
||||
{
|
||||
return $this->renderSubTables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output HTTP Content-Type header
|
||||
*/
|
||||
protected function renderHeader()
|
||||
{
|
||||
@header('Content-Type: text/plain; charset=utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the dataTable output and returns the string/binary
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function render();
|
||||
|
||||
/**
|
||||
* Computes the exception output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
abstract public function renderException();
|
||||
|
||||
protected function getExceptionMessage()
|
||||
{
|
||||
$message = $this->exception->getMessage();
|
||||
if (\Piwik_ShouldPrintBackTraceWithMessage()) {
|
||||
$message .= "\n" . $this->exception->getTraceAsString();
|
||||
}
|
||||
return self::renderHtmlEntities($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see render()
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the DataTable to be rendered
|
||||
*
|
||||
* @param DataTable|Simple|DataTable\Map $table table to be rendered
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setTable($table)
|
||||
{
|
||||
if (!is_array($table)
|
||||
&& !($table instanceof DataTable)
|
||||
&& !($table instanceof DataTable\Map)
|
||||
) {
|
||||
throw new Exception("DataTable renderers renderer accepts only DataTable and Map instances, and arrays.");
|
||||
}
|
||||
$this->table = $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Exception to be rendered
|
||||
*
|
||||
* @param Exception $exception to be rendered
|
||||
* @throws Exception
|
||||
*/
|
||||
public function setException($exception)
|
||||
{
|
||||
if (!($exception instanceof Exception)) {
|
||||
throw new Exception("The exception renderer accepts only an Exception object.");
|
||||
}
|
||||
$this->exception = $exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
static protected $availableRenderers = array('xml',
|
||||
'json',
|
||||
'csv',
|
||||
'tsv',
|
||||
'html',
|
||||
'php'
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns available renderers
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
static public function getRenderers()
|
||||
{
|
||||
return self::$availableRenderers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the DataTable associated to the output format $name
|
||||
*
|
||||
* @param string $name
|
||||
* @throws Exception If the renderer is unknown
|
||||
* @return \Piwik\DataTable\Renderer
|
||||
*/
|
||||
static public function factory($name)
|
||||
{
|
||||
$className = ucfirst(strtolower($name));
|
||||
$className = 'Piwik\DataTable\Renderer\\' . $className;
|
||||
try {
|
||||
Loader::loadClass($className);
|
||||
return new $className;
|
||||
} catch (Exception $e) {
|
||||
$availableRenderers = implode(', ', self::getRenderers());
|
||||
@header('Content-Type: text/plain; charset=utf-8');
|
||||
throw new Exception(Piwik::translate('General_ExceptionInvalidRendererFormat', array($className, $availableRenderers)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns $rawData after all applicable characters have been converted to HTML entities.
|
||||
*
|
||||
* @param String $rawData data to be converted
|
||||
* @return String
|
||||
*/
|
||||
static protected function renderHtmlEntities($rawData)
|
||||
{
|
||||
return self::formatValueXml($rawData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a value to xml
|
||||
*
|
||||
* @param string|number|bool $value value to format
|
||||
* @return int|string
|
||||
*/
|
||||
public static function formatValueXml($value)
|
||||
{
|
||||
if (is_string($value)
|
||||
&& !is_numeric($value)
|
||||
) {
|
||||
$value = html_entity_decode($value, ENT_COMPAT, 'UTF-8');
|
||||
// make sure non-UTF-8 chars don't cause htmlspecialchars to choke
|
||||
if (function_exists('mb_convert_encoding')) {
|
||||
$value = @mb_convert_encoding($value, 'UTF-8', 'UTF-8');
|
||||
}
|
||||
$value = htmlspecialchars($value, ENT_COMPAT, 'UTF-8');
|
||||
$htmlentities = array(" ", "¡", "¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "­", "®", "¯", "°", "±", "²", "³", "´", "µ", "¶", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à", "á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "€");
|
||||
$xmlentities = array("¢", "£", "¤", "¥", "¦", "§", "¨", "©", "ª", "«", "¬", "­", "®", "¯", "°", "±", "²", "³", "´", "µ", "¶", "·", "¸", "¹", "º", "»", "¼", "½", "¾", "¿", "À", "Á", "Â", "Ã", "Ä", "Å", "Æ", "Ç", "È", "É", "Ê", "Ë", "Ì", "Í", "Î", "Ï", "Ð", "Ñ", "Ò", "Ó", "Ô", "Õ", "Ö", "×", "Ø", "Ù", "Ú", "Û", "Ü", "Ý", "Þ", "ß", "à", "á", "â", "ã", "ä", "å", "æ", "ç", "è", "é", "ê", "ë", "ì", "í", "î", "ï", "ð", "ñ", "ò", "ó", "ô", "õ", "ö", "÷", "ø", "ù", "ú", "û", "ü", "ý", "þ", "ÿ", "€");
|
||||
$value = str_replace($htmlentities, $xmlentities, $value);
|
||||
} elseif ($value === false) {
|
||||
$value = 0;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate column names to the current language.
|
||||
* Used in subclasses.
|
||||
*
|
||||
* @param array $names
|
||||
* @return array
|
||||
*/
|
||||
protected function translateColumnNames($names)
|
||||
{
|
||||
if (!$this->apiMethod) {
|
||||
return $names;
|
||||
}
|
||||
|
||||
// load the translations only once
|
||||
// when multiple dates are requested (date=...,...&period=day), the meta data would
|
||||
// be loaded lots of times otherwise
|
||||
if ($this->columnTranslations === false) {
|
||||
$meta = $this->getApiMetaData();
|
||||
if ($meta === false) {
|
||||
return $names;
|
||||
}
|
||||
|
||||
$t = Metrics::getDefaultMetricTranslations();
|
||||
foreach (array('metrics', 'processedMetrics', 'metricsGoal', 'processedMetricsGoal') as $index) {
|
||||
if (isset($meta[$index]) && is_array($meta[$index])) {
|
||||
$t = array_merge($t, $meta[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->columnTranslations = & $t;
|
||||
}
|
||||
|
||||
foreach ($names as &$name) {
|
||||
if (isset($this->columnTranslations[$name])) {
|
||||
$name = $this->columnTranslations[$name];
|
||||
}
|
||||
}
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getApiMetaData()
|
||||
{
|
||||
if ($this->apiMetaData === null) {
|
||||
list($apiModule, $apiAction) = explode('.', $this->apiMethod);
|
||||
|
||||
if (!$apiModule || !$apiAction) {
|
||||
$this->apiMetaData = false;
|
||||
}
|
||||
|
||||
$api = \Piwik\Plugins\API\API::getInstance();
|
||||
$meta = $api->getMetadata($this->idSite, $apiModule, $apiAction);
|
||||
if (is_array($meta[0])) {
|
||||
$meta = $meta[0];
|
||||
}
|
||||
|
||||
$this->apiMetaData = & $meta;
|
||||
}
|
||||
|
||||
return $this->apiMetaData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates the given column name
|
||||
*
|
||||
* @param string $column
|
||||
* @return mixed
|
||||
*/
|
||||
protected function translateColumnName($column)
|
||||
{
|
||||
$columns = array($column);
|
||||
$columns = $this->translateColumnNames($columns);
|
||||
return $columns[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables column translating
|
||||
*
|
||||
* @param bool $bool
|
||||
*/
|
||||
public function setTranslateColumnNames($bool)
|
||||
{
|
||||
$this->translateColumnNames = $bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the api method
|
||||
*
|
||||
* @param $method
|
||||
*/
|
||||
public function setApiMethod($method)
|
||||
{
|
||||
$this->apiMethod = $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the site id
|
||||
*
|
||||
* @param int $idSite
|
||||
*/
|
||||
public function setIdSite($idSite)
|
||||
{
|
||||
$this->idSite = $idSite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if an array should be wrapped before rendering. This is used to
|
||||
* mimic quirks in the old rendering logic (for backwards compatibility). The
|
||||
* specific meaning of 'wrap' is left up to the Renderer. For XML, this means a
|
||||
* new <row> node. For JSON, this means wrapping in an array.
|
||||
*
|
||||
* In the old code, arrays were added to new DataTable instances, and then rendered.
|
||||
* This transformation wrapped associative arrays except under certain circumstances,
|
||||
* including:
|
||||
* - single element (ie, array('nb_visits' => 0)) (not wrapped for some renderers)
|
||||
* - empty array (ie, array())
|
||||
* - array w/ arrays/DataTable instances as values (ie,
|
||||
* array('name' => 'myreport',
|
||||
* 'reportData' => new DataTable())
|
||||
* OR array('name' => 'myreport',
|
||||
* 'reportData' => array(...)) )
|
||||
*
|
||||
* @param array $array
|
||||
* @param bool $wrapSingleValues Whether to wrap array('key' => 'value') arrays. Some
|
||||
* renderers wrap them and some don't.
|
||||
* @param bool|null $isAssociativeArray Whether the array is associative or not.
|
||||
* If null, it is determined.
|
||||
* @return bool
|
||||
*/
|
||||
protected static function shouldWrapArrayBeforeRendering(
|
||||
$array, $wrapSingleValues = true, $isAssociativeArray = null)
|
||||
{
|
||||
if (empty($array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($isAssociativeArray === null) {
|
||||
$isAssociativeArray = Piwik::isAssociativeArray($array);
|
||||
}
|
||||
|
||||
$wrap = true;
|
||||
if ($isAssociativeArray) {
|
||||
// we don't wrap if the array has one element that is a value
|
||||
$firstValue = reset($array);
|
||||
if (!$wrapSingleValues
|
||||
&& count($array) === 1
|
||||
&& (!is_array($firstValue)
|
||||
&& !is_object($firstValue))
|
||||
) {
|
||||
$wrap = false;
|
||||
} else {
|
||||
foreach ($array as $value) {
|
||||
if (is_array($value)
|
||||
|| is_object($value)
|
||||
) {
|
||||
$wrap = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$wrap = false;
|
||||
}
|
||||
|
||||
return $wrap;
|
||||
}
|
||||
}
|
||||
167
www/analytics/core/DataTable/Renderer/Console.php
Normal file
167
www/analytics/core/DataTable/Renderer/Console.php
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Renderer;
|
||||
|
||||
use Piwik\DataTable\Manager;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Renderer;
|
||||
|
||||
/**
|
||||
* Simple output
|
||||
*/
|
||||
class Console extends Renderer
|
||||
{
|
||||
/**
|
||||
* Prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $prefixRows = '#';
|
||||
|
||||
/**
|
||||
* Computes the dataTable output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$this->renderHeader();
|
||||
return $this->renderTable($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the exception output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderException()
|
||||
{
|
||||
$this->renderHeader();
|
||||
$exceptionMessage = $this->getExceptionMessage();
|
||||
return 'Error: ' . $exceptionMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the prefix to be used
|
||||
*
|
||||
* @param string $str new prefix
|
||||
*/
|
||||
public function setPrefixRow($str)
|
||||
{
|
||||
$this->prefixRows = $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output of the given array of data tables
|
||||
*
|
||||
* @param DataTable\Map $map data tables to render
|
||||
* @param string $prefix prefix to output before table data
|
||||
* @return string
|
||||
*/
|
||||
protected function renderDataTableMap(DataTable\Map $map, $prefix)
|
||||
{
|
||||
$output = "Set<hr />";
|
||||
$prefix = $prefix . ' ';
|
||||
foreach ($map->getDataTables() as $descTable => $table) {
|
||||
$output .= $prefix . "<b>" . $descTable . "</b><br />";
|
||||
$output .= $prefix . $this->renderTable($table, $prefix . ' ');
|
||||
$output .= "<hr />";
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the given dataTable output and returns the string/binary
|
||||
*
|
||||
* @param DataTable $table data table to render
|
||||
* @param string $prefix prefix to output before table data
|
||||
* @return string
|
||||
*/
|
||||
protected function renderTable($table, $prefix = "")
|
||||
{
|
||||
if (is_array($table)) // convert array to DataTable
|
||||
{
|
||||
$table = DataTable::makeFromSimpleArray($table);
|
||||
}
|
||||
|
||||
if ($table instanceof DataTable\Map) {
|
||||
return $this->renderDataTableMap($table, $prefix);
|
||||
}
|
||||
|
||||
if ($table->getRowsCount() == 0) {
|
||||
return "Empty table<br />\n";
|
||||
}
|
||||
|
||||
static $depth = 0;
|
||||
$output = '';
|
||||
$i = 1;
|
||||
foreach ($table->getRows() as $row) {
|
||||
$dataTableMapBreak = false;
|
||||
$columns = array();
|
||||
foreach ($row->getColumns() as $column => $value) {
|
||||
if ($value instanceof DataTable\Map) {
|
||||
$output .= $this->renderDataTableMap($value, $prefix);
|
||||
$dataTableMapBreak = true;
|
||||
break;
|
||||
}
|
||||
if (is_string($value)) $value = "'$value'";
|
||||
elseif (is_array($value)) $value = var_export($value, true);
|
||||
|
||||
$columns[] = "'$column' => $value";
|
||||
}
|
||||
if ($dataTableMapBreak === true) {
|
||||
continue;
|
||||
}
|
||||
$columns = implode(", ", $columns);
|
||||
|
||||
$metadata = array();
|
||||
foreach ($row->getMetadata() as $name => $value) {
|
||||
if (is_string($value)) $value = "'$value'";
|
||||
elseif (is_array($value)) $value = var_export($value, true);
|
||||
$metadata[] = "'$name' => $value";
|
||||
}
|
||||
$metadata = implode(", ", $metadata);
|
||||
|
||||
$output .= str_repeat($this->prefixRows, $depth)
|
||||
. "- $i [" . $columns . "] [" . $metadata . "] [idsubtable = "
|
||||
. $row->getIdSubDataTable() . "]<br />\n";
|
||||
|
||||
if (!is_null($row->getIdSubDataTable())) {
|
||||
if ($row->isSubtableLoaded()) {
|
||||
$depth++;
|
||||
$output .= $this->renderTable(
|
||||
Manager::getInstance()->getTable(
|
||||
$row->getIdSubDataTable()
|
||||
),
|
||||
$prefix . ' '
|
||||
);
|
||||
$depth--;
|
||||
} else {
|
||||
$output .= "-- Sub DataTable not loaded<br />\n";
|
||||
}
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
|
||||
$metadata = $table->getAllTableMetadata();
|
||||
if (!empty($metadata)) {
|
||||
$output .= "<hr />Metadata<br />";
|
||||
foreach ($metadata as $id => $metadataIn) {
|
||||
$output .= "<br />";
|
||||
$output .= $prefix . " <b>$id</b><br />";
|
||||
if(is_array($metadataIn)) {
|
||||
foreach ($metadataIn as $name => $value) {
|
||||
$output .= $prefix . $prefix . "$name => $value";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
403
www/analytics/core/DataTable/Renderer/Csv.php
Normal file
403
www/analytics/core/DataTable/Renderer/Csv.php
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Renderer;
|
||||
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable\Renderer;
|
||||
use Piwik\DataTable\Simple;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Date;
|
||||
use Piwik\Period;
|
||||
use Piwik\Period\Range;
|
||||
use Piwik\Piwik;
|
||||
use Piwik\ProxyHttp;
|
||||
|
||||
/**
|
||||
* CSV export
|
||||
*
|
||||
* When rendered using the default settings, a CSV report has the following characteristics:
|
||||
* The first record contains headers for all the columns in the report.
|
||||
* All rows have the same number of columns.
|
||||
* The default field delimiter string is a comma (,).
|
||||
* Formatting and layout are ignored.
|
||||
*
|
||||
*/
|
||||
class Csv extends Renderer
|
||||
{
|
||||
/**
|
||||
* Column separator
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $separator = ",";
|
||||
|
||||
/**
|
||||
* Line end
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $lineEnd = "\n";
|
||||
|
||||
/**
|
||||
* 'metadata' columns will be exported, prefixed by 'metadata_'
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $exportMetadata = true;
|
||||
|
||||
/**
|
||||
* Converts the content to unicode so that UTF8 characters (eg. chinese) can be imported in Excel
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $convertToUnicode = true;
|
||||
|
||||
/**
|
||||
* idSubtable will be exported in a column called 'idsubdatatable'
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $exportIdSubtable = true;
|
||||
|
||||
/**
|
||||
* This string is also hardcoded in archive,sh
|
||||
*/
|
||||
const NO_DATA_AVAILABLE = 'No data available';
|
||||
|
||||
/**
|
||||
* Computes the dataTable output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$str = $this->renderTable($this->table);
|
||||
if (empty($str)) {
|
||||
return self::NO_DATA_AVAILABLE;
|
||||
}
|
||||
|
||||
$this->renderHeader();
|
||||
|
||||
if ($this->convertToUnicode
|
||||
&& function_exists('mb_convert_encoding')
|
||||
) {
|
||||
$str = chr(255) . chr(254) . mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the exception output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function renderException()
|
||||
{
|
||||
@header('Content-Type: text/html; charset=utf-8');
|
||||
$exceptionMessage = $this->getExceptionMessage();
|
||||
return 'Error: ' . $exceptionMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables / Disables unicode converting
|
||||
*
|
||||
* @param $bool
|
||||
*/
|
||||
public function setConvertToUnicode($bool)
|
||||
{
|
||||
$this->convertToUnicode = $bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the column separator
|
||||
*
|
||||
* @param $separator
|
||||
*/
|
||||
public function setSeparator($separator)
|
||||
{
|
||||
$this->separator = $separator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output of the given data table
|
||||
*
|
||||
* @param DataTable|array $table
|
||||
* @param array $allColumns
|
||||
* @return string
|
||||
*/
|
||||
protected function renderTable($table, &$allColumns = array())
|
||||
{
|
||||
if (is_array($table)) // convert array to DataTable
|
||||
{
|
||||
$table = DataTable::makeFromSimpleArray($table);
|
||||
}
|
||||
|
||||
if ($table instanceof DataTable\Map) {
|
||||
$str = $this->renderDataTableMap($table, $allColumns);
|
||||
} else {
|
||||
$str = $this->renderDataTable($table, $allColumns);
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output of the given data table array
|
||||
*
|
||||
* @param DataTable\Map $table
|
||||
* @param array $allColumns
|
||||
* @return string
|
||||
*/
|
||||
protected function renderDataTableMap($table, &$allColumns = array())
|
||||
{
|
||||
$str = '';
|
||||
foreach ($table->getDataTables() as $currentLinePrefix => $dataTable) {
|
||||
$returned = explode("\n", $this->renderTable($dataTable, $allColumns));
|
||||
|
||||
// get rid of the columns names
|
||||
$returned = array_slice($returned, 1);
|
||||
|
||||
// case empty datatable we dont print anything in the CSV export
|
||||
// when in xml we would output <result date="2008-01-15" />
|
||||
if (!empty($returned)) {
|
||||
foreach ($returned as &$row) {
|
||||
$row = $currentLinePrefix . $this->separator . $row;
|
||||
}
|
||||
$str .= "\n" . implode("\n", $returned);
|
||||
}
|
||||
}
|
||||
|
||||
// prepend table key to column list
|
||||
$allColumns = array_merge(array($table->getKeyName() => true), $allColumns);
|
||||
|
||||
// add header to output string
|
||||
$str = $this->getHeaderLine(array_keys($allColumns)) . $str;
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the output of the given simple data table
|
||||
*
|
||||
* @param DataTable|Simple $table
|
||||
* @param array $allColumns
|
||||
* @return string
|
||||
*/
|
||||
protected function renderDataTable($table, &$allColumns = array())
|
||||
{
|
||||
if ($table instanceof Simple) {
|
||||
$row = $table->getFirstRow();
|
||||
if ($row !== false) {
|
||||
$columnNameToValue = $row->getColumns();
|
||||
if (count($columnNameToValue) == 1) {
|
||||
// simple tables should only have one column, the value
|
||||
$allColumns['value'] = true;
|
||||
|
||||
$value = array_values($columnNameToValue);
|
||||
$str = 'value' . $this->lineEnd . $this->formatValue($value[0]);
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$csv = array();
|
||||
foreach ($table->getRows() as $row) {
|
||||
$csvRow = $this->flattenColumnArray($row->getColumns());
|
||||
|
||||
if ($this->exportMetadata) {
|
||||
$metadata = $row->getMetadata();
|
||||
foreach ($metadata as $name => $value) {
|
||||
if ($name == 'idsubdatatable_in_db') {
|
||||
continue;
|
||||
}
|
||||
//if a metadata and a column have the same name make sure they dont overwrite
|
||||
if ($this->translateColumnNames) {
|
||||
$name = Piwik::translate('General_Metadata') . ': ' . $name;
|
||||
} else {
|
||||
$name = 'metadata_' . $name;
|
||||
}
|
||||
|
||||
$csvRow[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($csvRow as $name => $value) {
|
||||
$allColumns[$name] = true;
|
||||
}
|
||||
|
||||
if ($this->exportIdSubtable) {
|
||||
$idsubdatatable = $row->getIdSubDataTable();
|
||||
if ($idsubdatatable !== false
|
||||
&& $this->hideIdSubDatatable === false
|
||||
) {
|
||||
$csvRow['idsubdatatable'] = $idsubdatatable;
|
||||
}
|
||||
}
|
||||
|
||||
$csv[] = $csvRow;
|
||||
}
|
||||
|
||||
// now we make sure that all the rows in the CSV array have all the columns
|
||||
foreach ($csv as &$row) {
|
||||
foreach ($allColumns as $columnName => $true) {
|
||||
if (!isset($row[$columnName])) {
|
||||
$row[$columnName] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$str = '';
|
||||
|
||||
// specific case, we have only one column and this column wasn't named properly (indexed by a number)
|
||||
// we don't print anything in the CSV file => an empty line
|
||||
if (sizeof($allColumns) == 1
|
||||
&& reset($allColumns)
|
||||
&& !is_string(key($allColumns))
|
||||
) {
|
||||
$str .= '';
|
||||
} else {
|
||||
// render row names
|
||||
$str .= $this->getHeaderLine(array_keys($allColumns)) . $this->lineEnd;
|
||||
}
|
||||
|
||||
// we render the CSV
|
||||
foreach ($csv as $theRow) {
|
||||
$rowStr = '';
|
||||
foreach ($allColumns as $columnName => $true) {
|
||||
$rowStr .= $this->formatValue($theRow[$columnName]) . $this->separator;
|
||||
}
|
||||
// remove the last separator
|
||||
$rowStr = substr_replace($rowStr, "", -strlen($this->separator));
|
||||
$str .= $rowStr . $this->lineEnd;
|
||||
}
|
||||
$str = substr($str, 0, -strlen($this->lineEnd));
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CSV header line for a set of metrics. Will translate columns if desired.
|
||||
*
|
||||
* @param array $columnMetrics
|
||||
* @return array
|
||||
*/
|
||||
private function getHeaderLine($columnMetrics)
|
||||
{
|
||||
if ($this->translateColumnNames) {
|
||||
$columnMetrics = $this->translateColumnNames($columnMetrics);
|
||||
}
|
||||
return implode($this->separator, $columnMetrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats/Escapes the given value
|
||||
*
|
||||
* @param mixed $value
|
||||
* @return string
|
||||
*/
|
||||
protected function formatValue($value)
|
||||
{
|
||||
if (is_string($value)
|
||||
&& !is_numeric($value)
|
||||
) {
|
||||
$value = html_entity_decode($value, ENT_COMPAT, 'UTF-8');
|
||||
} elseif ($value === false) {
|
||||
$value = 0;
|
||||
}
|
||||
if (is_string($value)
|
||||
&& (strpos($value, '"') !== false
|
||||
|| strpos($value, $this->separator) !== false)
|
||||
) {
|
||||
$value = '"' . str_replace('"', '""', $value) . '"';
|
||||
}
|
||||
|
||||
// in some number formats (e.g. German), the decimal separator is a comma
|
||||
// we need to catch and replace this
|
||||
if (is_numeric($value)) {
|
||||
$value = (string)$value;
|
||||
$value = str_replace(',', '.', $value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the http headers for csv file
|
||||
*/
|
||||
protected function renderHeader()
|
||||
{
|
||||
$fileName = 'Piwik ' . Piwik::translate('General_Export');
|
||||
|
||||
$period = Common::getRequestVar('period', false);
|
||||
$date = Common::getRequestVar('date', false);
|
||||
if ($period || $date) // in test cases, there are no request params set
|
||||
{
|
||||
if ($period == 'range') {
|
||||
$period = new Range($period, $date);
|
||||
} else if (strpos($date, ',') !== false) {
|
||||
$period = new Range('range', $date);
|
||||
} else {
|
||||
$period = Period::factory($period, Date::factory($date));
|
||||
}
|
||||
|
||||
$prettyDate = $period->getLocalizedLongString();
|
||||
|
||||
$meta = $this->getApiMetaData();
|
||||
|
||||
$fileName .= ' _ ' . $meta['name']
|
||||
. ' _ ' . $prettyDate . '.csv';
|
||||
}
|
||||
|
||||
// silent fail otherwise unit tests fail
|
||||
@header('Content-Type: application/vnd.ms-excel');
|
||||
@header('Content-Disposition: attachment; filename="' . $fileName . '"');
|
||||
ProxyHttp::overrideCacheControlHeaders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens an array of column values so they can be outputted as CSV (which does not support
|
||||
* nested structures).
|
||||
*/
|
||||
private function flattenColumnArray($columns, &$csvRow = array(), $csvColumnNameTemplate = '%s')
|
||||
{
|
||||
foreach ($columns as $name => $value) {
|
||||
$csvName = sprintf($csvColumnNameTemplate, $this->getCsvColumnName($name));
|
||||
|
||||
if (is_array($value)) {
|
||||
// if we're translating column names and this is an array of arrays, the column name
|
||||
// format becomes a bit more complicated. also in this case, we assume $value is not
|
||||
// nested beyond 2 levels (ie, array(0 => array(0 => 1, 1 => 2)), but not array(
|
||||
// 0 => array(0 => array(), 1 => array())) )
|
||||
if ($this->translateColumnNames
|
||||
&& is_array(reset($value))
|
||||
) {
|
||||
foreach ($value as $level1Key => $level1Value) {
|
||||
$inner = $name == 'goals' ? Piwik::translate('Goals_GoalX', $level1Key) : $name . ' ' . $level1Key;
|
||||
$columnNameTemplate = '%s (' . $inner . ')';
|
||||
|
||||
$this->flattenColumnArray($level1Value, $csvRow, $columnNameTemplate);
|
||||
}
|
||||
} else {
|
||||
$this->flattenColumnArray($value, $csvRow, $csvName . '_%s');
|
||||
}
|
||||
} else {
|
||||
$csvRow[$csvName] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $csvRow;
|
||||
}
|
||||
|
||||
private function getCsvColumnName($name)
|
||||
{
|
||||
if ($this->translateColumnNames) {
|
||||
return $this->translateColumnName($name);
|
||||
} else {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
207
www/analytics/core/DataTable/Renderer/Html.php
Normal file
207
www/analytics/core/DataTable/Renderer/Html.php
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Renderer;
|
||||
|
||||
use Exception;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Renderer;
|
||||
|
||||
/**
|
||||
* Simple HTML output
|
||||
* Does not work with recursive DataTable (i.e., when a row can be associated with a subDataTable).
|
||||
*
|
||||
*/
|
||||
class Html extends Renderer
|
||||
{
|
||||
protected $tableId;
|
||||
protected $allColumns;
|
||||
protected $tableStructure;
|
||||
protected $i;
|
||||
|
||||
/**
|
||||
* Sets the table id
|
||||
*
|
||||
* @param string $id
|
||||
*/
|
||||
function setTableId($id)
|
||||
{
|
||||
$this->tableId = str_replace('.', '_', $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Output HTTP Content-Type header
|
||||
*/
|
||||
protected function renderHeader()
|
||||
{
|
||||
@header('Content-Type: text/html; charset=utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the dataTable output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function render()
|
||||
{
|
||||
$this->renderHeader();
|
||||
$this->tableStructure = array();
|
||||
$this->allColumns = array();
|
||||
$this->i = 0;
|
||||
|
||||
return $this->renderTable($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the exception output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function renderException()
|
||||
{
|
||||
$this->renderHeader();
|
||||
$exceptionMessage = $this->getExceptionMessage();
|
||||
return nl2br($exceptionMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output for the given data table
|
||||
*
|
||||
* @param DataTable $table
|
||||
* @return string
|
||||
*/
|
||||
protected function renderTable($table)
|
||||
{
|
||||
if (is_array($table)) // convert array to DataTable
|
||||
{
|
||||
$table = DataTable::makeFromSimpleArray($table);
|
||||
}
|
||||
|
||||
if ($table instanceof DataTable\Map) {
|
||||
foreach ($table->getDataTables() as $date => $subtable) {
|
||||
if ($subtable->getRowsCount()) {
|
||||
$this->buildTableStructure($subtable, '_' . $table->getKeyName(), $date);
|
||||
}
|
||||
}
|
||||
} else // Simple
|
||||
{
|
||||
if ($table->getRowsCount()) {
|
||||
$this->buildTableStructure($table);
|
||||
}
|
||||
}
|
||||
|
||||
$out = $this->renderDataTable();
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given data table to the table structure array
|
||||
*
|
||||
* @param DataTable $table
|
||||
* @param null|string $columnToAdd
|
||||
* @param null|string $valueToAdd
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function buildTableStructure($table, $columnToAdd = null, $valueToAdd = null)
|
||||
{
|
||||
$i = $this->i;
|
||||
$someMetadata = false;
|
||||
$someIdSubTable = false;
|
||||
|
||||
/*
|
||||
* table = array
|
||||
* ROW1 = col1 | col2 | col3 | metadata | idSubTable
|
||||
* ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable
|
||||
*/
|
||||
if (!($table instanceof DataTable)) {
|
||||
throw new Exception("HTML Renderer does not work with this combination of parameters");
|
||||
}
|
||||
foreach ($table->getRows() as $row) {
|
||||
if (isset($columnToAdd) && isset($valueToAdd)) {
|
||||
$this->allColumns[$columnToAdd] = true;
|
||||
$this->tableStructure[$i][$columnToAdd] = $valueToAdd;
|
||||
}
|
||||
|
||||
foreach ($row->getColumns() as $column => $value) {
|
||||
$this->allColumns[$column] = true;
|
||||
$this->tableStructure[$i][$column] = $value;
|
||||
}
|
||||
|
||||
$metadata = array();
|
||||
foreach ($row->getMetadata() as $name => $value) {
|
||||
if (is_string($value)) $value = "'$value'";
|
||||
$metadata[] = "'$name' => $value";
|
||||
}
|
||||
|
||||
if (count($metadata) != 0) {
|
||||
$someMetadata = true;
|
||||
$metadata = implode("<br />", $metadata);
|
||||
$this->tableStructure[$i]['_metadata'] = $metadata;
|
||||
}
|
||||
|
||||
$idSubtable = $row->getIdSubDataTable();
|
||||
if (!is_null($idSubtable)) {
|
||||
$someIdSubTable = true;
|
||||
$this->tableStructure[$i]['_idSubtable'] = $idSubtable;
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
$this->i = $i;
|
||||
|
||||
$this->allColumns['_metadata'] = $someMetadata;
|
||||
$this->allColumns['_idSubtable'] = $someIdSubTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output for the table structure array
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function renderDataTable()
|
||||
{
|
||||
$html = "<table " . ($this->tableId ? "id=\"{$this->tableId}\" " : "") . "border=\"1\">\n<thead>\n\t<tr>\n";
|
||||
|
||||
foreach ($this->allColumns as $name => $toDisplay) {
|
||||
if ($toDisplay !== false) {
|
||||
if ($name === 0) {
|
||||
$name = 'value';
|
||||
}
|
||||
if ($this->translateColumnNames) {
|
||||
$name = $this->translateColumnName($name);
|
||||
}
|
||||
$html .= "\t\t<th>$name</th>\n";
|
||||
}
|
||||
}
|
||||
|
||||
$html .= "\t</tr>\n</thead>\n<tbody>\n";
|
||||
|
||||
foreach ($this->tableStructure as $row) {
|
||||
$html .= "\t<tr>\n";
|
||||
foreach ($this->allColumns as $name => $toDisplay) {
|
||||
if ($toDisplay !== false) {
|
||||
$value = "-";
|
||||
if (isset($row[$name])) {
|
||||
if (is_array($row[$name])) {
|
||||
$value = "<pre>" . self::formatValueXml(var_export($row[$name], true)) . "</pre>";
|
||||
} else {
|
||||
$value = self::formatValueXml($row[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
$html .= "\t\t<td>$value</td>\n";
|
||||
}
|
||||
}
|
||||
$html .= "\t</tr>\n";
|
||||
}
|
||||
|
||||
$html .= "</tbody>\n</table>\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
140
www/analytics/core/DataTable/Renderer/Json.php
Normal file
140
www/analytics/core/DataTable/Renderer/Json.php
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Renderer;
|
||||
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable\Renderer;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\ProxyHttp;
|
||||
|
||||
/**
|
||||
* JSON export.
|
||||
* Works with recursive DataTable (when a row can be associated with a subDataTable).
|
||||
*
|
||||
*/
|
||||
class Json extends Renderer
|
||||
{
|
||||
/**
|
||||
* Computes the dataTable output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$this->renderHeader();
|
||||
return $this->renderTable($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the exception output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function renderException()
|
||||
{
|
||||
$this->renderHeader();
|
||||
|
||||
$exceptionMessage = $this->getExceptionMessage();
|
||||
$exceptionMessage = str_replace(array("\r\n", "\n"), "", $exceptionMessage);
|
||||
|
||||
$result = json_encode(array('result' => 'error', 'message' => $exceptionMessage));
|
||||
|
||||
return $this->jsonpWrap($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output for the given data table
|
||||
*
|
||||
* @param DataTable $table
|
||||
* @return string
|
||||
*/
|
||||
protected function renderTable($table)
|
||||
{
|
||||
if (is_array($table)) {
|
||||
$array = $table;
|
||||
if (self::shouldWrapArrayBeforeRendering($array, $wrapSingleValues = true)) {
|
||||
$array = array($array);
|
||||
}
|
||||
|
||||
foreach ($array as $key => $tab) {
|
||||
if ($tab instanceof DataTable\Map
|
||||
|| $tab instanceof DataTable
|
||||
|| $tab instanceof DataTable\Simple) {
|
||||
$array[$key] = $this->convertDataTableToArray($tab);
|
||||
|
||||
if (!is_array($array[$key])) {
|
||||
$array[$key] = array('value' => $array[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
$array = $this->convertDataTableToArray($table);
|
||||
}
|
||||
|
||||
if (!is_array($array)) {
|
||||
$array = array('value' => $array);
|
||||
}
|
||||
|
||||
// decode all entities
|
||||
$callback = function (&$value, $key) {
|
||||
if (is_string($value)) {
|
||||
$value = html_entity_decode($value, ENT_QUOTES, "UTF-8");
|
||||
};
|
||||
};
|
||||
array_walk_recursive($array, $callback);
|
||||
|
||||
$str = json_encode($array);
|
||||
|
||||
return $this->jsonpWrap($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $str
|
||||
* @return string
|
||||
*/
|
||||
protected function jsonpWrap($str)
|
||||
{
|
||||
if (($jsonCallback = Common::getRequestVar('callback', false)) === false)
|
||||
$jsonCallback = Common::getRequestVar('jsoncallback', false);
|
||||
if ($jsonCallback !== false) {
|
||||
if (preg_match('/^[0-9a-zA-Z_.]*$/D', $jsonCallback) > 0) {
|
||||
$str = $jsonCallback . "(" . $str . ")";
|
||||
}
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the http header for json file
|
||||
*/
|
||||
protected function renderHeader()
|
||||
{
|
||||
self::sendHeaderJSON();
|
||||
ProxyHttp::overrideCacheControlHeaders();
|
||||
}
|
||||
|
||||
public static function sendHeaderJSON()
|
||||
{
|
||||
@header('Content-Type: application/json; charset=utf-8');
|
||||
}
|
||||
|
||||
private function convertDataTableToArray($table)
|
||||
{
|
||||
$renderer = new Php();
|
||||
$renderer->setTable($table);
|
||||
$renderer->setRenderSubTables($this->isRenderSubtables());
|
||||
$renderer->setSerialize(false);
|
||||
$renderer->setHideIdSubDatableFromResponse($this->hideIdSubDatatable);
|
||||
$array = $renderer->flatRender();
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
271
www/analytics/core/DataTable/Renderer/Php.php
Normal file
271
www/analytics/core/DataTable/Renderer/Php.php
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Renderer;
|
||||
|
||||
use Exception;
|
||||
use Piwik\DataTable\Manager;
|
||||
use Piwik\DataTable\Renderer;
|
||||
use Piwik\DataTable\Simple;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Piwik;
|
||||
|
||||
/**
|
||||
* Returns the equivalent PHP array for a given DataTable.
|
||||
* You can specify in the constructor if you want the serialized version.
|
||||
* Please note that by default it will produce a flat version of the array.
|
||||
* See the method flatRender() for details. @see flatRender();
|
||||
*
|
||||
* Works with recursive DataTable (when a row can be associated with a subDataTable).
|
||||
*
|
||||
*/
|
||||
class Php extends Renderer
|
||||
{
|
||||
protected $prettyDisplay = false;
|
||||
protected $serialize = true;
|
||||
|
||||
/**
|
||||
* Enables/Disables serialize
|
||||
*
|
||||
* @param bool $bool
|
||||
*/
|
||||
public function setSerialize($bool)
|
||||
{
|
||||
$this->serialize = (bool)$bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables/Disables pretty display
|
||||
*
|
||||
* @param bool $bool
|
||||
*/
|
||||
public function setPrettyDisplay($bool)
|
||||
{
|
||||
$this->prettyDisplay = (bool)$bool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts current data table to string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$data = $this->render();
|
||||
if (!is_string($data)) {
|
||||
$data = serialize($data);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the dataTable output and returns the string/binary
|
||||
*
|
||||
* @param null|DataTable|DataTable\Map|Simple $dataTable
|
||||
* @return string
|
||||
*/
|
||||
public function render($dataTable = null)
|
||||
{
|
||||
$this->renderHeader();
|
||||
|
||||
if (is_null($dataTable)) {
|
||||
$dataTable = $this->table;
|
||||
}
|
||||
$toReturn = $this->flatRender($dataTable);
|
||||
|
||||
if ($this->prettyDisplay) {
|
||||
if (!is_array($toReturn)) {
|
||||
$toReturn = unserialize($toReturn);
|
||||
}
|
||||
$toReturn = "<pre>" . var_export($toReturn, true) . "</pre>";
|
||||
}
|
||||
return $toReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the exception output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderException()
|
||||
{
|
||||
$this->renderHeader();
|
||||
|
||||
$exceptionMessage = $this->getExceptionMessage();
|
||||
|
||||
$return = array('result' => 'error', 'message' => $exceptionMessage);
|
||||
|
||||
if ($this->serialize) {
|
||||
$return = serialize($return);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a flat php array from the DataTable, putting "columns" and "metadata" on the same level.
|
||||
*
|
||||
* For example, when a originalRender() would be
|
||||
* array( 'columns' => array( 'col1_name' => value1, 'col2_name' => value2 ),
|
||||
* 'metadata' => array( 'metadata1_name' => value_metadata) )
|
||||
*
|
||||
* a flatRender() is
|
||||
* array( 'col1_name' => value1,
|
||||
* 'col2_name' => value2,
|
||||
* 'metadata1_name' => value_metadata )
|
||||
*
|
||||
* @param null|DataTable|DataTable\Map|Simple $dataTable
|
||||
* @return array Php array representing the 'flat' version of the datatable
|
||||
*/
|
||||
public function flatRender($dataTable = null)
|
||||
{
|
||||
if (is_null($dataTable)) {
|
||||
$dataTable = $this->table;
|
||||
}
|
||||
|
||||
if (is_array($dataTable)) {
|
||||
$flatArray = $dataTable;
|
||||
if (self::shouldWrapArrayBeforeRendering($flatArray)) {
|
||||
$flatArray = array($flatArray);
|
||||
}
|
||||
} else if ($dataTable instanceof DataTable\Map) {
|
||||
$flatArray = array();
|
||||
foreach ($dataTable->getDataTables() as $keyName => $table) {
|
||||
$serializeSave = $this->serialize;
|
||||
$this->serialize = false;
|
||||
$flatArray[$keyName] = $this->flatRender($table);
|
||||
$this->serialize = $serializeSave;
|
||||
}
|
||||
} else if ($dataTable instanceof Simple) {
|
||||
$flatArray = $this->renderSimpleTable($dataTable);
|
||||
|
||||
// if we return only one numeric value then we print out the result in a simple <result> tag
|
||||
// keep it simple!
|
||||
if (count($flatArray) == 1) {
|
||||
$flatArray = current($flatArray);
|
||||
}
|
||||
} // A normal DataTable needs to be handled specifically
|
||||
else {
|
||||
$array = $this->renderTable($dataTable);
|
||||
$flatArray = $this->flattenArray($array);
|
||||
}
|
||||
|
||||
if ($this->serialize) {
|
||||
$flatArray = serialize($flatArray);
|
||||
}
|
||||
|
||||
return $flatArray;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $array
|
||||
* @return array
|
||||
*/
|
||||
protected function flattenArray($array)
|
||||
{
|
||||
$flatArray = array();
|
||||
foreach ($array as $row) {
|
||||
$newRow = $row['columns'] + $row['metadata'];
|
||||
if (isset($row['idsubdatatable'])
|
||||
&& $this->hideIdSubDatatable === false
|
||||
) {
|
||||
$newRow += array('idsubdatatable' => $row['idsubdatatable']);
|
||||
}
|
||||
if (isset($row['subtable'])) {
|
||||
$newRow += array('subtable' => $this->flattenArray($row['subtable']));
|
||||
}
|
||||
$flatArray[] = $newRow;
|
||||
}
|
||||
return $flatArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the current data table to an array
|
||||
*
|
||||
* @return array
|
||||
* @throws Exception
|
||||
*/
|
||||
public function originalRender()
|
||||
{
|
||||
Piwik::checkObjectTypeIs($this->table, array('Simple', 'DataTable'));
|
||||
|
||||
if ($this->table instanceof Simple) {
|
||||
$array = $this->renderSimpleTable($this->table);
|
||||
} elseif ($this->table instanceof DataTable) {
|
||||
$array = $this->renderTable($this->table);
|
||||
}
|
||||
|
||||
if ($this->serialize) {
|
||||
$array = serialize($array);
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given data table to an array
|
||||
*
|
||||
* @param DataTable $table
|
||||
* @return array
|
||||
*/
|
||||
protected function renderTable($table)
|
||||
{
|
||||
$array = array();
|
||||
|
||||
foreach ($table->getRows() as $id => $row) {
|
||||
$newRow = array(
|
||||
'columns' => $row->getColumns(),
|
||||
'metadata' => $row->getMetadata(),
|
||||
'idsubdatatable' => $row->getIdSubDataTable(),
|
||||
);
|
||||
|
||||
if ($id == DataTable::ID_SUMMARY_ROW) {
|
||||
$newRow['issummaryrow'] = true;
|
||||
}
|
||||
|
||||
if ($this->isRenderSubtables()
|
||||
&& $row->isSubtableLoaded()
|
||||
) {
|
||||
$subTable = $this->renderTable(Manager::getInstance()->getTable($row->getIdSubDataTable()));
|
||||
$newRow['subtable'] = $subTable;
|
||||
if ($this->hideIdSubDatatable === false
|
||||
&& isset($newRow['metadata']['idsubdatatable_in_db'])
|
||||
) {
|
||||
$newRow['columns']['idsubdatatable'] = $newRow['metadata']['idsubdatatable_in_db'];
|
||||
}
|
||||
unset($newRow['metadata']['idsubdatatable_in_db']);
|
||||
}
|
||||
if ($this->hideIdSubDatatable !== false) {
|
||||
unset($newRow['idsubdatatable']);
|
||||
}
|
||||
|
||||
$array[] = $newRow;
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the simple data table to an array
|
||||
*
|
||||
* @param Simple $table
|
||||
* @return array
|
||||
*/
|
||||
protected function renderSimpleTable($table)
|
||||
{
|
||||
$array = array();
|
||||
|
||||
$row = $table->getFirstRow();
|
||||
if ($row === false) {
|
||||
return $array;
|
||||
}
|
||||
foreach ($row->getColumns() as $columnName => $columnValue) {
|
||||
$array[$columnName] = $columnValue;
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
207
www/analytics/core/DataTable/Renderer/Rss.php
Normal file
207
www/analytics/core/DataTable/Renderer/Rss.php
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Renderer;
|
||||
|
||||
use Exception;
|
||||
use Piwik\Archive;
|
||||
use Piwik\Common;
|
||||
use Piwik\DataTable\Renderer;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Date;
|
||||
use Piwik\SettingsPiwik;
|
||||
use Piwik\Url;
|
||||
|
||||
/**
|
||||
* RSS Feed.
|
||||
* The RSS renderer can be used only on Set that are arrays of DataTable.
|
||||
* A RSS feed contains one dataTable per element in the Set.
|
||||
*
|
||||
*/
|
||||
class Rss extends Renderer
|
||||
{
|
||||
/**
|
||||
* Computes the dataTable output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function render()
|
||||
{
|
||||
$this->renderHeader();
|
||||
return $this->renderTable($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the exception output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function renderException()
|
||||
{
|
||||
header('Content-type: text/plain');
|
||||
$exceptionMessage = $this->getExceptionMessage();
|
||||
return 'Error: ' . $exceptionMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output for the given data table
|
||||
*
|
||||
* @param DataTable $table
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function renderTable($table)
|
||||
{
|
||||
if (!($table instanceof DataTable\Map)
|
||||
|| $table->getKeyName() != 'date'
|
||||
) {
|
||||
throw new Exception("RSS feeds can be generated for one specific website &idSite=X." .
|
||||
"\nPlease specify only one idSite or consider using &format=XML instead.");
|
||||
}
|
||||
|
||||
$idSite = Common::getRequestVar('idSite', 1, 'int');
|
||||
$period = Common::getRequestVar('period');
|
||||
|
||||
$piwikUrl = SettingsPiwik::getPiwikUrl()
|
||||
. "?module=CoreHome&action=index&idSite=" . $idSite . "&period=" . $period;
|
||||
$out = "";
|
||||
$moreRecentFirst = array_reverse($table->getDataTables(), true);
|
||||
foreach ($moreRecentFirst as $date => $subtable) {
|
||||
/** @var DataTable $subtable */
|
||||
$timestamp = $subtable->getMetadata(Archive\DataTableFactory::TABLE_METADATA_PERIOD_INDEX)->getDateStart()->getTimestamp();
|
||||
$site = $subtable->getMetadata(Archive\DataTableFactory::TABLE_METADATA_SITE_INDEX);
|
||||
|
||||
$pudDate = date('r', $timestamp);
|
||||
|
||||
$dateInSiteTimezone = Date::factory($timestamp)->setTimezone($site->getTimezone())->toString('Y-m-d');
|
||||
$thisPiwikUrl = Common::sanitizeInputValue($piwikUrl . "&date=$dateInSiteTimezone");
|
||||
$siteName = $site->getName();
|
||||
$title = $siteName . " on " . $date;
|
||||
|
||||
$out .= "\t<item>
|
||||
<pubDate>$pudDate</pubDate>
|
||||
<guid>$thisPiwikUrl</guid>
|
||||
<link>$thisPiwikUrl</link>
|
||||
<title>$title</title>
|
||||
<author>http://piwik.org</author>
|
||||
<description>";
|
||||
|
||||
$out .= Common::sanitizeInputValue($this->renderDataTable($subtable));
|
||||
$out .= "</description>\n\t</item>\n";
|
||||
}
|
||||
|
||||
$header = $this->getRssHeader();
|
||||
$footer = $this->getRssFooter();
|
||||
|
||||
return $header . $out . $footer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the xml file http header
|
||||
*/
|
||||
protected function renderHeader()
|
||||
{
|
||||
@header('Content-Type: text/xml; charset=utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS file footer
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getRssFooter()
|
||||
{
|
||||
return "\t</channel>\n</rss>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RSS file header
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getRssHeader()
|
||||
{
|
||||
$generationDate = date('r');
|
||||
$header = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
||||
<rss version=\"2.0\">
|
||||
<channel>
|
||||
<title>piwik statistics - RSS</title>
|
||||
<link>http://piwik.org</link>
|
||||
<description>Piwik RSS feed</description>
|
||||
<pubDate>$generationDate</pubDate>
|
||||
<generator>piwik</generator>
|
||||
<language>en</language>
|
||||
<lastBuildDate>$generationDate</lastBuildDate>";
|
||||
return $header;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DataTable $table
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function renderDataTable($table)
|
||||
{
|
||||
if ($table->getRowsCount() == 0) {
|
||||
return "<strong><em>Empty table</em></strong><br />\n";
|
||||
}
|
||||
|
||||
$i = 1;
|
||||
$tableStructure = array();
|
||||
|
||||
/*
|
||||
* table = array
|
||||
* ROW1 = col1 | col2 | col3 | metadata | idSubTable
|
||||
* ROW2 = col1 | col2 (no value but appears) | col3 | metadata | idSubTable
|
||||
* subtable here
|
||||
*/
|
||||
$allColumns = array();
|
||||
foreach ($table->getRows() as $row) {
|
||||
foreach ($row->getColumns() as $column => $value) {
|
||||
// for example, goals data is array: not supported in export RSS
|
||||
// in the future we shall reuse ViewDataTable for html exports in RSS anyway
|
||||
if (is_array($value)) {
|
||||
continue;
|
||||
}
|
||||
$allColumns[$column] = true;
|
||||
$tableStructure[$i][$column] = $value;
|
||||
}
|
||||
$i++;
|
||||
}
|
||||
$html = "\n";
|
||||
$html .= "<table border=1 width=70%>";
|
||||
$html .= "\n<tr>";
|
||||
foreach ($allColumns as $name => $toDisplay) {
|
||||
if ($toDisplay !== false) {
|
||||
if ($this->translateColumnNames) {
|
||||
$name = $this->translateColumnName($name);
|
||||
}
|
||||
$html .= "\n\t<td><strong>$name</strong></td>";
|
||||
}
|
||||
}
|
||||
$html .= "\n</tr>";
|
||||
$colspan = count($allColumns);
|
||||
|
||||
foreach ($tableStructure as $row) {
|
||||
$html .= "\n\n<tr>";
|
||||
foreach ($allColumns as $columnName => $toDisplay) {
|
||||
if ($toDisplay !== false) {
|
||||
$value = "-";
|
||||
if (isset($row[$columnName])) {
|
||||
$value = urldecode($row[$columnName]);
|
||||
}
|
||||
|
||||
$html .= "\n\t<td>$value</td>";
|
||||
}
|
||||
}
|
||||
$html .= "</tr>";
|
||||
}
|
||||
$html .= "\n\n</table>";
|
||||
return $html;
|
||||
}
|
||||
}
|
||||
39
www/analytics/core/DataTable/Renderer/Tsv.php
Normal file
39
www/analytics/core/DataTable/Renderer/Tsv.php
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Renderer;
|
||||
|
||||
|
||||
/**
|
||||
* TSV export
|
||||
*
|
||||
* Excel doesn't import CSV properly, it expects TAB separated values by default.
|
||||
* TSV is therefore the 'CSV' that is Excel compatible
|
||||
*
|
||||
*/
|
||||
class Tsv extends Csv
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setSeparator("\t");
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the dataTable output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function render()
|
||||
{
|
||||
return parent::render();
|
||||
}
|
||||
}
|
||||
443
www/analytics/core/DataTable/Renderer/Xml.php
Normal file
443
www/analytics/core/DataTable/Renderer/Xml.php
Normal file
|
|
@ -0,0 +1,443 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Renderer;
|
||||
|
||||
use Exception;
|
||||
use Piwik\DataTable\Map;
|
||||
use Piwik\DataTable\Renderer;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Simple;
|
||||
use Piwik\Piwik;
|
||||
|
||||
/**
|
||||
* XML export of a given DataTable.
|
||||
* See the tests cases for more information about the XML format (/tests/core/DataTable/Renderer.test.php)
|
||||
* Or have a look at the API calls examples.
|
||||
*
|
||||
* Works with recursive DataTable (when a row can be associated with a subDataTable).
|
||||
*
|
||||
*/
|
||||
class Xml extends Renderer
|
||||
{
|
||||
/**
|
||||
* Computes the dataTable output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function render()
|
||||
{
|
||||
$this->renderHeader();
|
||||
return '<?xml version="1.0" encoding="utf-8" ?>' . "\n" . $this->renderTable($this->table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the exception output and returns the string/binary
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function renderException()
|
||||
{
|
||||
$this->renderHeader();
|
||||
|
||||
$exceptionMessage = $this->getExceptionMessage();
|
||||
|
||||
$return = '<?xml version="1.0" encoding="utf-8" ?>' . "\n" .
|
||||
"<result>\n" .
|
||||
"\t<error message=\"" . $exceptionMessage . "\" />\n" .
|
||||
"</result>";
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given data table to an array
|
||||
*
|
||||
* @param DataTable|DataTable/Map $table data table to convert
|
||||
* @return array
|
||||
*/
|
||||
protected function getArrayFromDataTable($table)
|
||||
{
|
||||
if (is_array($table)) {
|
||||
return $table;
|
||||
}
|
||||
|
||||
$renderer = new Php();
|
||||
$renderer->setRenderSubTables($this->isRenderSubtables());
|
||||
$renderer->setSerialize(false);
|
||||
$renderer->setTable($table);
|
||||
$renderer->setHideIdSubDatableFromResponse($this->hideIdSubDatatable);
|
||||
return $renderer->flatRender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output for the given data table
|
||||
*
|
||||
* @param DataTable|DataTable/Map $table
|
||||
* @param bool $returnOnlyDataTableXml
|
||||
* @param string $prefixLines
|
||||
* @return array|string
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function renderTable($table, $returnOnlyDataTableXml = false, $prefixLines = '')
|
||||
{
|
||||
$array = $this->getArrayFromDataTable($table);
|
||||
if ($table instanceof Map) {
|
||||
$out = $this->renderDataTableMap($table, $array, $prefixLines);
|
||||
|
||||
if ($returnOnlyDataTableXml) {
|
||||
return $out;
|
||||
}
|
||||
$out = "<results>\n$out</results>";
|
||||
return $out;
|
||||
}
|
||||
|
||||
// integer value of ZERO is a value we want to display
|
||||
if ($array != 0 && empty($array)) {
|
||||
if ($returnOnlyDataTableXml) {
|
||||
throw new Exception("Illegal state, what xml shall we return?");
|
||||
}
|
||||
$out = "<result />";
|
||||
return $out;
|
||||
}
|
||||
if ($table instanceof Simple) {
|
||||
if (is_array($array)) {
|
||||
$out = $this->renderDataTableSimple($array);
|
||||
} else {
|
||||
$out = $array;
|
||||
}
|
||||
if ($returnOnlyDataTableXml) {
|
||||
return $out;
|
||||
}
|
||||
|
||||
if (is_array($array)) {
|
||||
$out = "<result>\n" . $out . "</result>";
|
||||
} else {
|
||||
$value = self::formatValueXml($out);
|
||||
if ($value === '') {
|
||||
$out = "<result />";
|
||||
} else {
|
||||
$out = "<result>" . $value . "</result>";
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
if ($table instanceof DataTable) {
|
||||
$out = $this->renderDataTable($array);
|
||||
if ($returnOnlyDataTableXml) {
|
||||
return $out;
|
||||
}
|
||||
$out = "<result>\n$out</result>";
|
||||
return $out;
|
||||
}
|
||||
|
||||
if (is_array($array)) {
|
||||
$out = $this->renderArray($array, $prefixLines . "\t");
|
||||
if ($returnOnlyDataTableXml) {
|
||||
return $out;
|
||||
}
|
||||
return "<result>\n$out</result>";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an array as XML.
|
||||
*
|
||||
* @param array $array The array to render.
|
||||
* @param string $prefixLines The string to prefix each line in the output.
|
||||
* @return string
|
||||
*/
|
||||
private function renderArray($array, $prefixLines)
|
||||
{
|
||||
$isAssociativeArray = Piwik::isAssociativeArray($array);
|
||||
|
||||
// check if array contains arrays, and if not wrap the result in an extra <row> element
|
||||
// (only check if this is the root renderArray call)
|
||||
// NOTE: this is for backwards compatibility. before, array's were added to a new DataTable.
|
||||
// if the array had arrays, they were added as multiple rows, otherwise it was treated as
|
||||
// one row. removing will change API output.
|
||||
$wrapInRow = $prefixLines === "\t"
|
||||
&& self::shouldWrapArrayBeforeRendering($array, $wrapSingleValues = false, $isAssociativeArray);
|
||||
|
||||
// render the array
|
||||
$result = "";
|
||||
if ($wrapInRow) {
|
||||
$result .= "$prefixLines<row>\n";
|
||||
$prefixLines .= "\t";
|
||||
}
|
||||
foreach ($array as $key => $value) {
|
||||
// based on the type of array & the key, determine how this node will look
|
||||
if ($isAssociativeArray) {
|
||||
$keyIsInvalidXmlElement = is_numeric($key) || is_numeric($key[0]);
|
||||
if ($keyIsInvalidXmlElement) {
|
||||
$prefix = "<row key=\"$key\">";
|
||||
$suffix = "</row>";
|
||||
$emptyNode = "<row key=\"$key\"/>";
|
||||
} else if (strpos($key, '=') !== false) {
|
||||
list($keyAttributeName, $key) = explode('=', $key, 2);
|
||||
|
||||
$prefix = "<row $keyAttributeName=\"$key\">";
|
||||
$suffix = "</row>";
|
||||
$emptyNode = "<row $keyAttributeName=\"$key\">";
|
||||
} else {
|
||||
$prefix = "<$key>";
|
||||
$suffix = "</$key>";
|
||||
$emptyNode = "<$key />";
|
||||
}
|
||||
} else {
|
||||
$prefix = "<row>";
|
||||
$suffix = "</row>";
|
||||
$emptyNode = "<row/>";
|
||||
}
|
||||
|
||||
// render the array item
|
||||
if (is_array($value)) {
|
||||
$result .= $prefixLines . $prefix . "\n";
|
||||
$result .= $this->renderArray($value, $prefixLines . "\t");
|
||||
$result .= $prefixLines . $suffix . "\n";
|
||||
} else if ($value instanceof DataTable
|
||||
|| $value instanceof Map
|
||||
) {
|
||||
if ($value->getRowsCount() == 0) {
|
||||
$result .= $prefixLines . $emptyNode . "\n";
|
||||
} else {
|
||||
$result .= $prefixLines . $prefix . "\n";
|
||||
if ($value instanceof Map) {
|
||||
$result .= $this->renderDataTableMap($value, $this->getArrayFromDataTable($value), $prefixLines);
|
||||
} else if ($value instanceof Simple) {
|
||||
$result .= $this->renderDataTableSimple($this->getArrayFromDataTable($value), $prefixLines);
|
||||
} else {
|
||||
$result .= $this->renderDataTable($this->getArrayFromDataTable($value), $prefixLines);
|
||||
}
|
||||
$result .= $prefixLines . $suffix . "\n";
|
||||
}
|
||||
} else {
|
||||
$xmlValue = self::formatValueXml($value);
|
||||
if (strlen($xmlValue) != 0) {
|
||||
$result .= $prefixLines . $prefix . $xmlValue . $suffix . "\n";
|
||||
} else {
|
||||
$result .= $prefixLines . $emptyNode . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($wrapInRow) {
|
||||
$result .= substr($prefixLines, 0, strlen($prefixLines) - 1) . "</row>\n";
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output for the given data table array
|
||||
*
|
||||
* @param Map $table
|
||||
* @param array $array
|
||||
* @param string $prefixLines
|
||||
* @return string
|
||||
*/
|
||||
protected function renderDataTableMap($table, $array, $prefixLines = "")
|
||||
{
|
||||
// CASE 1
|
||||
//array
|
||||
// 'day1' => string '14' (length=2)
|
||||
// 'day2' => string '6' (length=1)
|
||||
$firstTable = current($array);
|
||||
if (!is_array($firstTable)) {
|
||||
$xml = '';
|
||||
$nameDescriptionAttribute = $table->getKeyName();
|
||||
foreach ($array as $valueAttribute => $value) {
|
||||
if (empty($value)) {
|
||||
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\" />\n";
|
||||
} elseif ($value instanceof Map) {
|
||||
$out = $this->renderTable($value, true);
|
||||
//TODO somehow this code is not tested, cover this case
|
||||
$xml .= "\t<result $nameDescriptionAttribute=\"$valueAttribute\">\n$out</result>\n";
|
||||
} else {
|
||||
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">" . self::formatValueXml($value) . "</result>\n";
|
||||
}
|
||||
}
|
||||
return $xml;
|
||||
}
|
||||
|
||||
$subTables = $table->getDataTables();
|
||||
$firstTable = current($subTables);
|
||||
|
||||
// CASE 2
|
||||
//array
|
||||
// 'day1' =>
|
||||
// array
|
||||
// 'nb_uniq_visitors' => string '18'
|
||||
// 'nb_visits' => string '101'
|
||||
// 'day2' =>
|
||||
// array
|
||||
// 'nb_uniq_visitors' => string '28'
|
||||
// 'nb_visits' => string '11'
|
||||
if ($firstTable instanceof Simple) {
|
||||
$xml = '';
|
||||
$nameDescriptionAttribute = $table->getKeyName();
|
||||
foreach ($array as $valueAttribute => $dataTableSimple) {
|
||||
if (count($dataTableSimple) == 0) {
|
||||
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\" />\n";
|
||||
} else {
|
||||
if (is_array($dataTableSimple)) {
|
||||
$dataTableSimple = "\n" . $this->renderDataTableSimple($dataTableSimple, $prefixLines . "\t") . $prefixLines . "\t";
|
||||
}
|
||||
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">" . $dataTableSimple . "</result>\n";
|
||||
}
|
||||
}
|
||||
return $xml;
|
||||
}
|
||||
|
||||
// CASE 3
|
||||
//array
|
||||
// 'day1' =>
|
||||
// array
|
||||
// 0 =>
|
||||
// array
|
||||
// 'label' => string 'phpmyvisites'
|
||||
// 'nb_uniq_visitors' => int 11
|
||||
// 'nb_visits' => int 13
|
||||
// 1 =>
|
||||
// array
|
||||
// 'label' => string 'phpmyvisits'
|
||||
// 'nb_uniq_visitors' => int 2
|
||||
// 'nb_visits' => int 2
|
||||
// 'day2' =>
|
||||
// array
|
||||
// 0 =>
|
||||
// array
|
||||
// 'label' => string 'piwik'
|
||||
// 'nb_uniq_visitors' => int 121
|
||||
// 'nb_visits' => int 130
|
||||
// 1 =>
|
||||
// array
|
||||
// 'label' => string 'piwik bis'
|
||||
// 'nb_uniq_visitors' => int 20
|
||||
// 'nb_visits' => int 120
|
||||
if ($firstTable instanceof DataTable) {
|
||||
$xml = '';
|
||||
$nameDescriptionAttribute = $table->getKeyName();
|
||||
foreach ($array as $keyName => $arrayForSingleDate) {
|
||||
$dataTableOut = $this->renderDataTable($arrayForSingleDate, $prefixLines . "\t");
|
||||
if (empty($dataTableOut)) {
|
||||
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$keyName\" />\n";
|
||||
} else {
|
||||
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$keyName\">\n";
|
||||
$xml .= $dataTableOut;
|
||||
$xml .= $prefixLines . "\t</result>\n";
|
||||
}
|
||||
}
|
||||
return $xml;
|
||||
}
|
||||
|
||||
if ($firstTable instanceof Map) {
|
||||
$xml = '';
|
||||
$tables = $table->getDataTables();
|
||||
$nameDescriptionAttribute = $table->getKeyName();
|
||||
foreach ($tables as $valueAttribute => $tableInArray) {
|
||||
$out = $this->renderTable($tableInArray, true, $prefixLines . "\t");
|
||||
$xml .= $prefixLines . "\t<result $nameDescriptionAttribute=\"$valueAttribute\">\n" . $out . $prefixLines . "\t</result>\n";
|
||||
}
|
||||
return $xml;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output for the given data array
|
||||
*
|
||||
* @param array $array
|
||||
* @param string $prefixLine
|
||||
* @return string
|
||||
*/
|
||||
protected function renderDataTable($array, $prefixLine = "")
|
||||
{
|
||||
$out = '';
|
||||
foreach ($array as $rowId => $row) {
|
||||
if (!is_array($row)) {
|
||||
$value = self::formatValueXml($row);
|
||||
if (strlen($value) == 0) {
|
||||
$out .= $prefixLine . "\t\t<$rowId />\n";
|
||||
} else {
|
||||
$out .= $prefixLine . "\t\t<$rowId>" . $value . "</$rowId>\n";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Handing case idgoal=7, creating a new array for that one
|
||||
$rowAttribute = '';
|
||||
if (($equalFound = strstr($rowId, '=')) !== false) {
|
||||
$rowAttribute = explode('=', $rowId);
|
||||
$rowAttribute = " " . $rowAttribute[0] . "='" . $rowAttribute[1] . "'";
|
||||
}
|
||||
$out .= $prefixLine . "\t<row$rowAttribute>";
|
||||
|
||||
if (count($row) === 1
|
||||
&& key($row) === 0
|
||||
) {
|
||||
$value = self::formatValueXml(current($row));
|
||||
$out .= $prefixLine . $value;
|
||||
} else {
|
||||
$out .= "\n";
|
||||
foreach ($row as $name => $value) {
|
||||
// handle the recursive dataTable case by XML outputting the recursive table
|
||||
if (is_array($value)) {
|
||||
$value = "\n" . $this->renderDataTable($value, $prefixLine . "\t\t");
|
||||
$value .= $prefixLine . "\t\t";
|
||||
} else {
|
||||
$value = self::formatValueXml($value);
|
||||
}
|
||||
if (strlen($value) == 0) {
|
||||
$out .= $prefixLine . "\t\t<$name />\n";
|
||||
} else {
|
||||
$out .= $prefixLine . "\t\t<$name>" . $value . "</$name>\n";
|
||||
}
|
||||
}
|
||||
$out .= "\t";
|
||||
}
|
||||
$out .= $prefixLine . "</row>\n";
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the output for the given data array (representing a simple data table)
|
||||
*
|
||||
* @param $array
|
||||
* @param string $prefixLine
|
||||
* @return string
|
||||
*/
|
||||
protected function renderDataTableSimple($array, $prefixLine = "")
|
||||
{
|
||||
if (!is_array($array)) {
|
||||
$array = array('value' => $array);
|
||||
}
|
||||
|
||||
$out = '';
|
||||
foreach ($array as $keyName => $value) {
|
||||
$xmlValue = self::formatValueXml($value);
|
||||
if (strlen($xmlValue) == 0) {
|
||||
$out .= $prefixLine . "\t<$keyName />\n";
|
||||
} else {
|
||||
$out .= $prefixLine . "\t<$keyName>" . $xmlValue . "</$keyName>\n";
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the XML headers
|
||||
*/
|
||||
protected function renderHeader()
|
||||
{
|
||||
// silent fail because otherwise it throws an exception in the unit tests
|
||||
@header('Content-Type: text/xml; charset=utf-8');
|
||||
}
|
||||
}
|
||||
665
www/analytics/core/DataTable/Row.php
Normal file
665
www/analytics/core/DataTable/Row.php
Normal file
|
|
@ -0,0 +1,665 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable;
|
||||
|
||||
use Exception;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\Metrics;
|
||||
|
||||
/**
|
||||
* This is what a {@link Piwik\DataTable} is composed of.
|
||||
*
|
||||
* DataTable rows contain columns, metadata and a subtable ID. Columns and metadata
|
||||
* are stored as an array of name => value mappings.
|
||||
*
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class Row
|
||||
{
|
||||
/**
|
||||
* List of columns that cannot be summed. An associative array for speed.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $unsummableColumns = array(
|
||||
'label' => true,
|
||||
'full_url' => true // column used w/ old Piwik versions
|
||||
);
|
||||
|
||||
/**
|
||||
* This array contains the row information:
|
||||
* - array indexed by self::COLUMNS contains the columns, pairs of (column names, value)
|
||||
* - (optional) array indexed by self::METADATA contains the metadata, pairs of (metadata name, value)
|
||||
* - (optional) integer indexed by self::DATATABLE_ASSOCIATED contains the ID of the DataTable associated to this row.
|
||||
* This ID can be used to read the DataTable from the DataTable_Manager.
|
||||
*
|
||||
* @var array
|
||||
* @see constructor for more information
|
||||
* @ignore
|
||||
*/
|
||||
public $c = array();
|
||||
|
||||
private $subtableIdWasNegativeBeforeSerialize = false;
|
||||
|
||||
// @see sumRow - implementation detail
|
||||
public $maxVisitsSummed = 0;
|
||||
|
||||
const COLUMNS = 0;
|
||||
const METADATA = 1;
|
||||
const DATATABLE_ASSOCIATED = 3;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param array $row An array with the following structure:
|
||||
*
|
||||
* array(
|
||||
* Row::COLUMNS => array('label' => 'Piwik',
|
||||
* 'column1' => 42,
|
||||
* 'visits' => 657,
|
||||
* 'time_spent' => 155744),
|
||||
* Row::METADATA => array('logo' => 'test.png'),
|
||||
* Row::DATATABLE_ASSOCIATED => $subtable // DataTable object
|
||||
* // (but in the row only the ID will be stored)
|
||||
* )
|
||||
*/
|
||||
public function __construct($row = array())
|
||||
{
|
||||
$this->c[self::COLUMNS] = array();
|
||||
$this->c[self::METADATA] = array();
|
||||
$this->c[self::DATATABLE_ASSOCIATED] = null;
|
||||
|
||||
if (isset($row[self::COLUMNS])) {
|
||||
$this->c[self::COLUMNS] = $row[self::COLUMNS];
|
||||
}
|
||||
if (isset($row[self::METADATA])) {
|
||||
$this->c[self::METADATA] = $row[self::METADATA];
|
||||
}
|
||||
if (isset($row[self::DATATABLE_ASSOCIATED])
|
||||
&& $row[self::DATATABLE_ASSOCIATED] instanceof DataTable
|
||||
) {
|
||||
$this->setSubtable($row[self::DATATABLE_ASSOCIATED]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Because $this->c[self::DATATABLE_ASSOCIATED] is negative when the table is in memory,
|
||||
* we must prior to serialize() call, make sure the ID is saved as positive integer
|
||||
*
|
||||
* Only serialize the "c" member
|
||||
* @ignore
|
||||
*/
|
||||
public function __sleep()
|
||||
{
|
||||
if (!empty($this->c[self::DATATABLE_ASSOCIATED])
|
||||
&& $this->c[self::DATATABLE_ASSOCIATED] < 0
|
||||
) {
|
||||
$this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED];
|
||||
$this->subtableIdWasNegativeBeforeSerialize = true;
|
||||
}
|
||||
return array('c');
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called after the row was serialized and __sleep was called.
|
||||
* @ignore
|
||||
*/
|
||||
public function cleanPostSerialize()
|
||||
{
|
||||
if ($this->subtableIdWasNegativeBeforeSerialize) {
|
||||
$this->c[self::DATATABLE_ASSOCIATED] = -1 * $this->c[self::DATATABLE_ASSOCIATED];
|
||||
$this->subtableIdWasNegativeBeforeSerialize = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When destroyed, a row destroys its associated subtable if there is one.
|
||||
* @ignore
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->isSubtableLoaded()) {
|
||||
Manager::getInstance()->deleteTable($this->getIdSubDataTable());
|
||||
$this->c[self::DATATABLE_ASSOCIATED] = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a basic rendering to the Row and returns the output.
|
||||
*
|
||||
* @return string describing the row. Example:
|
||||
* "- 1 ['label' => 'piwik', 'nb_uniq_visitors' => 1685, 'nb_visits' => 1861] [] [idsubtable = 1375]"
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$columns = array();
|
||||
foreach ($this->getColumns() as $column => $value) {
|
||||
if (is_string($value)) $value = "'$value'";
|
||||
elseif (is_array($value)) $value = var_export($value, true);
|
||||
$columns[] = "'$column' => $value";
|
||||
}
|
||||
$columns = implode(", ", $columns);
|
||||
$metadata = array();
|
||||
foreach ($this->getMetadata() as $name => $value) {
|
||||
if (is_string($value)) $value = "'$value'";
|
||||
elseif (is_array($value)) $value = var_export($value, true);
|
||||
$metadata[] = "'$name' => $value";
|
||||
}
|
||||
$metadata = implode(", ", $metadata);
|
||||
$output = "# [" . $columns . "] [" . $metadata . "] [idsubtable = " . $this->getIdSubDataTable() . "]<br />\n";
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given column.
|
||||
*
|
||||
* @param string $name The column name.
|
||||
* @return bool `true` on success, `false` if the column does not exist.
|
||||
*/
|
||||
public function deleteColumn($name)
|
||||
{
|
||||
if (!array_key_exists($name, $this->c[self::COLUMNS])) {
|
||||
return false;
|
||||
}
|
||||
unset($this->c[self::COLUMNS][$name]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames a column.
|
||||
*
|
||||
* @param string $oldName The current name of the column.
|
||||
* @param string $newName The new name of the column.
|
||||
*/
|
||||
public function renameColumn($oldName, $newName)
|
||||
{
|
||||
if (isset($this->c[self::COLUMNS][$oldName])) {
|
||||
$this->c[self::COLUMNS][$newName] = $this->c[self::COLUMNS][$oldName];
|
||||
}
|
||||
// outside the if() since we want to delete nulled columns
|
||||
unset($this->c[self::COLUMNS][$oldName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a column by name.
|
||||
*
|
||||
* @param string $name The column name.
|
||||
* @return mixed|false The column value or false if it doesn't exist.
|
||||
*/
|
||||
public function getColumn($name)
|
||||
{
|
||||
if (!isset($this->c[self::COLUMNS][$name])) {
|
||||
return false;
|
||||
}
|
||||
return $this->c[self::COLUMNS][$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of all metadata, or one requested metadata value.
|
||||
*
|
||||
* @param string|null $name The name of the metadata to return or null to return all metadata.
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMetadata($name = null)
|
||||
{
|
||||
if (is_null($name)) {
|
||||
return $this->c[self::METADATA];
|
||||
}
|
||||
if (!isset($this->c[self::METADATA][$name])) {
|
||||
return false;
|
||||
}
|
||||
return $this->c[self::METADATA][$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array containing all the columns.
|
||||
*
|
||||
* @return array Example:
|
||||
*
|
||||
* array(
|
||||
* 'column1' => VALUE,
|
||||
* 'label' => 'www.php.net'
|
||||
* 'nb_visits' => 15894,
|
||||
* )
|
||||
*/
|
||||
public function getColumns()
|
||||
{
|
||||
return $this->c[self::COLUMNS];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the subDataTable.
|
||||
* If there is no such a table, returns null.
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getIdSubDataTable()
|
||||
{
|
||||
return !is_null($this->c[self::DATATABLE_ASSOCIATED])
|
||||
// abs() is to ensure we return a positive int, @see isSubtableLoaded()
|
||||
? abs($this->c[self::DATATABLE_ASSOCIATED])
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the associated subtable, if one exists. Returns `false` if none exists.
|
||||
*
|
||||
* @return DataTable|bool
|
||||
*/
|
||||
public function getSubtable()
|
||||
{
|
||||
if ($this->isSubtableLoaded()) {
|
||||
return Manager::getInstance()->getTable($this->getIdSubDataTable());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums a DataTable to this row's subtable. If this row has no subtable a new
|
||||
* one is created.
|
||||
*
|
||||
* See {@link Piwik\DataTable::addDataTable()} to learn how DataTables are summed.
|
||||
*
|
||||
* @param DataTable $subTable Table to sum to this row's subtable.
|
||||
*/
|
||||
public function sumSubtable(DataTable $subTable)
|
||||
{
|
||||
if ($this->isSubtableLoaded()) {
|
||||
$thisSubTable = $this->getSubtable();
|
||||
} else {
|
||||
$thisSubTable = new DataTable();
|
||||
$this->addSubtable($thisSubTable);
|
||||
}
|
||||
$columnOps = $subTable->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME);
|
||||
$thisSubTable->setMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME, $columnOps);
|
||||
$thisSubTable->addDataTable($subTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a subtable to this row.
|
||||
*
|
||||
* @param DataTable $subTable DataTable to associate to this row.
|
||||
* @return DataTable Returns `$subTable`.
|
||||
* @throws Exception if a subtable already exists for this row.
|
||||
*/
|
||||
public function addSubtable(DataTable $subTable)
|
||||
{
|
||||
if (!is_null($this->c[self::DATATABLE_ASSOCIATED])) {
|
||||
throw new Exception("Adding a subtable to the row, but it already has a subtable associated.");
|
||||
}
|
||||
return $this->setSubtable($subTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a subtable to this row, overwriting the existing subtable,
|
||||
* if any.
|
||||
*
|
||||
* @param DataTable $subTable DataTable to associate to this row.
|
||||
* @return DataTable Returns `$subTable`.
|
||||
*/
|
||||
public function setSubtable(DataTable $subTable)
|
||||
{
|
||||
// Hacking -1 to ensure value is negative, so we know the table was loaded
|
||||
// @see isSubtableLoaded()
|
||||
$this->c[self::DATATABLE_ASSOCIATED] = -1 * $subTable->getId();
|
||||
return $subTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the subtable is currently loaded in memory via {@link Piwik\DataTable\Manager}.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSubtableLoaded()
|
||||
{
|
||||
// self::DATATABLE_ASSOCIATED are set as negative values,
|
||||
// as a flag to signify that the subtable is loaded in memory
|
||||
return !is_null($this->c[self::DATATABLE_ASSOCIATED])
|
||||
&& $this->c[self::DATATABLE_ASSOCIATED] < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the subtable reference.
|
||||
*/
|
||||
public function removeSubtable()
|
||||
{
|
||||
$this->c[self::DATATABLE_ASSOCIATED] = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all the columns at once. Overwrites **all** previously set columns.
|
||||
*
|
||||
* @param array eg, `array('label' => 'www.php.net', 'nb_visits' => 15894)`
|
||||
*/
|
||||
public function setColumns($columns)
|
||||
{
|
||||
$this->c[self::COLUMNS] = $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value `$value` to the column called `$name`.
|
||||
*
|
||||
* @param string $name name of the column to set.
|
||||
* @param mixed $value value of the column to set.
|
||||
*/
|
||||
public function setColumn($name, $value)
|
||||
{
|
||||
$this->c[self::COLUMNS][$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value `$value` to the metadata called `$name`.
|
||||
*
|
||||
* @param string $name name of the metadata to set.
|
||||
* @param mixed $value value of the metadata to set.
|
||||
*/
|
||||
public function setMetadata($name, $value)
|
||||
{
|
||||
$this->c[self::METADATA][$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes one metadata value or all metadata values.
|
||||
*
|
||||
* @param bool|string $name Metadata name (omit to delete entire metadata).
|
||||
* @return bool `true` on success, `false` if the column didn't exist
|
||||
*/
|
||||
public function deleteMetadata($name = false)
|
||||
{
|
||||
if ($name === false) {
|
||||
$this->c[self::METADATA] = array();
|
||||
return true;
|
||||
}
|
||||
if (!isset($this->c[self::METADATA][$name])) {
|
||||
return false;
|
||||
}
|
||||
unset($this->c[self::METADATA][$name]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new column to the row. If the column already exists, throws an exception.
|
||||
*
|
||||
* @param string $name name of the column to add.
|
||||
* @param mixed $value value of the column to set.
|
||||
* @throws Exception if the column already exists.
|
||||
*/
|
||||
public function addColumn($name, $value)
|
||||
{
|
||||
if (isset($this->c[self::COLUMNS][$name])) {
|
||||
throw new Exception("Column $name already in the array!");
|
||||
}
|
||||
$this->c[self::COLUMNS][$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add many columns to this row.
|
||||
*
|
||||
* @param array $columns Name/Value pairs, e.g., `array('name' => $value , ...)`
|
||||
* @throws Exception if any column name does not exist.
|
||||
* @return void
|
||||
*/
|
||||
public function addColumns($columns)
|
||||
{
|
||||
foreach ($columns as $name => $value) {
|
||||
try {
|
||||
$this->addColumn($name, $value);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($e)) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new metadata to the row. If the metadata already exists, throws an exception.
|
||||
*
|
||||
* @param string $name name of the metadata to add.
|
||||
* @param mixed $value value of the metadata to set.
|
||||
* @throws Exception if the metadata already exists.
|
||||
*/
|
||||
public function addMetadata($name, $value)
|
||||
{
|
||||
if (isset($this->c[self::METADATA][$name])) {
|
||||
throw new Exception("Metadata $name already in the array!");
|
||||
}
|
||||
$this->c[self::METADATA][$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums the given `$rowToSum` columns values to the existing row column values.
|
||||
* Only the int or float values will be summed. Label columns will be ignored
|
||||
* even if they have a numeric value.
|
||||
*
|
||||
* Columns in `$rowToSum` that don't exist in `$this` are added to `$this`.
|
||||
*
|
||||
* @param \Piwik\DataTable\Row $rowToSum The row to sum to this row.
|
||||
* @param bool $enableCopyMetadata Whether metadata should be copied or not.
|
||||
* @param array $aggregationOperations for columns that should not be summed, determine which
|
||||
* aggregation should be used (min, max). format:
|
||||
* `array('column name' => 'function name')`
|
||||
*/
|
||||
public function sumRow(Row $rowToSum, $enableCopyMetadata = true, $aggregationOperations = false)
|
||||
{
|
||||
foreach ($rowToSum->getColumns() as $columnToSumName => $columnToSumValue) {
|
||||
if (!isset(self::$unsummableColumns[$columnToSumName])) // make sure we can add this column
|
||||
{
|
||||
$thisColumnValue = $this->getColumn($columnToSumName);
|
||||
|
||||
$operation = (is_array($aggregationOperations) && isset($aggregationOperations[$columnToSumName]) ?
|
||||
strtolower($aggregationOperations[$columnToSumName]) : 'sum');
|
||||
|
||||
// max_actions is a core metric that is generated in ArchiveProcess_Day. Therefore, it can be
|
||||
// present in any data table and is not part of the $aggregationOperations mechanism.
|
||||
if ($columnToSumName == Metrics::INDEX_MAX_ACTIONS) {
|
||||
$operation = 'max';
|
||||
}
|
||||
if(empty($operation)) {
|
||||
throw new Exception("Unknown aggregation operation for column $columnToSumName.");
|
||||
}
|
||||
$newValue = $this->getColumnValuesMerged($operation, $thisColumnValue, $columnToSumValue);
|
||||
|
||||
$this->setColumn($columnToSumName, $newValue);
|
||||
}
|
||||
}
|
||||
|
||||
if ($enableCopyMetadata) {
|
||||
$this->sumRowMetadata($rowToSum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
private function getColumnValuesMerged($operation, $thisColumnValue, $columnToSumValue)
|
||||
{
|
||||
switch ($operation) {
|
||||
case 'skip':
|
||||
$newValue = null;
|
||||
break;
|
||||
case 'max':
|
||||
$newValue = max($thisColumnValue, $columnToSumValue);
|
||||
break;
|
||||
case 'min':
|
||||
if (!$thisColumnValue) {
|
||||
$newValue = $columnToSumValue;
|
||||
} else if (!$columnToSumValue) {
|
||||
$newValue = $thisColumnValue;
|
||||
} else {
|
||||
$newValue = min($thisColumnValue, $columnToSumValue);
|
||||
}
|
||||
break;
|
||||
case 'sum':
|
||||
$newValue = $this->sumRowArray($thisColumnValue, $columnToSumValue);
|
||||
break;
|
||||
default:
|
||||
throw new Exception("Unknown operation '$operation'.");
|
||||
}
|
||||
return $newValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums the metadata in `$rowToSum` with the metadata in `$this` row.
|
||||
*
|
||||
* @param Row $rowToSum
|
||||
*/
|
||||
public function sumRowMetadata($rowToSum)
|
||||
{
|
||||
if (!empty($rowToSum->c[self::METADATA])
|
||||
&& !$this->isSummaryRow()
|
||||
) {
|
||||
// We shall update metadata, and keep the metadata with the _most visits or pageviews_, rather than first or last seen
|
||||
$visits = max($rowToSum->getColumn(Metrics::INDEX_PAGE_NB_HITS) || $rowToSum->getColumn(Metrics::INDEX_NB_VISITS),
|
||||
// Old format pre-1.2, @see also method doSumVisitsMetrics()
|
||||
$rowToSum->getColumn('nb_actions') || $rowToSum->getColumn('nb_visits'));
|
||||
if (($visits && $visits > $this->maxVisitsSummed)
|
||||
|| empty($this->c[self::METADATA])
|
||||
) {
|
||||
$this->maxVisitsSummed = $visits;
|
||||
$this->c[self::METADATA] = $rowToSum->c[self::METADATA];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if this row is the summary row, `false` if otherwise. This function
|
||||
* depends on the label of the row, and so, is not 100% accurate.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSummaryRow()
|
||||
{
|
||||
return $this->getColumn('label') === DataTable::LABEL_SUMMARY_ROW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function: sums 2 values
|
||||
*
|
||||
* @param number|bool $thisColumnValue
|
||||
* @param number|array $columnToSumValue
|
||||
*
|
||||
* @throws Exception
|
||||
* @return array|int
|
||||
*/
|
||||
protected function sumRowArray($thisColumnValue, $columnToSumValue)
|
||||
{
|
||||
if (is_numeric($columnToSumValue)) {
|
||||
if ($thisColumnValue === false) {
|
||||
$thisColumnValue = 0;
|
||||
}
|
||||
return $thisColumnValue + $columnToSumValue;
|
||||
}
|
||||
|
||||
if (is_array($columnToSumValue)) {
|
||||
if ($thisColumnValue == false) {
|
||||
return $columnToSumValue;
|
||||
}
|
||||
$newValue = $thisColumnValue;
|
||||
foreach ($columnToSumValue as $arrayIndex => $arrayValue) {
|
||||
if (!isset($newValue[$arrayIndex])) {
|
||||
$newValue[$arrayIndex] = false;
|
||||
}
|
||||
$newValue[$arrayIndex] = $this->sumRowArray($newValue[$arrayIndex], $arrayValue);
|
||||
}
|
||||
return $newValue;
|
||||
}
|
||||
|
||||
if (is_string($columnToSumValue)) {
|
||||
if ($thisColumnValue === false) {
|
||||
return $columnToSumValue;
|
||||
} else if ($columnToSumValue === false) {
|
||||
return $thisColumnValue;
|
||||
} else {
|
||||
throw new Exception("Trying to add two strings values in DataTable\Row::sumRowArray: "
|
||||
. "'$thisColumnValue' + '$columnToSumValue'");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to compare array elements
|
||||
*
|
||||
* @param mixed $elem1
|
||||
* @param mixed $elem2
|
||||
* @return bool
|
||||
* @ignore
|
||||
*/
|
||||
static public function compareElements($elem1, $elem2)
|
||||
{
|
||||
if (is_array($elem1)) {
|
||||
if (is_array($elem2)) {
|
||||
return strcmp(serialize($elem1), serialize($elem2));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (is_array($elem2))
|
||||
return -1;
|
||||
|
||||
if ((string)$elem1 === (string)$elem2)
|
||||
return 0;
|
||||
|
||||
return ((string)$elem1 > (string)$elem2) ? 1 : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that tests if two rows are equal.
|
||||
*
|
||||
* Two rows are equal if:
|
||||
*
|
||||
* - they have exactly the same columns / metadata
|
||||
* - they have a subDataTable associated, then we check that both of them are the same.
|
||||
*
|
||||
* Column order is not important.
|
||||
*
|
||||
* @param \Piwik\DataTable\Row $row1 first to compare
|
||||
* @param \Piwik\DataTable\Row $row2 second to compare
|
||||
* @return bool
|
||||
*/
|
||||
static public function isEqual(Row $row1, Row $row2)
|
||||
{
|
||||
//same columns
|
||||
$cols1 = $row1->getColumns();
|
||||
$cols2 = $row2->getColumns();
|
||||
|
||||
$diff1 = array_udiff($cols1, $cols2, array(__CLASS__, 'compareElements'));
|
||||
$diff2 = array_udiff($cols2, $cols1, array(__CLASS__, 'compareElements'));
|
||||
|
||||
if ($diff1 != $diff2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$dets1 = $row1->getMetadata();
|
||||
$dets2 = $row2->getMetadata();
|
||||
|
||||
ksort($dets1);
|
||||
ksort($dets2);
|
||||
|
||||
if ($dets1 != $dets2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// either both are null
|
||||
// or both have a value
|
||||
if (!(is_null($row1->getIdSubDataTable())
|
||||
&& is_null($row2->getIdSubDataTable())
|
||||
)
|
||||
) {
|
||||
$subtable1 = $row1->getSubtable();
|
||||
$subtable2 = $row2->getSubtable();
|
||||
if (!DataTable::isEqual($subtable1, $subtable2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
68
www/analytics/core/DataTable/Row/DataTableSummaryRow.php
Normal file
68
www/analytics/core/DataTable/Row/DataTableSummaryRow.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable\Row;
|
||||
|
||||
use Piwik\DataTable\Manager;
|
||||
use Piwik\DataTable;
|
||||
use Piwik\DataTable\Row;
|
||||
|
||||
/**
|
||||
* A special row whose column values are the aggregate of the row's subtable.
|
||||
*
|
||||
* This class creates sets its own columns to the sum of each row in the row's subtable.
|
||||
*
|
||||
* Non-numeric columns are bypassed during summation and do not appear in this
|
||||
* rows columns.
|
||||
*
|
||||
* See {@link Piwik\DataTable\Row::sumRow()} for more information on the algorithm.
|
||||
*
|
||||
*/
|
||||
class DataTableSummaryRow extends Row
|
||||
{
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param DataTable|null $subTable The subtable of this row. This parameter is mostly for
|
||||
* convenience. If set, its rows will be summed to this one,
|
||||
* but it will not be set as this row's subtable (so
|
||||
* getSubtable() will return false).
|
||||
*/
|
||||
public function __construct($subTable = null)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
if ($subTable !== null) {
|
||||
$this->sumTable($subTable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this row to an empty one and sums the associated subtable again.
|
||||
*/
|
||||
public function recalculate()
|
||||
{
|
||||
$id = $this->getIdSubDataTable();
|
||||
if ($id !== null) {
|
||||
$subTable = Manager::getInstance()->getTable($id);
|
||||
$this->sumTable($subTable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sums a tables row with this one.
|
||||
*
|
||||
* @param DataTable $table
|
||||
*/
|
||||
private function sumTable($table)
|
||||
{
|
||||
foreach ($table->getRows() as $row) {
|
||||
$this->sumRow($row, $enableCopyMetadata = false, $table->getMetadata(DataTable::COLUMN_AGGREGATION_OPS_METADATA_NAME));
|
||||
}
|
||||
}
|
||||
}
|
||||
38
www/analytics/core/DataTable/Simple.php
Normal file
38
www/analytics/core/DataTable/Simple.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable;
|
||||
|
||||
use Piwik\DataTable;
|
||||
|
||||
/**
|
||||
* A {@link Piwik\DataTable} where every row has two columns: **label** and **value**.
|
||||
*
|
||||
* Simple DataTables are only used to slightly alter the output of some renderers
|
||||
* (notably the XML renderer).
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class Simple extends DataTable
|
||||
{
|
||||
/**
|
||||
* Adds rows based on an array mapping label column values to value column
|
||||
* values.
|
||||
*
|
||||
* @param array $array Array containing the rows, eg,
|
||||
*
|
||||
* array(
|
||||
* 'Label row 1' => $value1,
|
||||
* 'Label row 2' => $value2,
|
||||
* )
|
||||
*/
|
||||
public function addRowsFromArray($array)
|
||||
{
|
||||
$this->addRowsFromSimpleArray(array($array));
|
||||
}
|
||||
}
|
||||
15
www/analytics/core/DataTable/TableNotFoundException.php
Normal file
15
www/analytics/core/DataTable/TableNotFoundException.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
/**
|
||||
* Piwik - Open source web analytics
|
||||
*
|
||||
* @link http://piwik.org
|
||||
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
|
||||
*
|
||||
*/
|
||||
namespace Piwik\DataTable;
|
||||
|
||||
|
||||
class TableNotFoundException extends \Exception
|
||||
{
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue