Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/matomo-org/matomo.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/core
diff options
context:
space:
mode:
authorThomas Steur <tsteur@users.noreply.github.com>2017-10-03 23:22:01 +0300
committerGitHub <noreply@github.com>2017-10-03 23:22:01 +0300
commit9af4e95aa976f3a6533e95b776b5298f73e5f916 (patch)
treed612cd4d32019e9e52ce1398b8bf214ec06a8e0f /core
parent359c3ec875b554c7b71a933b26d18cdde0bb8f4e (diff)
Better segment editor and fixes (#12040)
* column tweak * fix install * more tweaks * rename column to dimension * various fixes * added new control expandable select * starting to refactor segment selector * make segment editor work again * use translation keys * defined some metrics * set types * simplify * simplify * fix join generator * add possibility to use custom join table names when using query builder and it uses an inner query * fix bug in query selector when selecting same field name from different tables twice * more metadata * more tweaks * improve selector * add possibility to use custom entity names * also processed archived metrics * generate sql filter, suggested values callback, and accept values automatically for columns with enums * several tweaks * focus search field when opening it * various tweaks * added missing method * format and fix more metadata * more fixes * better definition * define custom filter * fix definition * fix various tests * fix more tests * fix bug in logquery builder * fix referrerurl segment was missing * fix some tests * fix more tests * add group * refactor for better definition * fix a bug in log query builder when similar columns are used in archiver * add goal metrics * various fixes * make datatable row more flexible * various fixes and visualization enhancements * simply segment editor and make it smaller * remove trailing comma * various fixes and added new dimension * fix formatting of returning customer * added missing primary key * fixes * various fixes and improvements * make sure to update segment definition when selecting a value from auto complete list * various fixes and more metrics * more metrics * more dimensions and fixes * fix some tests * fix some integration tests * update submodule * fix some system tests * fix ui tests * trigger new test run * fix more ui tests * fix system tests * update submodule * fix categories * sort segments by category for more consistency * add custom variables * some translations and fixes * add minute segment * more segments * added plurals * added some docs * fix test * fix tests * fix tests * added suggested values * fix some tests * various fixes * fix more tests * allow to select segments on any site * make sure to include file * added doc block * fix some system tests * fix most system tests * fix ui test * fix system test * adjust examples * added more tests and docs * no metrics for these dimensions * added developer changelog and made some classes public api * some fixes for entity names * add possibility to set format metrics in test * more consistency in defining the name * get idsites only if provided * fix integration tests * added another segment for visit start hour and visit start minute * more clear name for segment * use old segment name to not break bc * various fixes * more test fixes * fix no suggested values for new segment * add event value * for boolean dimensions only sum metric * update available widgets when updating reporting menu * Add new segments in developer changelog + typo * fix system tests * fix screenshot test
Diffstat (limited to 'core')
-rw-r--r--core/API/DataTableManipulator.php11
-rw-r--r--core/API/DocumentationGenerator.php10
-rw-r--r--core/ArchiveProcessor/PluginsArchiver.php7
-rw-r--r--core/Columns/ComputedMetricFactory.php57
-rw-r--r--core/Columns/Dimension.php670
-rw-r--r--core/Columns/DimensionMetricFactory.php122
-rw-r--r--core/Columns/DimensionsProvider.php62
-rw-r--r--core/Columns/Discriminator.php75
-rw-r--r--core/Columns/Join.php61
-rw-r--r--core/Columns/Join/ActionNameJoin.php24
-rw-r--r--core/Columns/Join/GoalNameJoin.php24
-rw-r--r--core/Columns/Join/SiteNameJoin.php24
-rw-r--r--core/Columns/MetricsList.php190
-rw-r--r--core/Columns/Updater.php15
-rw-r--r--core/DataAccess/LogAggregator.php7
-rw-r--r--core/DataAccess/LogQueryBuilder.php88
-rw-r--r--core/DataAccess/LogQueryBuilder/JoinGenerator.php13
-rw-r--r--core/DataAccess/LogQueryBuilder/JoinTables.php17
-rw-r--r--core/Metrics/Formatter.php4
-rw-r--r--core/Plugin/ArchivedMetric.php207
-rw-r--r--core/Plugin/ComputedMetric.php260
-rw-r--r--core/Plugin/Dimension/ActionDimension.php127
-rw-r--r--core/Plugin/Dimension/ConversionDimension.php116
-rw-r--r--core/Plugin/Dimension/VisitDimension.php65
-rw-r--r--core/Plugin/Metric.php10
-rw-r--r--core/Plugin/Report.php3
-rw-r--r--core/Plugin/Segment.php45
-rw-r--r--core/Plugin/Visualization.php14
-rw-r--r--core/RankingQuery.php8
-rw-r--r--core/Segment.php12
-rw-r--r--core/Segment/SegmentExpression.php18
-rw-r--r--core/Settings/FieldConfig.php6
-rw-r--r--core/Tracker/LogTable.php11
-rw-r--r--core/ViewDataTable/Config.php24
34 files changed, 2042 insertions, 365 deletions
diff --git a/core/API/DataTableManipulator.php b/core/API/DataTableManipulator.php
index 7dad3b7a0d..e9140fecf6 100644
--- a/core/API/DataTableManipulator.php
+++ b/core/API/DataTableManipulator.php
@@ -10,6 +10,7 @@ namespace Piwik\API;
use Exception;
use Piwik\Archive\DataTableFactory;
+use Piwik\Container\StaticContainer;
use Piwik\DataTable\Row;
use Piwik\DataTable;
use Piwik\Period\Range;
@@ -153,11 +154,11 @@ abstract class DataTableManipulator
}
$apiParameters = array();
- if (!empty($request['idDimension'])) {
- $apiParameters['idDimension'] = $request['idDimension'];
- }
- if (!empty($request['idGoal'])) {
- $apiParameters['idGoal'] = $request['idGoal'];
+ $entityNames = StaticContainer::get('entities.idNames');
+ foreach ($entityNames as $idName) {
+ if (!empty($request[$idName])) {
+ $apiParameters[$idName] = $request[$idName];
+ }
}
$meta = API::getInstance()->getMetadata($idSite, $this->apiModule, $this->apiMethod, $apiParameters);
diff --git a/core/API/DocumentationGenerator.php b/core/API/DocumentationGenerator.php
index 990d232163..e0116b7dfa 100644
--- a/core/API/DocumentationGenerator.php
+++ b/core/API/DocumentationGenerator.php
@@ -10,6 +10,7 @@ namespace Piwik\API;
use Exception;
use Piwik\Common;
+use Piwik\Container\StaticContainer;
use Piwik\Piwik;
use Piwik\Url;
use ReflectionClass;
@@ -283,6 +284,15 @@ class DocumentationGenerator
$aParameters['disable_generic_filters'] = false;
$aParameters['expanded'] = false;
$aParameters['idDimenson'] = false;
+ $aParameters['format_metrics'] = false;
+
+ $entityNames = StaticContainer::get('entities.idNames');
+ foreach ($entityNames as $entityName) {
+ if (isset($aParameters[$entityName])) {
+ continue;
+ }
+ $aParameters[$entityName] = false;
+ }
$moduleName = Proxy::getInstance()->getModuleNameFromClassName($class);
$aParameters = array_merge(array('module' => 'API', 'method' => $moduleName . '.' . $methodName), $aParameters);
diff --git a/core/ArchiveProcessor/PluginsArchiver.php b/core/ArchiveProcessor/PluginsArchiver.php
index 4c963e17bc..2b46ea29f6 100644
--- a/core/ArchiveProcessor/PluginsArchiver.php
+++ b/core/ArchiveProcessor/PluginsArchiver.php
@@ -147,7 +147,12 @@ class PluginsArchiver
);
} catch (Exception $e) {
$className = get_class($e);
- $exception = new $className($e->getMessage() . " - caused by plugin $pluginName", $e->getCode(), $e);
+
+ if ($className === 'PHPUnit_Framework_Exception' || (class_exists('PHPUnit_Framework_Exception', false) && is_subclass_of($className, 'PHPUnit_Framework_Exception'))) {
+ $exception = new $className($e->getMessage() . " - caused by plugin $pluginName", $e->getCode(), $e->getFile(), $e->getLine(), $e);
+ } else {
+ $exception = new $className($e->getMessage() . " - caused by plugin $pluginName", $e->getCode(), $e);
+ }
throw $exception;
}
diff --git a/core/Columns/ComputedMetricFactory.php b/core/Columns/ComputedMetricFactory.php
new file mode 100644
index 0000000000..0b2b2f5426
--- /dev/null
+++ b/core/Columns/ComputedMetricFactory.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Columns;
+
+use Piwik\Piwik;
+use Piwik\Plugin\ArchivedMetric;
+use Piwik\Plugin\ComputedMetric;
+use Piwik\Plugin\Report;
+
+/**
+ * A factory to create computed metrics.
+ *
+ * @api since Piwik 3.2.0
+ */
+class ComputedMetricFactory
+{
+ /**
+ * @var MetricsList
+ */
+ private $metricsList = null;
+
+ /**
+ * Generates a new report metric factory.
+ * @param MetricsList $list A report list instance
+ * @ignore
+ */
+ public function __construct(MetricsList $list)
+ {
+ $this->metricsList = $list;
+ }
+
+ /**
+ * @return \Piwik\Plugin\ComputedMetric
+ */
+ public function createComputedMetric($metricName1, $metricName2, $aggregation)
+ {
+ $metric1 = $this->metricsList->getMetric($metricName1);
+
+ if (!$metric1 instanceof ArchivedMetric || !$metric1->getDimension()) {
+ throw new \Exception('Only possible to create computed metric for an archived metric with a dimension');
+ }
+
+ $dimension1 = $metric1->getDimension();
+
+ $metric = new ComputedMetric($metricName1, $metricName2, $aggregation);
+ $metric->setCategory($dimension1->getCategoryId());
+
+ return $metric;
+ }
+
+} \ No newline at end of file
diff --git a/core/Columns/Dimension.php b/core/Columns/Dimension.php
index 3f31073bea..74266af05c 100644
--- a/core/Columns/Dimension.php
+++ b/core/Columns/Dimension.php
@@ -7,29 +7,48 @@
*
*/
namespace Piwik\Columns;
-
-use Exception;
-use Piwik\CacheId;
+use Piwik\Common;
+use Piwik\Db;
use Piwik\Piwik;
use Piwik\Plugin;
+use Piwik\Plugin\ArchivedMetric;
use Piwik\Plugin\ComponentFactory;
-use Piwik\Plugin\Dimension\ActionDimension;
-use Piwik\Plugin\Dimension\ConversionDimension;
-use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugin\Segment;
+use Exception;
+use Piwik\CacheId;
use Piwik\Cache as PiwikCache;
use Piwik\Plugin\Manager as PluginManager;
+use Piwik\Metrics\Formatter;
/**
* @api
- * @since 2.5.0
+ * @since 3.1.0
*/
abstract class Dimension
{
const COMPONENT_SUBNAMESPACE = 'Columns';
- // TODO that we have quite a few @ignore in public methods might show we should maybe split some code into two
- // classes.
+ /**
+ * Segment type 'dimension'. Can be used along with {@link setType()}.
+ * @api
+ */
+ const TYPE_DIMENSION = 'dimension';
+ const TYPE_BINARY = 'binary';
+ const TYPE_TEXT = 'text';
+ const TYPE_ENUM = 'enum';
+ const TYPE_MONEY = 'money';
+ const TYPE_BYTE = 'byte';
+ const TYPE_DURATION_MS = 'duration_ms';
+ const TYPE_DURATION_S = 'duration_s';
+ const TYPE_NUMBER = 'number';
+ const TYPE_FLOAT = 'float';
+ const TYPE_URL = 'url';
+ const TYPE_DATE = 'date';
+ const TYPE_TIME = 'time';
+ const TYPE_DATETIME = 'datetime';
+ const TYPE_TIMESTAMP = 'timestamp';
+ const TYPE_BOOL = 'bool';
+ const TYPE_PERCENT = 'percent';
/**
* This will be the name of the column in the database table if a $columnType is specified.
@@ -54,6 +73,392 @@ abstract class Dimension
protected $segments = array();
/**
+ * Defines what kind of data type this dimension holds. By default the type is auto-detected based on
+ * `$columnType` but sometimes it may be needed to correct this value. Depending on this type, a dimension will be
+ * formatted differently for example.
+ * @var string
+ * @api since Piwik 3.2.0
+ */
+ protected $type = '';
+
+ /**
+ * Translation key for name singular
+ * @var string
+ */
+ protected $nameSingular = '';
+
+ /**
+ * Translation key for name plural
+ * @var string
+ * @api since Piwik 3.2.0
+ */
+ protected $namePlural = '';
+
+ /**
+ * Translation key for category
+ * @var string
+ */
+ protected $category = '';
+
+ /**
+ * By defining a segment name a user will be able to filter their visitors by this column. If you do not want to
+ * define a segment for this dimension, simply leave the name empty.
+ * @api since Piwik 3.2.0
+ */
+ protected $segmentName = '';
+
+ /**
+ * Sets a callback which will be executed when user will call for suggested values for segment.
+ *
+ * @var callable
+ * @api since Piwik 3.2.0
+ */
+ protected $suggestedValuesCallback;
+
+ /**
+ * Here you should explain which values are accepted/useful for your segment, for example:
+ * "1, 2, 3, etc." or "comcast.net, proxad.net, etc.". If the value needs any special encoding you should mention
+ * this as well. For example "Any URL including protocol. The URL must be URL encoded."
+ *
+ * @var string
+ * @api since Piwik 3.2.0
+ */
+ protected $acceptValues;
+
+ /**
+ * Defines to which column in the MySQL database the segment belongs (if one is conifugred). Defaults to
+ * `$this.dbTableName . '.'. $this.columnName` but you can customize it eg like `HOUR(log_visit.visit_last_action_time)`.
+ *
+ * @param string $sqlSegment
+ * @api since Piwik 3.2.0
+ */
+ protected $sqlSegment;
+
+ /**
+ * Interesting when specifying a segment. Sometimes you want users to set segment values that differ from the way
+ * they are actually stored. For instance if you want to allow to filter by any URL than you might have to resolve
+ * this URL to an action id. Or a country name maybe has to be mapped to a 2 letter country code. You can do this by
+ * specifing either a callable such as `array('Classname', 'methodName')` or by passing a closure.
+ * There will be four values passed to the given closure or callable: `string $valueToMatch`, `string $segment`
+ * (see {@link setSegment()}), `string $matchType` (eg SegmentExpression::MATCH_EQUAL or any other match constant
+ * of this class) and `$segmentName`.
+ *
+ * If the closure returns NULL, then Piwik assumes the segment sub-string will not match any visitor.
+ *
+ * @var string|\Closure
+ * @api since Piwik 3.2.0
+ */
+ protected $sqlFilter;
+
+ /**
+ * Similar to {@link $sqlFilter} you can map a given segment value to another value. For instance you could map
+ * "new" to 0, 'returning' to 1 and any other value to '2'. You can either define a callable or a closure. There
+ * will be only one value passed to the closure or callable which contains the value a user has set for this
+ * segment.
+ * @var string|array
+ * @api since Piwik 3.2.0
+ */
+ protected $sqlFilterValue;
+
+ /**
+ * Defines whether this dimension (and segment based on this dimension) is available to anonymous users.
+ * @var bool
+ * @api since Piwik 3.2.0
+ */
+ protected $allowAnonymous = true;
+
+ /**
+ * The name of the database table this dimension refers to
+ * @var string
+ * @api
+ */
+ protected $dbTableName = '';
+
+ /**
+ * By default the metricId is automatically generated based on the dimensionId. This might sometimes not be as
+ * readable and quite long. If you want more expressive metric names like `nb_visits` compared to
+ * `nb_corehomevisitid`, you can eg set a metricId `visit`.
+ *
+ * @var string
+ * @api since Piwik 3.2.0
+ */
+ protected $metricId = '';
+
+ /**
+ * To be implemented when a column references another column
+ * @return Join|null
+ * @api since Piwik 3.2.0
+ */
+ public function getDbColumnJoin()
+ {
+ return null;
+ }
+
+ /**
+ * @return Discriminator|null
+ * @api since Piwik 3.2.0
+ */
+ public function getDbDiscriminator()
+ {
+ return null;
+ }
+
+ /**
+ * To be implemented when a column represents an enum.
+ * @return array
+ * @api since Piwik 3.2.0
+ */
+ public function getEnumColumnValues()
+ {
+ return array();
+ }
+
+ /**
+ * Get the metricId which is used to generate metric names based on this dimension.
+ * @return string
+ */
+ public function getMetricId()
+ {
+ if (!empty($this->metricId)) {
+ return $this->metricId;
+ }
+
+ $id = $this->getId();
+
+ return str_replace(array('.', ' ', '-'), '_', strtolower($id));
+ }
+
+ /**
+ * Installs the action dimension in case it is not installed yet. The installation is already implemented based on
+ * the {@link $columnName} and {@link $columnType}. If you want to perform additional actions beside adding the
+ * column to the database - for instance adding an index - you can overwrite this method. We recommend to call
+ * this parent method to get the minimum required actions and then add further custom actions since this makes sure
+ * the column will be installed correctly. We also recommend to change the default install behavior only if really
+ * needed. FYI: We do not directly execute those alter table statements here as we group them together with several
+ * other alter table statements do execute those changes in one step which results in a faster installation. The
+ * column will be added to the `log_link_visit_action` MySQL table.
+ *
+ * Example:
+ * ```
+ public function install()
+ {
+ $changes = parent::install();
+ $changes['log_link_visit_action'][] = "ADD INDEX index_idsite_servertime ( idsite, server_time )";
+
+ return $changes;
+ }
+ ```
+ *
+ * @return array An array containing the table name as key and an array of MySQL alter table statements that should
+ * be executed on the given table. Example:
+ * ```
+ array(
+ 'log_link_visit_action' => array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...")
+ );
+ ```
+ * @api
+ */
+ public function install()
+ {
+ if (empty($this->columnName) || empty($this->columnType) || empty($this->dbTableName)) {
+ return array();
+ }
+
+ // TODO if table does not exist, create it with a primary key, but at this point we cannot really create it
+ // cause we need to show the query in the UI first and user needs to be able to create table manually.
+ // we cannot return something like "create table " here as it would be returned for each table etc.
+ // we need to do this in column updater etc!
+
+ return array(
+ $this->dbTableName => array("ADD COLUMN `$this->columnName` $this->columnType")
+ );
+ }
+
+ /**
+ * Updates the action dimension in case the {@link $columnType} has changed. The update is already implemented based
+ * on the {@link $columnName} and {@link $columnType}. This method is intended not to overwritten by plugin
+ * developers as it is only supposed to make sure the column has the correct type. Adding additional custom "alter
+ * table" actions would not really work since they would be executed with every {@link $columnType} change. So
+ * adding an index here would be executed whenever the columnType changes resulting in an error if the index already
+ * exists. If an index needs to be added after the first version is released a plugin update class should be
+ * created since this makes sure it is only executed once.
+ *
+ * @return array An array containing the table name as key and an array of MySQL alter table statements that should
+ * be executed on the given table. Example:
+ * ```
+ array(
+ 'log_link_visit_action' => array("MODIFY COLUMN `$this->columnName` $this->columnType", "DROP COLUMN ...")
+ );
+ ```
+ * @ignore
+ */
+ public function update()
+ {
+ if (empty($this->columnName) || empty($this->columnType) || empty($this->dbTableName)) {
+ return array();
+ }
+
+ return array(
+ $this->dbTableName => array("MODIFY COLUMN `$this->columnName` $this->columnType")
+ );
+ }
+
+ /**
+ * Uninstalls the dimension if a {@link $columnName} and {@link columnType} is set. In case you perform any custom
+ * actions during {@link install()} - for instance adding an index - you should make sure to undo those actions by
+ * overwriting this method. Make sure to call this parent method to make sure the uninstallation of the column
+ * will be done.
+ * @throws Exception
+ * @api
+ */
+ public function uninstall()
+ {
+ if (empty($this->columnName) || empty($this->columnType) || empty($this->dbTableName)) {
+ return;
+ }
+
+ try {
+ $sql = "ALTER TABLE `" . Common::prefixTable($this->dbTableName) . "` DROP COLUMN `$this->columnName`";
+ Db::exec($sql);
+ } catch (Exception $e) {
+ if (!Db::get()->isErrNo($e, '1091')) {
+ throw $e;
+ }
+ }
+ }
+
+ /**
+ * Returns the ID of the category (typically a translation key).
+ * @return string
+ */
+ public function getCategoryId()
+ {
+ return $this->category;
+ }
+
+ /**
+ * Returns the translated name of this dimension which is typically in singular.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ if (!empty($this->nameSingular)) {
+ return Piwik::translate($this->nameSingular);
+ }
+
+ return $this->nameSingular;
+ }
+
+ /**
+ * Returns a translated name in plural for this dimension.
+ * @return string
+ * @api since Piwik 3.2.0
+ */
+ public function getNamePlural()
+ {
+ if (!empty($this->namePlural)) {
+ return Piwik::translate($this->namePlural);
+ }
+
+ return $this->getName();
+ }
+
+ /**
+ * Defines whether an anonymous user is allowed to view this dimension
+ * @return bool
+ * @api since Piwik 3.2.0
+ */
+ public function isAnonymousAllowed()
+ {
+ return $this->allowAnonymous;
+ }
+
+ /**
+ * Sets (overwrites) the SQL segment
+ * @param $segment
+ * @api since Piwik 3.2.0
+ */
+ public function setSqlSegment($segment)
+ {
+ $this->sqlSegment = $segment;
+ }
+
+ /**
+ * Sets (overwrites the dimension type)
+ * @param $type
+ * @api since Piwik 3.2.0
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+ }
+
+ /**
+ * A dimension should group values by using this method. Otherwise the same row may appear several times.
+ *
+ * @param mixed $value
+ * @param int $idSite
+ * @return mixed
+ * @api since Piwik 3.2.0
+ */
+ public function groupValue($value, $idSite)
+ {
+ switch ($this->type) {
+ case Dimension::TYPE_URL:
+ return str_replace(array('http://', 'https://'), '', $value);
+ case Dimension::TYPE_BOOL:
+ return !empty($value) ? '1' : '0';
+ case Dimension::TYPE_DURATION_MS:
+ return number_format($value / 1000, 2); // because we divide we need to group them and cannot do this in formatting step
+ }
+ return $value;
+ }
+
+ /**
+ * Formats the dimension value. By default, the dimension is formatted based on the set dimension type.
+ *
+ * @param mixed $value
+ * @param int $idSite
+ * @param Formatter $formatter
+ * @return mixed
+ * @api since Piwik 3.2.0
+ */
+ public function formatValue($value, $idSite, Formatter $formatter)
+ {
+ switch ($this->type) {
+ case Dimension::TYPE_BOOL:
+ if (empty($value)) {
+ return Piwik::translate('General_No');
+ }
+
+ return Piwik::translate('General_Yes');
+ case Dimension::TYPE_ENUM:
+ $values = $this->getEnumColumnValues();
+ if (isset($values[$value])) {
+ return $values[$value];
+ }
+ break;
+ case Dimension::TYPE_MONEY:
+ return $formatter->getPrettyMoney($value, $idSite);
+ case Dimension::TYPE_FLOAT:
+ return $formatter->getPrettyNumber((float) $value, $precision = 2);
+ case Dimension::TYPE_NUMBER:
+ return $formatter->getPrettyNumber($value);
+ case Dimension::TYPE_DURATION_S:
+ return $formatter->getPrettyTimeFromSeconds($value, $displayAsSentence = false);
+ case Dimension::TYPE_DURATION_MS:
+ return $formatter->getPrettyTimeFromSeconds($value, $displayAsSentence = true);
+ case Dimension::TYPE_PERCENT:
+ return $formatter->getPrettyPercentFromQuotient($value);
+ case Dimension::TYPE_BYTE:
+ return $formatter->getPrettySizeFromBytes($value);
+ }
+
+ return $value;
+ }
+
+ /**
* Overwrite this method to configure segments. To do so just create an instance of a {@link \Piwik\Plugin\Segment}
* class, configure it and call the {@link addSegment()} method. You can add one or more segments for this
* dimension. Example:
@@ -68,6 +473,42 @@ abstract class Dimension
*/
protected function configureSegments()
{
+ if ($this->segmentName && $this->category
+ && ($this->sqlSegment || ($this->columnName && $this->dbTableName))
+ && $this->nameSingular) {
+ $segment = new Segment();
+ $this->addSegment($segment);
+ }
+ }
+
+ /**
+ * Configures metrics for this dimension.
+ *
+ * For certain dimension types, some metrics will be added automatically.
+ *
+ * @param MetricsList $metricsList
+ * @param DimensionMetricFactory $dimensionMetricFactory
+ */
+ public function configureMetrics(MetricsList $metricsList, DimensionMetricFactory $dimensionMetricFactory)
+ {
+ if ($this->getMetricId() && $this->dbTableName && $this->columnName && $this->getNamePlural()) {
+ if (in_array($this->getType(), array(self::TYPE_DATETIME, self::TYPE_DATE, self::TYPE_TIME, self::TYPE_TIMESTAMP))) {
+ // we do not generate any metrics from these types
+ return;
+ } elseif (in_array($this->getType(), array(self::TYPE_URL, self::TYPE_TEXT, self::TYPE_BINARY, self::TYPE_ENUM))) {
+ $metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_UNIQUE);
+ $metricsList->addMetric($metric);
+ } elseif (in_array($this->getType(), array(self::TYPE_BOOL))) {
+ $metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM);
+ $metricsList->addMetric($metric);
+ } else {
+ $metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_SUM);
+ $metricsList->addMetric($metric);
+
+ $metric = $dimensionMetricFactory->createMetric(ArchivedMetric::AGGREGATION_MAX);
+ $metricsList->addMetric($metric);
+ }
+ }
}
/**
@@ -85,16 +526,110 @@ abstract class Dimension
}
/**
- * Adds a new segment. The segment type will be set to 'dimension' automatically if not already set.
+ * Adds a new segment. It automatically sets the SQL segment depending on the column name in case none is set
+ * already.
+ * @see \Piwik\Columns\Dimension::addSegment()
* @param Segment $segment
* @api
*/
protected function addSegment(Segment $segment)
{
- $type = $segment->getType();
+ if (!$segment->getSegment() && $this->segmentName) {
+ $segment->setSegment($this->segmentName);
+ }
+
+ if (!$segment->getType()) {
+ $metricTypes = array(self::TYPE_NUMBER, self::TYPE_FLOAT, self::TYPE_MONEY, self::TYPE_DURATION_S, self::TYPE_DURATION_MS);
+ if (in_array($this->getType(), $metricTypes, $strict = true)) {
+ $segment->setType(Segment::TYPE_METRIC);
+ } else {
+ $segment->setType(Segment::TYPE_DIMENSION);
+ }
+ }
+
+ if (!$segment->getCategoryId() && $this->category) {
+ $segment->setCategory($this->category);
+ }
+
+ if (!$segment->getName() && $this->nameSingular) {
+ $segment->setName($this->nameSingular);
+ }
+
+ $sqlSegment = $segment->getSqlSegment();
+
+ if (empty($sqlSegment) && !$segment->getUnionOfSegments()) {
+ if (!empty($this->sqlSegment)) {
+ $segment->setSqlSegment($this->sqlSegment);
+ } elseif ($this->dbTableName && $this->columnName) {
+ $segment->setSqlSegment($this->dbTableName . '.' . $this->columnName);
+ } else {
+ throw new Exception('Segment cannot be added because no sql segment is set');
+ }
+ }
+
+ if (!$this->suggestedValuesCallback) {
+ // we can generate effecient value callback for enums automatically
+ $enum = $this->getEnumColumnValues();
+ if (!empty($enum)) {
+ $this->suggestedValuesCallback = function ($idSite, $maxValuesToReturn) use ($enum) {
+ $values = array_values($enum);
+ return array_slice($values, 0, $maxValuesToReturn);
+ };
+ }
+ }
+
+ if (!$this->acceptValues) {
+ // we can generate accept values for enums automatically
+ $enum = $this->getEnumColumnValues();
+ if (!empty($enum)) {
+ $enumValues = array_values($enum);
+ $enumValues = array_slice($enumValues, 0, 20);
+ $this->acceptValues = 'Eg. ' . implode(', ', $enumValues);
+ };
+ }
+
+ if ($this->acceptValues && !$segment->getAcceptValues()) {
+ $segment->setAcceptedValues($this->acceptValues);
+ }
+
+ if (!$this->sqlFilterValue && !$segment->getSqlFilter() && !$segment->getSqlFilterValue()) {
+ // no sql filter configured, we try to configure automatically for enums
+ $enum = $this->getEnumColumnValues();
+ if (!empty($enum)) {
+ $this->sqlFilterValue = function ($value, $sqlSegmentName) use ($enum) {
+ if (isset($enum[$value])) {
+ return $value;
+ }
+
+ $id = array_search($value, $enum);
+
+ if ($id === false) {
+ $id = array_search(strtolower(trim(urldecode($value))), $enum);
+
+ if ($id === false) {
+ throw new \Exception("Invalid '$sqlSegmentName' segment value $value");
+ }
+ }
+
+ return $id;
+ };
+ };
+ }
+
+ if ($this->suggestedValuesCallback && !$segment->getSuggestedValuesCallback()) {
+ $segment->setSuggestedValuesCallback($this->suggestedValuesCallback);
+ }
+
+ if ($this->sqlFilterValue && !$segment->getSqlFilterValue()) {
+ $segment->setSqlFilterValue($this->sqlFilterValue);
+ }
+
+ if ($this->sqlFilter && !$segment->getSqlFilter()) {
+ $segment->setSqlFilter($this->sqlFilter);
+ }
- if (empty($type)) {
- $segment->setType(Segment::TYPE_DIMENSION);
+ if (!$this->allowAnonymous) {
+ $segment->setRequiresAtLeastViewAccess(true);
}
$this->segments[] = $segment;
@@ -115,6 +650,16 @@ abstract class Dimension
}
/**
+ * Returns the name of the segment that this dimension defines
+ * @return string
+ * @api since Piwik 3.2.0
+ */
+ public function getSegmentName()
+ {
+ return $this->segmentName;
+ }
+
+ /**
* Get the name of the dimension column.
* @return string
* @ignore
@@ -125,6 +670,22 @@ abstract class Dimension
}
/**
+ * Returns a sql segment expression for this dimension.
+ * @return string
+ * @api since Piwik 3.2.0
+ */
+ public function getSqlSegment()
+ {
+ if (!empty($this->sqlSegment)) {
+ return $this->sqlSegment;
+ }
+
+ if ($this->dbTableName && $this->columnName) {
+ return $this->dbTableName . '.' . $this->columnName;
+ }
+ }
+
+ /**
* Check whether the dimension has a column type configured
* @return bool
* @ignore
@@ -135,13 +696,13 @@ abstract class Dimension
}
/**
- * Get the translated name of the dimension. Defaults to an empty string.
+ * Returns the name of the database table this dimension belongs to.
* @return string
- * @api
+ * @api since Piwik 3.2.0
*/
- public function getName()
+ public function getDbTableName()
{
- return '';
+ return $this->dbTableName;
}
/**
@@ -157,6 +718,17 @@ abstract class Dimension
{
$className = get_class($this);
+ return $this->generateIdFromClass($className);
+ }
+
+ /**
+ * @param string $className
+ * @return string
+ * @throws Exception
+ * @ignore
+ */
+ protected function generateIdFromClass($className)
+ {
// parse plugin name & dimension name
$regex = "/Piwik\\\\Plugins\\\\([^\\\\]+)\\\\" . self::COMPONENT_SUBNAMESPACE . "\\\\([^\\\\]+)/";
if (!preg_match($regex, $className, $matches)) {
@@ -232,11 +804,11 @@ abstract class Dimension
public static function getDimensions(Plugin $plugin)
{
- $dimensions = $plugin->findMultipleComponents('Columns', '\\Piwik\\Columns\\Dimension');
+ $columns = $plugin->findMultipleComponents('Columns', '\\Piwik\\Columns\\Dimension');
$instances = array();
- foreach ($dimensions as $dimension) {
- $instances[] = new $dimension();
+ foreach ($columns as $colum) {
+ $instances[] = new $colum();
}
return $instances;
@@ -250,6 +822,7 @@ abstract class Dimension
* $dimensionId or if the plugin that contains the Dimension is
* not loaded.
* @api
+ * @deprecated Please use DimensionProvider::factory instead
*/
public static function factory($dimensionId)
{
@@ -274,4 +847,61 @@ abstract class Dimension
$parts = explode('.', $id);
return reset($parts);
}
+
+ /**
+ * Returns the type of the dimension which defines what kind of value this dimension stores.
+ * @return string
+ * @api since Piwik 3.2.0
+ */
+ public function getType()
+ {
+ if (!empty($this->type)) {
+ return $this->type;
+ }
+
+ if ($this->getDbColumnJoin()) {
+ // best guess
+ return self::TYPE_TEXT;
+ }
+
+ if ($this->getEnumColumnValues()) {
+ // best guess
+ return self::TYPE_ENUM;
+ }
+
+ if (!empty($this->columnType)) {
+ // best guess
+ $type = strtolower($this->columnType);
+ if (strpos($type, 'datetime') !== false) {
+ return self::TYPE_DATETIME;
+ } elseif (strpos($type, 'timestamp') !== false) {
+ return self::TYPE_TIMESTAMP;
+ } elseif (strpos($type, 'date') !== false) {
+ return self::TYPE_DATE;
+ } elseif (strpos($type, 'time') !== false) {
+ return self::TYPE_TIME;
+ } elseif (strpos($type, 'float') !== false) {
+ return self::TYPE_FLOAT;
+ } elseif (strpos($type, 'decimal') !== false) {
+ return self::TYPE_FLOAT;
+ } elseif (strpos($type, 'int') !== false) {
+ return self::TYPE_NUMBER;
+ } elseif (strpos($type, 'binary') !== false) {
+ return self::TYPE_BINARY;
+ }
+ }
+
+ return self::TYPE_TEXT;
+ }
+
+ /**
+ * Get the version of the dimension which is used for update checks.
+ * @return string
+ * @ignore
+ */
+ public function getVersion()
+ {
+ return $this->columnType;
+ }
+
}
diff --git a/core/Columns/DimensionMetricFactory.php b/core/Columns/DimensionMetricFactory.php
new file mode 100644
index 0000000000..58178951ec
--- /dev/null
+++ b/core/Columns/DimensionMetricFactory.php
@@ -0,0 +1,122 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Columns;
+
+use Piwik\Piwik;
+use Piwik\Plugin\ArchivedMetric;
+use Piwik\Plugin\ComputedMetric;
+use Piwik\Plugin\Report;
+
+
+/**
+ * A factory to create metrics from a dimension.
+ *
+ * @api since Piwik 3.2.0
+ */
+class DimensionMetricFactory
+{
+ /**
+ * @var Dimension
+ */
+ private $dimension = null;
+
+ /**
+ * Generates a new dimension metric factory.
+ * @param Dimension $dimension A dimension instance the created metrics should be based on.
+ */
+ public function __construct(Dimension $dimension)
+ {
+ $this->dimension = $dimension;
+ }
+
+ /**
+ * @return ArchivedMetric
+ */
+ public function createCustomMetric($metricName, $readableName, $aggregation, $documentation = '')
+ {
+ if (!$this->dimension->getDbTableName() || !$this->dimension->getColumnName()) {
+ throw new \Exception(sprintf('Cannot make metric from dimension %s because DB table or column missing', $this->dimension->getId()));
+ }
+
+ $metric = new ArchivedMetric($this->dimension, $aggregation);
+ $metric->setType($this->dimension->getType());
+ $metric->setName($metricName);
+ $metric->setTranslatedName($readableName);
+ $metric->setDocumentation($documentation);
+ $metric->setCategory($this->dimension->getCategoryId());
+
+ return $metric;
+ }
+
+ /**
+ * @return \Piwik\Plugin\ComputedMetric
+ */
+ public function createComputedMetric($metricName1, $metricName2, $aggregation)
+ {
+ // We cannot use reuse ComputedMetricFactory here as it would result in an endless loop since ComputedMetricFactory
+ // requires a MetricsList which is just being built here...
+ $metric = new ComputedMetric($metricName1, $metricName2, $aggregation);
+ $metric->setCategory($this->dimension->getCategoryId());
+ return $metric;
+ }
+
+ /**
+ * @return ArchivedMetric
+ */
+ public function createMetric($aggregation)
+ {
+ $dimension = $this->dimension;
+
+ if (!$dimension->getNamePlural()) {
+ throw new \Exception(sprintf('No metric can be created for this dimension %s automatically because no $namePlural is set.', $dimension->getId()));
+ }
+
+ $prefix = '';
+ $translatedName = $dimension->getNamePlural();
+
+ $documentation = '';
+
+ switch ($aggregation) {
+ case ArchivedMetric::AGGREGATION_COUNT;
+ $prefix = ArchivedMetric::AGGREGATION_COUNT_PREFIX;
+ $translatedName = $dimension->getNamePlural();
+ $documentation = Piwik::translate('General_ComputedMetricCountDocumentation', $dimension->getNamePlural());
+ break;
+ case ArchivedMetric::AGGREGATION_SUM;
+ $prefix = ArchivedMetric::AGGREGATION_SUM_PREFIX;
+ $translatedName = Piwik::translate('General_ComputedMetricSum', $dimension->getNamePlural());
+ $documentation = Piwik::translate('General_ComputedMetricSumDocumentation', $dimension->getNamePlural());
+ break;
+ case ArchivedMetric::AGGREGATION_MAX;
+ $prefix = ArchivedMetric::AGGREGATION_MAX_PREFIX;
+ $translatedName = Piwik::translate('General_ComputedMetricMax', $dimension->getNamePlural());
+ $documentation = Piwik::translate('General_ComputedMetricMaxDocumentation', $dimension->getNamePlural());
+ break;
+ case ArchivedMetric::AGGREGATION_MIN;
+ $prefix = ArchivedMetric::AGGREGATION_MIN_PREFIX;
+ $translatedName = Piwik::translate('General_ComputedMetricMin', $dimension->getNamePlural());
+ $documentation = Piwik::translate('General_ComputedMetricMinDocumentation', $dimension->getNamePlural());
+ break;
+ case ArchivedMetric::AGGREGATION_UNIQUE;
+ $prefix = ArchivedMetric::AGGREGATION_UNIQUE_PREFIX;
+ $translatedName = Piwik::translate('General_ComputedMetricUniqueCount', $dimension->getNamePlural());
+ $documentation = Piwik::translate('General_ComputedMetricUniqueCountDocumentation', $dimension->getNamePlural());
+ break;
+ case ArchivedMetric::AGGREGATION_COUNT_WITH_NUMERIC_VALUE;
+ $prefix = ArchivedMetric::AGGREGATION_COUNT_WITH_NUMERIC_VALUE_PREFIX;
+ $translatedName = Piwik::translate('General_ComputedMetricCountWithValue', $dimension->getName());
+ $documentation = Piwik::translate('General_ComputedMetricCountWithValueDocumentation', $dimension->getName());
+ break;
+ }
+
+ $metricId = strtolower($dimension->getMetricId());
+
+ return $this->createCustomMetric($prefix . $metricId, $translatedName, $aggregation, $documentation);
+ }
+} \ No newline at end of file
diff --git a/core/Columns/DimensionsProvider.php b/core/Columns/DimensionsProvider.php
new file mode 100644
index 0000000000..cd9da962b6
--- /dev/null
+++ b/core/Columns/DimensionsProvider.php
@@ -0,0 +1,62 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Columns;
+
+use Piwik\CacheId;
+use Piwik\Cache as PiwikCache;
+
+class DimensionsProvider
+{
+ /**
+ * @param $dimensionId
+ * @return Dimension
+ */
+ public function factory($dimensionId)
+ {
+ $listDimensions = self::getMapOfNameToDimension();
+
+ if (empty($listDimensions) || !is_array($listDimensions) || !$dimensionId || !array_key_exists($dimensionId, $listDimensions)) {
+ return null;
+ }
+
+ return $listDimensions[$dimensionId];
+ }
+
+ private static function getMapOfNameToDimension()
+ {
+ $cacheId = CacheId::pluginAware('DimensionFactoryMap');
+
+ $cache = PiwikCache::getTransientCache();
+ if ($cache->contains($cacheId)) {
+ $mapIdToDimension = $cache->fetch($cacheId);
+ } else {
+ $dimensions = new static();
+ $dimensions = $dimensions->getAllDimensions();
+
+ $mapIdToDimension = array();
+ foreach ($dimensions as $dimension) {
+ $mapIdToDimension[$dimension->getId()] = $dimension;
+ }
+
+ $cache->save($cacheId, $mapIdToDimension);
+ }
+
+ return $mapIdToDimension;
+ }
+
+ /**
+ * Returns a list of all available dimensions.
+ * @return Dimension[]
+ */
+ public function getAllDimensions()
+ {
+ return Dimension::getAllDimensions();
+ }
+
+} \ No newline at end of file
diff --git a/core/Columns/Discriminator.php b/core/Columns/Discriminator.php
new file mode 100644
index 0000000000..91ad6d2e38
--- /dev/null
+++ b/core/Columns/Discriminator.php
@@ -0,0 +1,75 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Columns;
+
+use Exception;
+use Piwik\Plugins\Actions\Actions\ActionSiteSearch;
+
+/**
+ * @api
+ * @since 3.1.0
+ */
+class Discriminator
+{
+ private $table;
+ private $discriminatorColumn;
+ private $discriminatorValue;
+
+ /**
+ * Join constructor.
+ * @param string $table unprefixed table name
+ * @param null|string $discriminatorColumn
+ * @param null|int $discriminatorValue should be only hard coded, safe values.
+ * @throws Exception
+ */
+ public function __construct($table, $discriminatorColumn = null, $discriminatorValue = null)
+ {
+ if (empty($discriminatorColumn) || !isset($discriminatorValue)) {
+ throw new Exception('Both discriminatorColumn and discriminatorValue need to be defined');
+ }
+ $this->table = $table;
+ $this->discriminatorColumn = $discriminatorColumn;
+ $this->discriminatorValue = $discriminatorValue;
+
+ if (!$this->isValid()) {
+ // if adding another string value please post an event instead to get a list of allowed values
+ throw new Exception('$discriminatorValue needs to be null or numeric');
+ }
+ }
+
+ public function isValid()
+ {
+ return isset($this->discriminatorColumn)
+ && (is_numeric($this->discriminatorValue) || $this->discriminatorValue == ActionSiteSearch::CVAR_KEY_SEARCH_CATEGORY);
+ }
+
+ /**
+ * @return string
+ */
+ public function getTable()
+ {
+ return $this->table;
+ }
+
+ /**
+ * @return string
+ */
+ public function getColumn()
+ {
+ return $this->discriminatorColumn;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getValue()
+ {
+ return $this->discriminatorValue;
+ }
+}
diff --git a/core/Columns/Join.php b/core/Columns/Join.php
new file mode 100644
index 0000000000..75afec98a7
--- /dev/null
+++ b/core/Columns/Join.php
@@ -0,0 +1,61 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Columns;
+
+use Exception;
+
+/**
+ * @api
+ * @since 3.1.0
+ */
+class Join
+{
+ private $table;
+ private $column;
+ private $targetColumn;
+
+ /**
+ * Join constructor.
+ * @param $table
+ * @param $column
+ * @param $targetColumn
+ * @throws Exception
+ */
+ public function __construct($table, $column, $targetColumn)
+ {
+ $this->table = $table;
+ $this->column = $column;
+ $this->targetColumn = $targetColumn;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTable()
+ {
+ return $this->table;
+ }
+
+ /**
+ * @return string
+ */
+ public function getColumn()
+ {
+ return $this->column;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTargetColumn()
+ {
+ return $this->targetColumn;
+ }
+
+}
diff --git a/core/Columns/Join/ActionNameJoin.php b/core/Columns/Join/ActionNameJoin.php
new file mode 100644
index 0000000000..64fdd25c8c
--- /dev/null
+++ b/core/Columns/Join/ActionNameJoin.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Columns\Join;
+
+use Piwik\Columns;
+
+/**
+ * @api
+ * @since 3.1.0
+ */
+class ActionNameJoin extends Columns\Join
+{
+ public function __construct()
+ {
+ return parent::__construct('log_action', 'idaction', 'name');
+ }
+
+}
diff --git a/core/Columns/Join/GoalNameJoin.php b/core/Columns/Join/GoalNameJoin.php
new file mode 100644
index 0000000000..ed6f875983
--- /dev/null
+++ b/core/Columns/Join/GoalNameJoin.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Columns\Join;
+
+use Piwik\Columns;
+
+/**
+ * @api
+ * @since 3.1.0
+ */
+class GoalNameJoin extends Columns\Join
+{
+ public function __construct()
+ {
+ return parent::__construct('goal', 'idgoal', 'name');
+ }
+
+}
diff --git a/core/Columns/Join/SiteNameJoin.php b/core/Columns/Join/SiteNameJoin.php
new file mode 100644
index 0000000000..206c585b75
--- /dev/null
+++ b/core/Columns/Join/SiteNameJoin.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Columns\Join;
+
+use Piwik\Columns;
+
+/**
+ * @api
+ * @since 3.1.0
+ */
+class SiteNameJoin extends Columns\Join
+{
+ public function __construct()
+ {
+ return parent::__construct('site', 'idsite', 'name');
+ }
+
+}
diff --git a/core/Columns/MetricsList.php b/core/Columns/MetricsList.php
new file mode 100644
index 0000000000..e65ec67ec7
--- /dev/null
+++ b/core/Columns/MetricsList.php
@@ -0,0 +1,190 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ *
+ */
+namespace Piwik\Columns;
+
+use Piwik\Cache;
+use Piwik\Piwik;
+use Piwik\Plugin\ArchivedMetric;
+use Piwik\Plugin\Metric;
+use Piwik\Plugin\ProcessedMetric;
+
+/**
+ * Manages the global list of metrics that can be used in reports.
+ *
+ * Metrics are added automatically by dimensions as well as through the {@hook Metric.addMetrics} and
+ * {@hook Metric.addComputedMetrics} and filtered through the {@hook Metric.filterMetrics} event.
+ * Observers for this event should call the {@link addMetric()} method to add metrics or use any of the other
+ * methods to remove metrics.
+ *
+ * @api since Piwik 3.2.0
+ */
+class MetricsList
+{
+ /**
+ * List of metrics
+ *
+ * @var Metric[]
+ */
+ private $metrics = array();
+
+ /**
+ * @param Metric $metric
+ */
+ public function addMetric(Metric $metric)
+ {
+ $this->metrics[] = $metric;
+ }
+
+ /**
+ * Get all available metrics.
+ *
+ * @return Metric[]
+ */
+ public function getMetrics()
+ {
+ return $this->metrics;
+ }
+
+ /**
+ * Removes one or more metrics from the metrics list.
+ *
+ * @param string $metricCategory The metric category id. Can be a translation token eg 'General_Visits'
+ * see {@link Metric::getCategory()}.
+ * @param string|false $metricName The name of the metric to remove eg 'nb_visits'.
+ * If not supplied, all metrics within that category will be removed.
+ */
+ public function remove($metricCategory, $metricName = false)
+ {
+ foreach ($this->metrics as $index => $metric) {
+ if ($metric->getCategoryId() === $metricCategory) {
+ if (!$metricName || $metric->getName() === $metricName) {
+ unset($this->metrics[$index]);
+ }
+ }
+ }
+ }
+
+ /**
+ * @param string $metricName
+ * @return Metric|ArchivedMetric|null
+ */
+ public function getMetric($metricName)
+ {
+ foreach ($this->metrics as $index => $metric) {
+ if ($metric->getName() === $metricName) {
+ return $metric;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get all metrics defined in the Piwik platform.
+ * @ignore
+ * @return static
+ */
+ public static function get()
+ {
+ $cache = Cache::getTransientCache();
+ $cacheKey = 'MetricsList';
+
+ foreach (array('idsite', 'idSite') as $param) {
+ if (!empty($_GET[$param]) && is_numeric($_GET[$param])) {
+ $cacheKey .= $cacheKey . '_' . $_GET[$param];
+ }
+
+ if (!empty($_POST[$param]) && is_numeric($_POST[$param])) {
+ $cacheKey .= $cacheKey . '_' . $_POST[$param];
+ }
+ }
+
+ if ($cache->contains($cacheKey)) {
+ return $cache->fetch($cacheKey);
+ }
+
+ $list = new static;
+
+ /**
+ * Triggered to add new metrics that cannot be picked up automatically by the platform.
+ * This is useful if the plugin allows a user to create metrics dynamically. For example
+ * CustomDimensions or CustomVariables.
+ *
+ * **Example**
+ *
+ * public function addMetric(&$list)
+ * {
+ * $list->addMetric(new MyCustomMetric());
+ * }
+ *
+ * @param MetricsList $list An instance of the MetricsList. You can add metrics to the list this way.
+ */
+ Piwik::postEvent('Metric.addMetrics', array($list));
+
+ $dimensions = Dimension::getAllDimensions();
+ foreach ($dimensions as $dimension) {
+ $factory = new DimensionMetricFactory($dimension);
+ $dimension->configureMetrics($list, $factory);
+ }
+
+ $computedFactory = new ComputedMetricFactory($list);
+
+ /**
+ * Triggered to add new metrics that cannot be picked up automatically by the platform.
+ * This is useful if the plugin allows a user to create metrics dynamically. For example
+ * CustomDimensions or CustomVariables.
+ *
+ * **Example**
+ *
+ * public function addMetric(&$list)
+ * {
+ * $list->addMetric(new MyCustomMetric());
+ * }
+ *
+ * @param MetricsList $list An instance of the MetricsList. You can add metrics to the list this way.
+ */
+ Piwik::postEvent('Metric.addComputedMetrics', array($list, $computedFactory));
+
+ /**
+ * Triggered to filter metrics.
+ *
+ * **Example**
+ *
+ * public function removeMetrics(Piwik\Columns\MetricsList $list)
+ * {
+ * $list->remove($category='General_Visits'); // remove all metrics having this category
+ * }
+ *
+ * @param MetricsList $list An instance of the MetricsList. You can change the list of metrics this way.
+ */
+ Piwik::postEvent('Metric.filterMetrics', array($list));
+
+ $availableMetrics = array();
+ foreach ($list->getMetrics() as $metric) {
+ $availableMetrics[] = $metric->getName();
+ }
+
+ foreach ($list->metrics as $index => $metric) {
+ if ($metric instanceof ProcessedMetric) {
+ $depMetrics = $metric->getDependentMetrics();
+ if (is_array($depMetrics)) {
+ foreach ($depMetrics as $depMetric) {
+ if (!in_array($depMetric, $availableMetrics, $strict = true)) {
+ unset($list->metrics[$index]); // not resolvable metric
+ }
+ }
+ }
+ }
+ }
+
+ $cache->save($cacheKey, $list);
+
+ return $list;
+ }
+
+}
diff --git a/core/Columns/Updater.php b/core/Columns/Updater.php
index fe63d4c146..73da658fe8 100644
--- a/core/Columns/Updater.php
+++ b/core/Columns/Updater.php
@@ -123,7 +123,7 @@ class Updater extends \Piwik\Updates
$allUpdatesToRun = array();
foreach ($this->getVisitDimensions() as $dimension) {
- $updates = $this->getUpdatesForDimension($updater, $dimension, 'log_visit.', $visitColumns, $conversionColumns);
+ $updates = $this->getUpdatesForDimension($updater, $dimension, 'log_visit.', $visitColumns);
$allUpdatesToRun = $this->mixinUpdates($allUpdatesToRun, $updates);
}
@@ -143,11 +143,9 @@ class Updater extends \Piwik\Updates
/**
* @param ActionDimension|ConversionDimension|VisitDimension $dimension
* @param string $componentPrefix
- * @param array $existingColumnsInDb
- * @param array $conversionColumns
* @return array
*/
- private function getUpdatesForDimension(PiwikUpdater $updater, $dimension, $componentPrefix, $existingColumnsInDb, $conversionColumns = array())
+ private function getUpdatesForDimension(PiwikUpdater $updater, $dimension, $componentPrefix, $existingColumnsInDb)
{
$column = $dimension->getColumnName();
$componentName = $componentPrefix . $column;
@@ -157,11 +155,7 @@ class Updater extends \Piwik\Updates
}
if (array_key_exists($column, $existingColumnsInDb)) {
- if ($dimension instanceof VisitDimension) {
- $sqlUpdates = $dimension->update($conversionColumns);
- } else {
- $sqlUpdates = $dimension->update();
- }
+ $sqlUpdates = $dimension->update();
} else {
$sqlUpdates = $dimension->install();
}
@@ -218,7 +212,8 @@ class Updater extends \Piwik\Updates
}
/**
- * @param ActionDimension|ConversionDimension|VisitDimension $dimension
+ * @param PiwikUpdater $updater
+ * @param Dimension $dimension
* @param string $componentPrefix
* @param array $columns
* @param array $versions
diff --git a/core/DataAccess/LogAggregator.php b/core/DataAccess/LogAggregator.php
index c30132f41e..ede9f81526 100644
--- a/core/DataAccess/LogAggregator.php
+++ b/core/DataAccess/LogAggregator.php
@@ -164,6 +164,11 @@ class LogAggregator
$this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
}
+ public function getSegment()
+ {
+ return $this->segment;
+ }
+
public function setQueryOriginHint($nameOfOrigiin)
{
$this->queryOriginHint = $nameOfOrigiin;
@@ -515,7 +520,7 @@ class LogAggregator
*
* @return array
*/
- protected function getGeneralQueryBindParams()
+ public function getGeneralQueryBindParams()
{
$bind = array($this->dateStart->toString(Date::DATE_TIME_FORMAT), $this->dateEnd->toString(Date::DATE_TIME_FORMAT));
$bind = array_merge($bind, $this->sites);
diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php
index 186ed0d96e..7989b72e80 100644
--- a/core/DataAccess/LogQueryBuilder.php
+++ b/core/DataAccess/LogQueryBuilder.php
@@ -22,11 +22,26 @@ class LogQueryBuilder
*/
private $logTableProvider;
+ /**
+ * Forces to use a subselect when generating the query. Set value to `false` to force not using a subselect.
+ * @var string
+ */
+ private $forcedInnerGroupBy = '';
+
public function __construct(LogTablesProvider $logTablesProvider)
{
$this->logTableProvider = $logTablesProvider;
}
+ /**
+ * Forces to use a subselect when generating the query.
+ * @var string
+ */
+ public function forceInnerGroupBySubselect($innerGroupBy)
+ {
+ $this->forcedInnerGroupBy = $innerGroupBy;
+ }
+
public function getSelectQueryString(SegmentExpression $segmentExpression, $select, $from, $where, $bind, $groupBy,
$orderBy, $limitAndOffset)
{
@@ -55,11 +70,13 @@ class LogQueryBuilder
&& $fromInitially == array('log_conversion')
&& strpos($from, 'log_link_visit_action') !== false);
- if ($useSpecialConversionGroupBy) {
+ if (!empty($this->forcedInnerGroupBy)) {
+ $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $tables, $this->forcedInnerGroupBy);
+ } elseif ($useSpecialConversionGroupBy) {
$innerGroupBy = "CONCAT(log_conversion.idvisit, '_' , log_conversion.idgoal, '_', log_conversion.buster)";
- $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $innerGroupBy);
+ $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $tables, $innerGroupBy);
} elseif ($joinWithSubSelect) {
- $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset);
+ $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $tables);
} else {
$sql = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset);
}
@@ -91,9 +108,20 @@ class LogQueryBuilder
* @throws Exception
* @return string
*/
- private function buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, $innerGroupBy = null)
+ private function buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limitAndOffset, JoinTables $tables, $innerGroupBy = null)
{
- $matchTables = '(' . implode('|', $this->getKnownTables()) . ')';
+ $matchTables = $this->getKnownTables();
+ foreach ($tables as $table) {
+ if (is_array($table) && isset($table['tableAlias']) && !in_array($table['tableAlias'], $matchTables, $strict = true)) {
+ $matchTables[] = $table['tableAlias'];
+ } elseif (is_array($table) && isset($table['table']) && !in_array($table['table'], $matchTables, $strict = true)) {
+ $matchTables[] = $table['table'];
+ } elseif (is_string($table) && !in_array($table, $matchTables, $strict = true)) {
+ $matchTables[] = $table;
+ }
+ }
+
+ $matchTables = '(' . implode('|', $matchTables) . ')';
preg_match_all("/". $matchTables ."\.[a-z0-9_\*]+/", $select, $matches);
$neededFields = array_unique($matches[0]);
@@ -102,6 +130,30 @@ class LogQueryBuilder
. "Please use a table prefix.");
}
+ $fieldNames = array();
+ $toBeReplaced = array();
+ $epregReplace = array();
+ foreach ($neededFields as &$neededField) {
+ $parts = explode('.', $neededField);
+ if (count($parts) === 2 && !empty($parts[1])) {
+ if (in_array($parts[1], $fieldNames, $strict = true)) {
+ // eg when selecting 2 dimensions log_action_X.name
+ $columnAs = $parts[1] . md5($neededField);
+ $fieldNames[] = $columnAs;
+ // we make sure to not replace a idvisitor column when duplicate column is idvisit
+ $toBeReplaced[$neededField . ' '] = $parts[0] . '.' . $columnAs . ' ';
+ $toBeReplaced[$neededField . ')'] = $parts[0] . '.' . $columnAs . ')';
+ $toBeReplaced[$neededField . '`'] = $parts[0] . '.' . $columnAs . '`';
+ $toBeReplaced[$neededField . ','] = $parts[0] . '.' . $columnAs . ',';
+ // replace when string ends this, we need to use regex to check for this
+ $epregReplace["/(" . $neededField . ")$/"] = $parts[0] . '.' . $columnAs;
+ $neededField .= ' as ' . $columnAs;
+ } else {
+ $fieldNames[] = $parts[1];
+ }
+ }
+ }
+
preg_match_all("/". $matchTables . "/", $from, $matchesFrom);
$innerSelect = implode(", \n", $neededFields);
@@ -110,12 +162,6 @@ class LogQueryBuilder
$innerLimitAndOffset = $limitAndOffset;
- if (!isset($innerGroupBy) && in_array('log_visit', $matchesFrom[1])) {
- $innerGroupBy = "log_visit.idvisit";
- } elseif (!isset($innerGroupBy)) {
- throw new Exception('Cannot use subselect for join as no group by rule is specified');
- }
-
$innerOrderBy = "NULL";
if ($innerLimitAndOffset && $orderBy) {
// only When LIMITing we can apply to the inner query the same ORDER BY as the parent query
@@ -126,9 +172,29 @@ class LogQueryBuilder
$innerGroupBy = false;
}
+ if (!isset($innerGroupBy) && in_array('log_visit', $matchesFrom[1])) {
+ $innerGroupBy = "log_visit.idvisit";
+ } elseif (!isset($innerGroupBy)) {
+ throw new Exception('Cannot use subselect for join as no group by rule is specified');
+ }
+
+ if (!empty($toBeReplaced)) {
+ $select = preg_replace(array_keys($epregReplace), array_values($epregReplace), $select);
+ $select = str_replace(array_keys($toBeReplaced), array_values($toBeReplaced), $select);
+ if (!empty($groupBy)) {
+ $groupBy = preg_replace(array_keys($epregReplace), array_values($epregReplace), $groupBy);
+ $groupBy = str_replace(array_keys($toBeReplaced), array_values($toBeReplaced), $groupBy);
+ }
+ if (!empty($orderBy)) {
+ $orderBy = preg_replace(array_keys($epregReplace), array_values($epregReplace), $orderBy);
+ $orderBy = str_replace(array_keys($toBeReplaced), array_values($toBeReplaced), $orderBy);
+ }
+ }
+
$innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerGroupBy, $innerOrderBy, $innerLimitAndOffset);
$select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select);
+
$from = "
(
$innerQuery
diff --git a/core/DataAccess/LogQueryBuilder/JoinGenerator.php b/core/DataAccess/LogQueryBuilder/JoinGenerator.php
index a7f8b5499f..82cffc6b65 100644
--- a/core/DataAccess/LogQueryBuilder/JoinGenerator.php
+++ b/core/DataAccess/LogQueryBuilder/JoinGenerator.php
@@ -120,6 +120,11 @@ class JoinGenerator
$this->joinString .= ' LEFT JOIN';
}
+ if (!isset($table['joinOn']) && $this->tables->getLogTable($table['table']) && !empty($availableLogTables)) {
+ $logTable = $this->tables->getLogTable($table['table']);
+ $table['joinOn'] = $this->findJoinCriteriasForTables($logTable, $availableLogTables);
+ }
+
$this->joinString .= ' ' . Common::prefixTable($table['table']) . " AS " . $alias
. " ON " . $table['joinOn'];
continue;
@@ -166,7 +171,7 @@ class JoinGenerator
* to be joined
* @throws Exception if table cannot be joined for segmentation
*/
- protected function findJoinCriteriasForTables(LogTable $logTable, $availableLogTables)
+ public function findJoinCriteriasForTables(LogTable $logTable, $availableLogTables)
{
$join = null;
$alternativeJoin = null;
@@ -278,10 +283,16 @@ class JoinGenerator
}
if (is_array($tA)) {
+ if (isset($tA['joinOn']) && is_string($tA['joinOn']) && strpos($tA['joinOn'] . '.', $tB) === 0) {
+ return 1; // tA requires tB so needs to be listed before
+ }
return -1;
}
if (is_array($tB)) {
+ if (isset($tB['joinOn']) && is_string($tB['joinOn']) && strpos($tB['joinOn'] . '.', $tA) === 0) {
+ return -1; // tB requires tA so needs to be listed before
+ }
return 1;
}
diff --git a/core/DataAccess/LogQueryBuilder/JoinTables.php b/core/DataAccess/LogQueryBuilder/JoinTables.php
index c773750155..c05a333d1d 100644
--- a/core/DataAccess/LogQueryBuilder/JoinTables.php
+++ b/core/DataAccess/LogQueryBuilder/JoinTables.php
@@ -48,7 +48,22 @@ class JoinTables extends \ArrayObject
public function hasJoinedTable($tableName)
{
- return in_array($tableName, $this->getTables());
+ $tables = in_array($tableName, $this->getTables());
+ if ($tables) {
+ return true;
+ }
+
+ foreach ($this as $table) {
+ if (is_array($table)) {
+ if (!isset($table['tableAlias']) && $table['table'] === $table) {
+ return true;
+ } elseif (isset($table['tableAlias']) && $table['tableAlias'] === $table) {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
public function hasJoinedTableManually($tableToFind, $joinToFind)
diff --git a/core/Metrics/Formatter.php b/core/Metrics/Formatter.php
index 9ce179d769..6608b802e3 100644
--- a/core/Metrics/Formatter.php
+++ b/core/Metrics/Formatter.php
@@ -11,6 +11,7 @@ use Piwik\Common;
use Piwik\DataTable;
use Piwik\NumberFormatter;
use Piwik\Piwik;
+use Piwik\Plugin\ArchivedMetric;
use Piwik\Plugin\Metric;
use Piwik\Plugin\ProcessedMetric;
use Piwik\Plugin\Report;
@@ -214,7 +215,8 @@ class Formatter
if ($metricsToFormat !== null) {
$metricMatchRegex = $this->makeRegexToMatchMetrics($metricsToFormat);
- $metrics = array_filter($metrics, function (ProcessedMetric $metric) use ($metricMatchRegex) {
+ $metrics = array_filter($metrics, function ($metric) use ($metricMatchRegex) {
+ /** @var ProcessedMetric|ArchivedMetric $metric */
return preg_match($metricMatchRegex, $metric->getName());
});
}
diff --git a/core/Plugin/ArchivedMetric.php b/core/Plugin/ArchivedMetric.php
new file mode 100644
index 0000000000..05fa3d8665
--- /dev/null
+++ b/core/Plugin/ArchivedMetric.php
@@ -0,0 +1,207 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugin;
+
+use Piwik\Archive\DataTableFactory;
+use Piwik\Columns\Dimension;
+use Piwik\Common;
+use Piwik\DataTable;
+use Piwik\DataTable\Row;
+use Piwik\Metrics\Formatter;
+use Piwik\Piwik;
+
+class ArchivedMetric extends Metric
+{
+ const AGGREGATION_COUNT = 'count(%s)';
+ const AGGREGATION_COUNT_PREFIX = 'nb_';
+ const AGGREGATION_SUM = 'sum(%s)';
+ const AGGREGATION_SUM_PREFIX = 'sum_';
+ const AGGREGATION_MAX = 'max(%s)';
+ const AGGREGATION_MAX_PREFIX = 'max_';
+ const AGGREGATION_MIN = 'min(%s)';
+ const AGGREGATION_MIN_PREFIX = 'min_';
+ const AGGREGATION_UNIQUE = 'count(distinct %s)';
+ const AGGREGATION_UNIQUE_PREFIX = 'nb_uniq_';
+ const AGGREGATION_COUNT_WITH_NUMERIC_VALUE = 'sum(if(%s > 0, 1, 0))';
+ const AGGREGATION_COUNT_WITH_NUMERIC_VALUE_PREFIX = 'nb_with_';
+
+ /**
+ * @var string
+ */
+ private $aggregation;
+
+ /**
+ * @var int
+ */
+ protected $idSite;
+
+ private $name = '';
+ private $type = '';
+ private $translatedName = '';
+ private $documentation = '';
+ private $dbTable = '';
+ private $category = '';
+ private $query = '';
+
+ /**
+ * @var Dimension
+ */
+ private $dimension;
+
+ public function __construct(Dimension $dimension, $aggregation = false)
+ {
+ if (!empty($aggregation) && strpos($aggregation, '%s') === false) {
+ throw new \Exception(sprintf('The given aggregation for %s.%s needs to include a %%s for the column name', $dimension->getDbTableName(), $dimension->getColumnName()));
+ }
+
+ $this->setDimension($dimension);
+ $this->setDbTable($dimension->getDbTableName());
+ $this->aggregation = $aggregation;
+ }
+
+ public function setDimension($dimension)
+ {
+ $this->dimension = $dimension;
+ return $this;
+ }
+
+ public function getDimension()
+ {
+ return $this->dimension;
+ }
+
+ public function setCategory($category)
+ {
+ $this->category = $category;
+ return $this;
+ }
+
+ public function getCategoryId()
+ {
+ return $this->category;
+ }
+
+ public function setDbTable($dbTable)
+ {
+ $this->dbTable = $dbTable;
+ return $this;
+ }
+
+ public function setDocumentation($documentation)
+ {
+ $this->documentation = $documentation;
+ return $this;
+ }
+
+ public function setTranslatedName($name)
+ {
+ $this->translatedName = $name;
+ return $this;
+ }
+
+ public function setType($type)
+ {
+ $this->type = $type;
+ return $this;
+ }
+
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function setQuery($query)
+ {
+ $this->query = $query;
+ return $this;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function format($value, Formatter $formatter)
+ {
+ switch ($this->type) {
+ case Dimension::TYPE_BOOL:
+ return $formatter->getPrettyNumber($value);
+ case Dimension::TYPE_ENUM:
+ return $formatter->getPrettyNumber($value);
+ case Dimension::TYPE_MONEY:
+ return $formatter->getPrettyMoney($value, $this->idSite);
+ case Dimension::TYPE_FLOAT:
+ return $formatter->getPrettyNumber((float) $value, $precision = 2);
+ case Dimension::TYPE_NUMBER:
+ return $formatter->getPrettyNumber($value);
+ case Dimension::TYPE_DURATION_S:
+ return $formatter->getPrettyTimeFromSeconds($value, $displayAsSentence = true);
+ case Dimension::TYPE_DURATION_MS:
+ $val = number_format($value / 1000, 2);
+ if ($val > 60) {
+ $val = round($val);
+ }
+ return $formatter->getPrettyTimeFromSeconds($val, $displayAsSentence = true);
+ case Dimension::TYPE_PERCENT:
+ return $formatter->getPrettyPercentFromQuotient($value);
+ case Dimension::TYPE_BYTE:
+ return $formatter->getPrettySizeFromBytes($value);
+ }
+
+ return $value;
+ }
+
+ public function getTranslatedName()
+ {
+ if (!empty($this->translatedName)) {
+ return Piwik::translate($this->translatedName);
+ }
+
+ return $this->translatedName;
+ }
+
+ public function getDependentMetrics()
+ {
+ return array($this->getName());
+ }
+
+ public function getDocumentation()
+ {
+ return $this->documentation;
+ }
+
+ public function getDbTableName()
+ {
+ return $this->dbTable;
+ }
+
+ public function getQuery()
+ {
+ if ($this->query) {
+ return $this->query;
+ }
+
+ $column = $this->dbTable . '.' . $this->dimension->getColumnName();
+
+ if (!empty($this->aggregation)) {
+ return sprintf($this->aggregation, $column);
+ }
+
+ return $column;
+ }
+
+ public function beforeFormat($report, DataTable $table)
+ {
+ $this->idSite = DataTableFactory::getSiteIdFromMetadata($table);
+ if (empty($this->idSite)) {
+ $this->idSite = Common::getRequestVar('idSite', 0, 'int');
+ }
+ return !empty($this->idSite); // skip formatting if there is no site to get currency info from
+ }
+}
diff --git a/core/Plugin/ComputedMetric.php b/core/Plugin/ComputedMetric.php
new file mode 100644
index 0000000000..7825fedde3
--- /dev/null
+++ b/core/Plugin/ComputedMetric.php
@@ -0,0 +1,260 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+namespace Piwik\Plugin;
+
+use Piwik\Archive\DataTableFactory;
+use Piwik\Columns\Dimension;
+use Piwik\Columns\MetricsList;
+use Piwik\Common;
+use Piwik\DataTable;
+use Piwik\DataTable\Row;
+use Piwik\Metrics\Formatter;
+use Piwik\Piwik;
+
+class ComputedMetric extends ProcessedMetric
+{
+ const AGGREGATION_AVG = 'avg';
+ const AGGREGATION_RATE = 'rate';
+
+ /**
+ * @var string
+ */
+ private $aggregation;
+
+ /**
+ * @var int
+ */
+ protected $idSite;
+
+ private $name = '';
+ private $type = '';
+ private $translatedName = '';
+ private $documentation = '';
+ private $metric1 = '';
+ private $metric2 = '';
+ private $category = '';
+
+ private $metricsList;
+
+ public function __construct($metric1, $metric2, $aggregation = 'avg')
+ {
+ $nameShort1 = str_replace(array('nb_'), '', $metric1);
+ $nameShort2 = str_replace(array('nb_'), '', $metric2);
+
+ if ($aggregation === ComputedMetric::AGGREGATION_AVG) {
+ $this->name = 'avg_' . $nameShort1 . '_per_' . $nameShort2;
+ } elseif ($aggregation === ComputedMetric::AGGREGATION_RATE) {
+ $this->name = $nameShort1 . '_' . $nameShort2 . '_rate';
+ } else {
+ throw new \Exception('Not supported aggregation type');
+ }
+
+ $this->setMetric1($metric1);
+ $this->setMetric2($metric2);
+ $this->aggregation = $aggregation;
+ }
+
+ public function getDependentMetrics()
+ {
+ return array($this->metric1, $this->metric2);
+ }
+
+ public function setCategory($category)
+ {
+ $this->category = $category;
+ return $this;
+ }
+
+ public function getCategoryId()
+ {
+ return $this->category;
+ }
+
+ public function setMetric1($metric1)
+ {
+ $this->metric1 = $metric1;
+ return $this;
+ }
+
+ public function setMetric2($metric2)
+ {
+ $this->metric2 = $metric2;
+ return $this;
+ }
+
+ public function setDocumentation($documentation)
+ {
+ $this->documentation = $documentation;
+ return $this;
+ }
+
+ public function setTranslatedName($name)
+ {
+ $this->translatedName = $name;
+ return $this;
+ }
+
+ public function setType($type)
+ {
+ $this->type = $type;
+ return $this;
+ }
+
+ public function setName($name)
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function compute(Row $row)
+ {
+ $metric1 = $this->getMetric($row, $this->metric1);
+ $metric2 = $this->getMetric($row, $this->metric2);
+
+ return Piwik::getQuotientSafe($metric1, $metric2, $precision = 2);
+ }
+
+ private function getDetectedType()
+ {
+ if (!$this->type) {
+ if ($this->aggregation === self::AGGREGATION_RATE) {
+ $this->type = Dimension::TYPE_PERCENT;
+ } else {
+ $this->type = Dimension::TYPE_NUMBER; // default to number
+ $metric1 = $this->getMetricsList()->getMetric($this->metric1);
+ if ($metric1) {
+ $dimension = $metric1->getDimension();
+ if ($dimension) {
+ $this->type = $dimension->getType();
+ }
+ }
+ }
+ }
+
+ return $this->type;
+ }
+
+ public function format($value, Formatter $formatter)
+ {
+ if ($this->aggregation === self::AGGREGATION_RATE) {
+ return $formatter->getPrettyPercentFromQuotient($value);
+ }
+
+ $type = $this->getDetectedType();
+
+ switch ($type) {
+ case Dimension::TYPE_MONEY:
+ return $formatter->getPrettyMoney($value, $this->idSite);
+ case Dimension::TYPE_FLOAT:
+ return $formatter->getPrettyNumber($value, $precision = 2);
+ case Dimension::TYPE_NUMBER:
+ return $formatter->getPrettyNumber($value, 1); // we still need to round to have somewhat more accurate result
+ case Dimension::TYPE_DURATION_S:
+ return $formatter->getPrettyTimeFromSeconds(round($value), $displayAsSentence = true);
+ case Dimension::TYPE_DURATION_MS:
+ $val = number_format($value / 1000, 2);
+ if ($val > 60) {
+ $val = round($val);
+ }
+ return $formatter->getPrettyTimeFromSeconds($val, $displayAsSentence = true);
+ case Dimension::TYPE_PERCENT:
+ return $formatter->getPrettyPercentFromQuotient($value);
+ case Dimension::TYPE_BYTE:
+ return $formatter->getPrettySizeFromBytes($value);
+ }
+
+ return $value;
+ }
+
+ public function getTranslatedName()
+ {
+ if (!$this->translatedName) {
+ $metric = $this->getMetricsList();
+ $metric1 = $metric->getMetric($this->metric1);
+ $metric2 = $metric->getMetric($this->metric2);
+
+ if ($this->aggregation === self::AGGREGATION_AVG) {
+ if ($metric1 && $metric1 instanceof ArchivedMetric && $metric2 && $metric2 instanceof ArchivedMetric) {
+
+ $metric1Name = $metric1->getDimension()->getName();
+ $metric2Name = $metric2->getDimension()->getName();
+ return Piwik::translate('General_ComputedMetricAverage', array($metric1Name, $metric2Name));
+ }
+
+ if ($metric1 && $metric1 instanceof ArchivedMetric) {
+ $metric1Name = $metric1->getDimension()->getName();
+ return Piwik::translate('General_AverageX', array($metric1Name));
+ }
+
+ if ($metric1 && $metric2) {
+ return $metric1->getTranslatedName() . ' per ' . $metric2->getTranslatedName();
+ }
+
+ return $this->metric1 . ' per ' . $this->metric2;
+ } else if ($this->aggregation === self::AGGREGATION_RATE) {
+ if ($metric1 && $metric1 instanceof ArchivedMetric) {
+ return Piwik::translate('General_ComputedMetricRate', array($metric1->getTranslatedName()));
+ } else {
+ return Piwik::translate('General_ComputedMetricRate', array($this->metric1));
+ }
+ }
+ }
+ return $this->translatedName;
+ }
+
+ public function getDocumentation()
+ {
+ if (!$this->documentation) {
+ $metric = $this->getMetricsList();
+ $metric1 = $metric->getMetric($this->metric1);
+ $metric2 = $metric->getMetric($this->metric2);
+
+ if ($this->aggregation === self::AGGREGATION_AVG) {
+ if ($metric1 && $metric1 instanceof ArchivedMetric && $metric2 && $metric2 instanceof ArchivedMetric) {
+ return Piwik::translate('General_ComputedMetricAverageDocumentation', array($metric1->getDimension()->getName(), $metric2->getTranslatedName()));
+ }
+
+ if ($metric1 && $metric1 instanceof ArchivedMetric) {
+ return Piwik::translate('General_ComputedMetricAverageShortDocumentation', array($metric1->getDimension()->getName()));
+ }
+
+ return Piwik::translate('General_ComputedMetricAverageDocumentation', array($this->metric1, $this->metric2));
+
+ } else if ($this->aggregation === self::AGGREGATION_RATE) {
+ if ($metric1 && $metric1 instanceof ArchivedMetric) {
+ return Piwik::translate('General_ComputedMetricRateDocumentation', array($metric1->getDimension()->getNamePlural(), $metric2->getDimension()->getNamePlural()));
+ } else {
+ return Piwik::translate('General_ComputedMetricRateShortDocumentation', array($this->metric1));
+ }
+ }
+ }
+ return $this->documentation;
+ }
+
+ private function getMetricsList()
+ {
+ if (!$this->metricsList) {
+ $this->metricsList = MetricsList::get();
+ }
+ return $this->metricsList;
+ }
+
+ public function beforeFormat($report, DataTable $table)
+ {
+ $this->idSite = DataTableFactory::getSiteIdFromMetadata($table);
+ if (empty($this->idSite)) {
+ $this->idSite = Common::getRequestVar('idSite', 0, 'int');
+ }
+ return !empty($this->idSite); // skip formatting if there is no site to get currency info from
+ }
+}
diff --git a/core/Plugin/Dimension/ActionDimension.php b/core/Plugin/Dimension/ActionDimension.php
index ce8a1eeda2..4ed4867448 100644
--- a/core/Plugin/Dimension/ActionDimension.php
+++ b/core/Plugin/Dimension/ActionDimension.php
@@ -12,10 +12,7 @@ use Piwik\CacheId;
use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Plugin\Manager as PluginManager;
-use Piwik\Plugin\Segment;
-use Piwik\Common;
use Piwik\Plugin;
-use Piwik\Db;
use Piwik\Tracker\Action;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
@@ -38,111 +35,8 @@ abstract class ActionDimension extends Dimension
{
const INSTALLER_PREFIX = 'log_link_visit_action.';
- private $tableName = 'log_link_visit_action';
-
- /**
- * Installs the action dimension in case it is not installed yet. The installation is already implemented based on
- * the {@link $columnName} and {@link $columnType}. If you want to perform additional actions beside adding the
- * column to the database - for instance adding an index - you can overwrite this method. We recommend to call
- * this parent method to get the minimum required actions and then add further custom actions since this makes sure
- * the column will be installed correctly. We also recommend to change the default install behavior only if really
- * needed. FYI: We do not directly execute those alter table statements here as we group them together with several
- * other alter table statements do execute those changes in one step which results in a faster installation. The
- * column will be added to the `log_link_visit_action` MySQL table.
- *
- * Example:
- * ```
- public function install()
- {
- $changes = parent::install();
- $changes['log_link_visit_action'][] = "ADD INDEX index_idsite_servertime ( idsite, server_time )";
-
- return $changes;
- }
- ```
- *
- * @return array An array containing the table name as key and an array of MySQL alter table statements that should
- * be executed on the given table. Example:
- * ```
- array(
- 'log_link_visit_action' => array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...")
- );
- ```
- * @api
- */
- public function install()
- {
- if (empty($this->columnName) || empty($this->columnType)) {
- return array();
- }
-
- return array(
- $this->tableName => array("ADD COLUMN `$this->columnName` $this->columnType")
- );
- }
-
- /**
- * Updates the action dimension in case the {@link $columnType} has changed. The update is already implemented based
- * on the {@link $columnName} and {@link $columnType}. This method is intended not to overwritten by plugin
- * developers as it is only supposed to make sure the column has the correct type. Adding additional custom "alter
- * table" actions would not really work since they would be executed with every {@link $columnType} change. So
- * adding an index here would be executed whenever the columnType changes resulting in an error if the index already
- * exists. If an index needs to be added after the first version is released a plugin update class should be
- * created since this makes sure it is only executed once.
- *
- * @return array An array containing the table name as key and an array of MySQL alter table statements that should
- * be executed on the given table. Example:
- * ```
- array(
- 'log_link_visit_action' => array("MODIFY COLUMN `$this->columnName` $this->columnType", "DROP COLUMN ...")
- );
- ```
- * @ignore
- */
- public function update()
- {
- if (empty($this->columnName) || empty($this->columnType)) {
- return array();
- }
-
- return array(
- $this->tableName => array("MODIFY COLUMN `$this->columnName` $this->columnType")
- );
- }
-
- /**
- * Uninstalls the dimension if a {@link $columnName} and {@link columnType} is set. In case you perform any custom
- * actions during {@link install()} - for instance adding an index - you should make sure to undo those actions by
- * overwriting this method. Make sure to call this parent method to make sure the uninstallation of the column
- * will be done.
- * @throws Exception
- * @api
- */
- public function uninstall()
- {
- if (empty($this->columnName) || empty($this->columnType)) {
- return;
- }
-
- try {
- $sql = "ALTER TABLE `" . Common::prefixTable($this->tableName) . "` DROP COLUMN `$this->columnName`";
- Db::exec($sql);
- } catch (Exception $e) {
- if (!Db::get()->isErrNo($e, '1091')) {
- throw $e;
- }
- }
- }
-
- /**
- * Get the version of the dimension which is used for update checks.
- * @return string
- * @ignore
- */
- public function getVersion()
- {
- return $this->columnType;
- }
+ protected $dbTableName = 'log_link_visit_action';
+ protected $category = 'General_Actions';
/**
* If the value you want to save for your dimension is something like a page title or page url, you usually do not
@@ -192,23 +86,6 @@ abstract class ActionDimension extends Dimension
}
/**
- * Adds a new segment. It automatically sets the SQL segment depending on the column name in case none is set
- * already.
- * @see \Piwik\Columns\Dimension::addSegment()
- * @param Segment $segment
- * @api
- */
- protected function addSegment(Segment $segment)
- {
- $sqlSegment = $segment->getSqlSegment();
- if (!empty($this->columnName) && empty($sqlSegment)) {
- $segment->setSqlSegment($this->tableName . '.' . $this->columnName);
- }
-
- parent::addSegment($segment);
- }
-
- /**
* Get all action dimensions that are defined by all activated plugins.
* @return ActionDimension[]
* @ignore
diff --git a/core/Plugin/Dimension/ConversionDimension.php b/core/Plugin/Dimension/ConversionDimension.php
index ff1bff55ae..14a242d24f 100644
--- a/core/Plugin/Dimension/ConversionDimension.php
+++ b/core/Plugin/Dimension/ConversionDimension.php
@@ -12,15 +12,11 @@ use Piwik\CacheId;
use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Plugin\Manager as PluginManager;
-use Piwik\Common;
-use Piwik\Db;
use Piwik\Tracker\Action;
use Piwik\Tracker\GoalManager;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
-use Piwik\Plugin\Segment;
use Piwik\Plugin;
-use Exception;
/**
* Defines a new conversion dimension that records any visit related information during tracking.
@@ -40,116 +36,8 @@ abstract class ConversionDimension extends Dimension
{
const INSTALLER_PREFIX = 'log_conversion.';
- private $tableName = 'log_conversion';
-
- /**
- * Installs the conversion dimension in case it is not installed yet. The installation is already implemented based
- * on the {@link $columnName} and {@link $columnType}. If you want to perform additional actions beside adding the
- * column to the database - for instance adding an index - you can overwrite this method. We recommend to call
- * this parent method to get the minimum required actions and then add further custom actions since this makes sure
- * the column will be installed correctly. We also recommend to change the default install behavior only if really
- * needed. FYI: We do not directly execute those alter table statements here as we group them together with several
- * other alter table statements do execute those changes in one step which results in a faster installation. The
- * column will be added to the `log_conversion` MySQL table.
- *
- * Example:
- * ```
- public function install()
- {
- $changes = parent::install();
- $changes['log_conversion'][] = "ADD INDEX index_idsite_servertime ( idsite, server_time )";
-
- return $changes;
- }
- ```
- *
- * @return array An array containing the table name as key and an array of MySQL alter table statements that should
- * be executed on the given table. Example:
- * ```
- array(
- 'log_conversion' => array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...")
- );
- ```
- * @api
- */
- public function install()
- {
- if (empty($this->columnName) || empty($this->columnType)) {
- return array();
- }
-
- return array(
- $this->tableName => array("ADD COLUMN `$this->columnName` $this->columnType")
- );
- }
-
- /**
- * @see ActionDimension::update()
- * @return array
- * @ignore
- */
- public function update()
- {
- if (empty($this->columnName) || empty($this->columnType)) {
- return array();
- }
-
- return array(
- $this->tableName => array("MODIFY COLUMN `$this->columnName` $this->columnType")
- );
- }
-
- /**
- * Uninstalls the dimension if a {@link $columnName} and {@link columnType} is set. In case you perform any custom
- * actions during {@link install()} - for instance adding an index - you should make sure to undo those actions by
- * overwriting this method. Make sure to call this parent method to make sure the uninstallation of the column
- * will be done.
- * @throws Exception
- * @api
- */
- public function uninstall()
- {
- if (empty($this->columnName) || empty($this->columnType)) {
- return;
- }
-
- try {
- $sql = "ALTER TABLE `" . Common::prefixTable($this->tableName) . "` DROP COLUMN `$this->columnName`";
- Db::exec($sql);
- } catch (Exception $e) {
- if (!Db::get()->isErrNo($e, '1091')) {
- throw $e;
- }
- }
- }
-
- /**
- * @see ActionDimension::getVersion()
- * @return string
- * @ignore
- */
- public function getVersion()
- {
- return $this->columnType;
- }
-
- /**
- * Adds a new segment. It automatically sets the SQL segment depending on the column name in case none is set
- * already.
- *
- * @see \Piwik\Columns\Dimension::addSegment()
- * @param Segment $segment
- * @api
- */
- protected function addSegment(Segment $segment)
- {
- $sqlSegment = $segment->getSqlSegment();
- if (!empty($this->columnName) && empty($sqlSegment)) {
- $segment->setSqlSegment($this->tableName . '.' . $this->columnName);
- }
-
- parent::addSegment($segment);
- }
+ protected $dbTableName = 'log_conversion';
+ protected $category = 'Goals_Conversion';
/**
* Get all conversion dimensions that are defined by all activated plugins.
diff --git a/core/Plugin/Dimension/VisitDimension.php b/core/Plugin/Dimension/VisitDimension.php
index c59495d2aa..f6e3f19205 100644
--- a/core/Plugin/Dimension/VisitDimension.php
+++ b/core/Plugin/Dimension/VisitDimension.php
@@ -13,12 +13,11 @@ use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Common;
use Piwik\Db;
+use Piwik\DbHelper;
use Piwik\Plugin\Manager as PluginManager;
-use Piwik\Plugin\Segment;
use Piwik\Tracker\Request;
use Piwik\Tracker\Visitor;
use Piwik\Tracker\Action;
-use Piwik\Tracker;
use Piwik\Plugin;
use Exception;
@@ -39,38 +38,9 @@ abstract class VisitDimension extends Dimension
{
const INSTALLER_PREFIX = 'log_visit.';
- private $tableName = 'log_visit';
+ protected $dbTableName = 'log_visit';
+ protected $category = 'General_Visitors';
- /**
- * Installs the visit dimension in case it is not installed yet. The installation is already implemented based on
- * the {@link $columnName} and {@link $columnType}. If you want to perform additional actions beside adding the
- * column to the database - for instance adding an index - you can overwrite this method. We recommend to call
- * this parent method to get the minimum required actions and then add further custom actions since this makes sure
- * the column will be installed correctly. We also recommend to change the default install behavior only if really
- * needed. FYI: We do not directly execute those alter table statements here as we group them together with several
- * other alter table statements do execute those changes in one step which results in a faster installation. The
- * column will be added to the `log_visit` MySQL table.
- *
- * Example:
- * ```
- public function install()
- {
- $changes = parent::install();
- $changes['log_visit'][] = "ADD INDEX index_idsite_servertime ( idsite, server_time )";
-
- return $changes;
- }
- ```
- *
- * @return array An array containing the table name as key and an array of MySQL alter table statements that should
- * be executed on the given table. Example:
- * ```
- array(
- 'log_visit' => array("ADD COLUMN `$this->columnName` $this->columnType", "ADD INDEX ...")
- );
- ```
- * @api
- */
public function install()
{
if (empty($this->columnType) || empty($this->columnName)) {
@@ -78,7 +48,7 @@ abstract class VisitDimension extends Dimension
}
$changes = array(
- $this->tableName => array("ADD COLUMN `$this->columnName` $this->columnType")
+ $this->dbTableName => array("ADD COLUMN `$this->columnName` $this->columnType")
);
if ($this->isHandlingLogConversion()) {
@@ -90,19 +60,20 @@ abstract class VisitDimension extends Dimension
/**
* @see ActionDimension::update()
- * @param array $conversionColumns An array of currently installed columns in the conversion table.
* @return array
* @ignore
*/
- public function update($conversionColumns)
+ public function update()
{
if (!$this->columnType) {
return array();
}
+ $conversionColumns = DbHelper::getTableColumns(Common::prefixTable('log_conversion'));
+
$changes = array();
- $changes[$this->tableName] = array("MODIFY COLUMN `$this->columnName` $this->columnType");
+ $changes[$this->dbTableName] = array("MODIFY COLUMN `$this->columnName` $this->columnType");
$handlingConversion = $this->isHandlingLogConversion();
$hasConversionColumn = array_key_exists($this->columnName, $conversionColumns);
@@ -119,7 +90,6 @@ abstract class VisitDimension extends Dimension
}
/**
- * @see ActionDimension::getVersion()
* @return string
* @ignore
*/
@@ -152,7 +122,7 @@ abstract class VisitDimension extends Dimension
}
try {
- $sql = "ALTER TABLE `" . Common::prefixTable($this->tableName) . "` DROP COLUMN `$this->columnName`";
+ $sql = "ALTER TABLE `" . Common::prefixTable($this->dbTableName) . "` DROP COLUMN `$this->columnName`";
Db::exec($sql);
} catch (Exception $e) {
if (!Db::get()->isErrNo($e, '1091')) {
@@ -175,23 +145,6 @@ abstract class VisitDimension extends Dimension
}
/**
- * Adds a new segment. It automatically sets the SQL segment depending on the column name in case none is set
- * already.
- * @see \Piwik\Columns\Dimension::addSegment()
- * @param Segment $segment
- * @api
- */
- protected function addSegment(Segment $segment)
- {
- $sqlSegment = $segment->getSqlSegment();
- if (!empty($this->columnName) && empty($sqlSegment)) {
- $segment->setSqlSegment('log_visit.' . $this->columnName);
- }
-
- parent::addSegment($segment);
- }
-
- /**
* Sometimes you may want to make sure another dimension is executed before your dimension so you can persist
* this dimensions' value depending on the value of other dimensions. You can do this by defining an array of
* dimension names. If you access any value of any other column within your events, you should require them here.
diff --git a/core/Plugin/Metric.php b/core/Plugin/Metric.php
index e1bdb78cad..464575b6c0 100644
--- a/core/Plugin/Metric.php
+++ b/core/Plugin/Metric.php
@@ -54,6 +54,16 @@ abstract class Metric
abstract public function getTranslatedName();
/**
+ * Returns the category that this metric belongs to.
+ * @return string
+ * @api since Piwik 3.2.0
+ */
+ public function getCategoryId()
+ {
+ return '';
+ }
+
+ /**
* Returns a string describing what the metric represents. The result will be included in report metadata
* API output, including processed reports.
*
diff --git a/core/Plugin/Report.php b/core/Plugin/Report.php
index 24887fb4c0..c300ad11a3 100644
--- a/core/Plugin/Report.php
+++ b/core/Plugin/Report.php
@@ -410,6 +410,7 @@ class Report
if (empty($restrictToColumns)) {
$restrictToColumns = array_merge($allMetrics, array_keys($this->getProcessedMetrics()));
}
+ $restrictToColumns = array_unique($restrictToColumns);
$processedMetricsById = $this->getProcessedMetricsById();
$metricsSet = array_flip($allMetrics);
@@ -860,7 +861,7 @@ class Report
$result = array();
foreach ($processedMetrics as $processedMetric) {
- if ($processedMetric instanceof ProcessedMetric) { // instanceof check for backwards compatibility
+ if ($processedMetric instanceof ProcessedMetric || $processedMetric instanceof ArchivedMetric) { // instanceof check for backwards compatibility
$result[$processedMetric->getName()] = $processedMetric;
}
}
diff --git a/core/Plugin/Segment.php b/core/Plugin/Segment.php
index 2a20208ea2..494e562d86 100644
--- a/core/Plugin/Segment.php
+++ b/core/Plugin/Segment.php
@@ -203,6 +203,33 @@ class Segment
}
/**
+ * @return string
+ * @ignore
+ */
+ public function getSqlFilterValue()
+ {
+ return $this->sqlFilterValue;
+ }
+
+ /**
+ * @return string
+ * @ignore
+ */
+ public function getAcceptValues()
+ {
+ return $this->acceptValues;
+ }
+
+ /**
+ * @return string
+ * @ignore
+ */
+ public function getSqlFilter()
+ {
+ return $this->sqlFilter;
+ }
+
+ /**
* Set (overwrite) the type of this segment which is usually either a 'dimension' or a 'metric'.
* @param string $type See constansts TYPE_*
* @api
@@ -231,6 +258,15 @@ class Segment
}
/**
+ * @return string
+ * @ignore
+ */
+ public function getCategoryId()
+ {
+ return $this->category;
+ }
+
+ /**
* Returns the name of this segment as it should appear in segment expressions.
*
* @return string
@@ -241,6 +277,15 @@ class Segment
}
/**
+ * @return string
+ * @ignore
+ */
+ public function getSuggestedValuesCallback()
+ {
+ return $this->suggestedValuesCallback;
+ }
+
+ /**
* Set callback which will be executed when user will call for suggested values for segment.
*
* @param callable $suggestedValuesCallback
diff --git a/core/Plugin/Visualization.php b/core/Plugin/Visualization.php
index 6ac18ea67e..6bb2c3a4f8 100644
--- a/core/Plugin/Visualization.php
+++ b/core/Plugin/Visualization.php
@@ -13,6 +13,7 @@ use Piwik\API\DataTablePostProcessor;
use Piwik\API\Proxy;
use Piwik\API\ResponseBuilder;
use Piwik\Common;
+use Piwik\Container\StaticContainer;
use Piwik\DataTable;
use Piwik\Date;
use Piwik\Log;
@@ -279,13 +280,12 @@ class Visualization extends ViewDataTable
$action = $this->requestConfig->getApiMethodToRequest();
$apiParameters = array();
- $idDimension = Common::getRequestVar('idDimension', 0, 'int');
- $idGoal = Common::getRequestVar('idGoal', 0, 'int');
- if ($idDimension > 0) {
- $apiParameters['idDimension'] = $idDimension;
- }
- if ($idGoal > 0) {
- $apiParameters['idGoal'] = $idGoal;
+ $entityNames = StaticContainer::get('entities.idNames');
+ foreach ($entityNames as $entityName) {
+ $idEntity = Common::getRequestVar($entityName, 0, 'int');
+ if ($idEntity > 0) {
+ $apiParameters[$entityName] = $idEntity;
+ }
}
$metadata = ApiApi::getInstance()->getMetadata($idSite, $module, $action, $apiParameters);
diff --git a/core/RankingQuery.php b/core/RankingQuery.php
index cd4f830669..2338cc9ba4 100644
--- a/core/RankingQuery.php
+++ b/core/RankingQuery.php
@@ -136,6 +136,14 @@ class RankingQuery
}
/**
+ * @return array
+ */
+ public function getLabelColumns()
+ {
+ return $this->labelColumns;
+ }
+
+ /**
* Add a column that has be added to the outer queries.
*
* @param $column
diff --git a/core/Segment.php b/core/Segment.php
index aa39083cab..8e3e23cd9a 100644
--- a/core/Segment.php
+++ b/core/Segment.php
@@ -107,6 +107,16 @@ class Segment
}
}
+ /**
+ * Returns the segment expression.
+ * @return SegmentExpression
+ * @api since Piwik 3.2.0
+ */
+ public function getSegmentExpression()
+ {
+ return $this->segmentExpression;
+ }
+
private function getAvailableSegments()
{
// segment metadata
@@ -253,7 +263,7 @@ class Segment
&& $matchType != SegmentExpression::MATCH_IS_NULL_OR_EMPTY) {
if (isset($segment['sqlFilterValue'])) {
- $value = call_user_func($segment['sqlFilterValue'], $value);
+ $value = call_user_func($segment['sqlFilterValue'], $value, $segment['sqlSegment']);
}
// apply presentation filter
diff --git a/core/Segment/SegmentExpression.php b/core/Segment/SegmentExpression.php
index 605cd13682..0428a5ca5f 100644
--- a/core/Segment/SegmentExpression.php
+++ b/core/Segment/SegmentExpression.php
@@ -325,9 +325,23 @@ class SegmentExpression
$table = preg_replace('/^[A-Z_]+\(/', '', $table);
$tableExists = !$table || in_array($table, $availableTables);
- if (!$tableExists) {
- $availableTables[] = $table;
+ if ($tableExists) {
+ return;
}
+
+ if (is_array($availableTables)) {
+ foreach ($availableTables as $availableTable) {
+ if (is_array($availableTable)) {
+ if (!isset($availableTable['tableAlias']) && $availableTable['table'] === $table) {
+ return;
+ } elseif (isset($availableTable['tableAlias']) && $availableTable['tableAlias'] === $table) {
+ return;
+ }
+ }
+ }
+ }
+
+ $availableTables[] = $table;
}
/**
diff --git a/core/Settings/FieldConfig.php b/core/Settings/FieldConfig.php
index 7b2ce1df03..8074db3efb 100644
--- a/core/Settings/FieldConfig.php
+++ b/core/Settings/FieldConfig.php
@@ -63,6 +63,12 @@ class FieldConfig
const UI_CONTROL_SINGLE_SELECT = 'select';
/**
+ * Shows an expandable select field which is useful when each selectable value belongs to a group.
+ * To use this field assign it to the `$uiControl` property.
+ */
+ const UI_CONTROL_SINGLE_EXPANDABLE_SELECT = 'expandable-select';
+
+ /**
* Generates a hidden form field. To use this field assign it to the `$uiControl` property.
*/
const UI_CONTROL_HIDDEN = 'hidden';
diff --git a/core/Tracker/LogTable.php b/core/Tracker/LogTable.php
index 3fedba6945..091258e1e0 100644
--- a/core/Tracker/LogTable.php
+++ b/core/Tracker/LogTable.php
@@ -82,4 +82,15 @@ abstract class LogTable {
return;
}
+ /**
+ * Get the names of the columns that represents the primary key. For example "idvisit" or "idlink_va". If the table
+ * defines the primary key based on multiple columns, you must specify them all
+ * (eg array('idvisit', 'idgoal', 'buster')).
+ *
+ * @return array
+ */
+ public function getPrimaryKey()
+ {
+ return array();
+ }
}
diff --git a/core/ViewDataTable/Config.php b/core/ViewDataTable/Config.php
index 150c3f42d7..2c7a59beff 100644
--- a/core/ViewDataTable/Config.php
+++ b/core/ViewDataTable/Config.php
@@ -11,6 +11,7 @@ namespace Piwik\ViewDataTable;
use Piwik\API\Request as ApiRequest;
use Piwik\Common;
+use Piwik\Container\StaticContainer;
use Piwik\DataTable;
use Piwik\DataTable\Filter\PivotByDimension;
use Piwik\Metrics;
@@ -82,7 +83,7 @@ use Piwik\Plugin\ReportsProvider;
*
* @api
*/
-class Config
+class Config
{
/**
* The list of ViewDataTable properties that are 'Client Side Properties'.
@@ -110,6 +111,7 @@ class Config
'show_related_reports',
'show_limit_control',
'show_search',
+ 'show_export',
'enable_sort',
'show_bar_chart',
'show_pie_chart',
@@ -320,6 +322,13 @@ class Config
public $show_search = true;
/**
+ * Controls whether the export feature under the datatable is shown.
+ *
+ * @api since Piwik 3.2.0
+ */
+ public $show_export = true;
+
+ /**
* Controls whether the user can sort DataTables by clicking on table column headings.
*/
public $enable_sort = true;
@@ -518,13 +527,12 @@ class Config
}
$apiParameters = array();
- $idDimension = Common::getRequestVar('idDimension', 0, 'int');
- $idGoal = Common::getRequestVar('idGoal', 0, 'int');
- if ($idDimension > 0) {
- $apiParameters['idDimension'] = $idDimension;
- }
- if ($idGoal > 0) {
- $apiParameters['idGoal'] = $idGoal;
+ $entityNames = StaticContainer::get('entities.idNames');
+ foreach ($entityNames as $entityName) {
+ $idEntity = Common::getRequestVar($entityName, 0, 'int');
+ if ($idEntity > 0) {
+ $apiParameters[$entityName] = $idEntity;
+ }
}
$report = API::getInstance()->getMetadata($idSite, $this->controllerName, $this->controllerAction, $apiParameters);