add piwik installation

This commit is contained in:
coderkun 2014-04-25 03:56:02 +02:00
commit 8c5d4f0c31
3197 changed files with 563902 additions and 0 deletions

View 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);
}
}
}

View 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
{
}
}

View 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();
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}
}
}

View 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);
}
}

View 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);
}
}

View 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('+'));
}
}
}

View 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;
}
}

View 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);
}
}
}

View 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\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) . '%';
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}
}

View 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\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);
}
}
}

View 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);
}
}

View 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);
}
}
}
}

View 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));
}
}

View 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);
}
}

View 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);
}
}
}

View 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\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);
}
}
}
}

View file

@ -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);
}
}

View 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);
}
}
}
}

View 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();
}
}

View 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);
}
}
}
}
}

View 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;
}
}

View file

@ -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);
}
}
}
}

View 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);
}
}
}
}

View 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);
}
}

View 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);
}
}

View 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 />";
}
}

View 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();
}
}

View 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("&nbsp;", "&iexcl;", "&cent;", "&pound;", "&curren;", "&yen;", "&brvbar;", "&sect;", "&uml;", "&copy;", "&ordf;", "&laquo;", "&not;", "&shy;", "&reg;", "&macr;", "&deg;", "&plusmn;", "&sup2;", "&sup3;", "&acute;", "&micro;", "&para;", "&middot;", "&cedil;", "&sup1;", "&ordm;", "&raquo;", "&frac14;", "&frac12;", "&frac34;", "&iquest;", "&Agrave;", "&Aacute;", "&Acirc;", "&Atilde;", "&Auml;", "&Aring;", "&AElig;", "&Ccedil;", "&Egrave;", "&Eacute;", "&Ecirc;", "&Euml;", "&Igrave;", "&Iacute;", "&Icirc;", "&Iuml;", "&ETH;", "&Ntilde;", "&Ograve;", "&Oacute;", "&Ocirc;", "&Otilde;", "&Ouml;", "&times;", "&Oslash;", "&Ugrave;", "&Uacute;", "&Ucirc;", "&Uuml;", "&Yacute;", "&THORN;", "&szlig;", "&agrave;", "&aacute;", "&acirc;", "&atilde;", "&auml;", "&aring;", "&aelig;", "&ccedil;", "&egrave;", "&eacute;", "&ecirc;", "&euml;", "&igrave;", "&iacute;", "&icirc;", "&iuml;", "&eth;", "&ntilde;", "&ograve;", "&oacute;", "&ocirc;", "&otilde;", "&ouml;", "&divide;", "&oslash;", "&ugrave;", "&uacute;", "&ucirc;", "&uuml;", "&yacute;", "&thorn;", "&yuml;", "&euro;");
$xmlentities = array("&#162;", "&#163;", "&#164;", "&#165;", "&#166;", "&#167;", "&#168;", "&#169;", "&#170;", "&#171;", "&#172;", "&#173;", "&#174;", "&#175;", "&#176;", "&#177;", "&#178;", "&#179;", "&#180;", "&#181;", "&#182;", "&#183;", "&#184;", "&#185;", "&#186;", "&#187;", "&#188;", "&#189;", "&#190;", "&#191;", "&#192;", "&#193;", "&#194;", "&#195;", "&#196;", "&#197;", "&#198;", "&#199;", "&#200;", "&#201;", "&#202;", "&#203;", "&#204;", "&#205;", "&#206;", "&#207;", "&#208;", "&#209;", "&#210;", "&#211;", "&#212;", "&#213;", "&#214;", "&#215;", "&#216;", "&#217;", "&#218;", "&#219;", "&#220;", "&#221;", "&#222;", "&#223;", "&#224;", "&#225;", "&#226;", "&#227;", "&#228;", "&#229;", "&#230;", "&#231;", "&#232;", "&#233;", "&#234;", "&#235;", "&#236;", "&#237;", "&#238;", "&#239;", "&#240;", "&#241;", "&#242;", "&#243;", "&#244;", "&#245;", "&#246;", "&#247;", "&#248;", "&#249;", "&#250;", "&#251;", "&#252;", "&#253;", "&#254;", "&#255;", "&#8364;");
$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;
}
}

View 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 . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
foreach ($map->getDataTables() as $descTable => $table) {
$output .= $prefix . "<b>" . $descTable . "</b><br />";
$output .= $prefix . $this->renderTable($table, $prefix . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;');
$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 . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'
);
$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;
}
}

View 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;
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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');
}
}

View 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;
}
}

View 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));
}
}
}

View 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));
}
}

View 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
{
}