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:
authorMichael Heerklotz <michael.heerklotz@check24.de>2017-11-02 12:36:30 +0300
committerMichael Heerklotz <michael.heerklotz@check24.de>2017-11-02 12:36:30 +0300
commit9ffb91805e4353ba149cd71b70a2c3f3af4c0177 (patch)
treef3be525b6bb8f904ad360eebfb52132ab1c9c433 /core
parent825facfd5ca6c285f5128c58b236f1c23ca26d45 (diff)
parentc46c56fd123ff7df014404a8a6b0315ec6b6663f (diff)
Merge branch '3.x-dev' of https://github.com/piwik/piwik into add_cookie_to_env
Diffstat (limited to 'core')
-rw-r--r--core/API/DataTableManipulator.php13
-rw-r--r--core/API/DataTablePostProcessor.php2
-rw-r--r--core/API/DocumentationGenerator.php10
-rw-r--r--core/Archive.php54
-rw-r--r--core/Archive/ArchiveQuery.php49
-rw-r--r--core/Archive/ArchiveQueryFactory.php127
-rw-r--r--core/ArchiveProcessor/Parameters.php26
-rw-r--r--core/ArchiveProcessor/PluginsArchiver.php11
-rw-r--r--core/ArchiveProcessor/Rules.php21
-rw-r--r--core/AssetManager/UIAssetCacheBuster.php32
-rw-r--r--core/AssetManager/UIAssetFetcher.php2
-rw-r--r--core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php4
-rw-r--r--core/CliMulti/Process.php2
-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/Common.php26
-rw-r--r--core/Config.php13
-rw-r--r--core/CronArchive.php14
-rw-r--r--core/DataAccess/ArchiveSelector.php3
-rw-r--r--core/DataAccess/LogAggregator.php29
-rw-r--r--core/DataAccess/LogQueryBuilder.php88
-rw-r--r--core/DataAccess/LogQueryBuilder/JoinGenerator.php30
-rw-r--r--core/DataAccess/LogQueryBuilder/JoinTables.php17
-rw-r--r--core/DataAccess/RawLogDao.php65
-rw-r--r--core/DataTable/Renderer/Console.php3
-rw-r--r--core/DataTable/Renderer/Csv.php2
-rw-r--r--core/DataTable/Renderer/Html.php2
-rw-r--r--core/Date.php48
-rw-r--r--core/ErrorHandler.php4
-rw-r--r--core/FileIntegrity.php22
-rw-r--r--core/Filechecks.php2
-rw-r--r--core/Filesystem.php6
-rw-r--r--core/Http.php8
-rw-r--r--core/Intl/Data/Provider/DateTimeFormatProvider.php10
-rw-r--r--core/Intl/Data/Resources/countries.php1
-rw-r--r--core/Intl/Data/Resources/languages-to-countries.php2
-rw-r--r--core/Mail.php5
-rw-r--r--core/Metrics/Formatter.php4
-rw-r--r--core/Period.php20
-rw-r--r--core/Period/Factory.php76
-rw-r--r--core/Period/PeriodValidator.php10
-rw-r--r--core/Plugin/ArchivedMetric.php207
-rw-r--r--core/Plugin/ComputedMetric.php260
-rw-r--r--core/Plugin/Controller.php1
-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/MetadataLoader.php4
-rw-r--r--core/Plugin/Metric.php10
-rw-r--r--core/Plugin/Report.php23
-rw-r--r--core/Plugin/ReportsProvider.php48
-rw-r--r--core/Plugin/Segment.php45
-rw-r--r--core/Plugin/Visualization.php15
-rw-r--r--core/RankingQuery.php8
-rw-r--r--core/Segment.php12
-rw-r--r--core/Segment/SegmentExpression.php18
-rw-r--r--core/Session.php2
-rw-r--r--core/Settings/FieldConfig.php6
-rw-r--r--core/Settings/Storage/Backend/MeasurableSettingsTable.php22
-rw-r--r--core/Settings/Storage/Backend/PluginSettingsTable.php22
-rw-r--r--core/SettingsPiwik.php18
-rw-r--r--core/Tracker.php5
-rw-r--r--core/Tracker/LogTable.php21
-rw-r--r--core/Tracker/Response.php1
-rw-r--r--core/Tracker/TrackerCodeGenerator.php4
-rw-r--r--core/Tracker/VisitorRecognizer.php6
-rw-r--r--core/Translation/Translator.php2
-rwxr-xr-xcore/Twig.php4
-rwxr-xr-xcore/Updates/1.7.2-rc7.php2
-rw-r--r--core/Version.php2
-rw-r--r--core/View.php9
-rw-r--r--core/ViewDataTable/Config.php33
-rw-r--r--core/ViewDataTable/Factory.php9
-rw-r--r--core/testMinimumPhpVersion.php16
83 files changed, 2786 insertions, 544 deletions
diff --git a/core/API/DataTableManipulator.php b/core/API/DataTableManipulator.php
index 7dad3b7a0d..7fd5f39a74 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);
@@ -169,7 +170,7 @@ abstract class DataTableManipulator
if (empty($meta)) {
throw new Exception(sprintf(
- "The DataTable cannot be manipulated: Metadata for report %s.%s could not be found. You can define the metadata in a hook, see example at: http://developer.piwik.org/api-reference/events#apigetreportmetadata",
+ "The DataTable cannot be manipulated: Metadata for report %s.%s could not be found. You can define the metadata in a hook, see example at: https://developer.piwik.org/api-reference/events#apigetreportmetadata",
$this->apiModule, $this->apiMethod
));
}
diff --git a/core/API/DataTablePostProcessor.php b/core/API/DataTablePostProcessor.php
index c4eb7441d0..a292139053 100644
--- a/core/API/DataTablePostProcessor.php
+++ b/core/API/DataTablePostProcessor.php
@@ -397,7 +397,7 @@ class DataTablePostProcessor
// this is needed because Proxy uses Common::getRequestVar which in turn
// uses Common::sanitizeInputValue. This causes the > that separates recursive labels
// to become &gt; and we need to undo that here.
- $label = str_replace( htmlentities('>'), '>', $label);
+ $label = str_replace( htmlentities('>', ENT_COMPAT | ENT_HTML401, 'UTF-8'), '>', $label);
return $label;
}
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/Archive.php b/core/Archive.php
index 4fc7e219bc..3a598eae88 100644
--- a/core/Archive.php
+++ b/core/Archive.php
@@ -8,12 +8,13 @@
*/
namespace Piwik;
+use Piwik\Archive\ArchiveQuery;
+use Piwik\Archive\ArchiveQueryFactory;
use Piwik\Archive\Parameters;
use Piwik\ArchiveProcessor\Rules;
use Piwik\Archive\ArchiveInvalidator;
use Piwik\Container\StaticContainer;
use Piwik\DataAccess\ArchiveSelector;
-use Piwik\Period\Factory as PeriodFactory;
/**
* The **Archive** class is used to query cached analytics statistics
@@ -106,7 +107,7 @@ use Piwik\Period\Factory as PeriodFactory;
*
* @api
*/
-class Archive
+class Archive implements ArchiveQuery
{
const REQUEST_ALL_WEBSITES_FLAG = 'all';
const ARCHIVE_ALL_PLUGINS_FLAG = 'all';
@@ -176,7 +177,7 @@ class Archive
* @param bool $forceIndexedBySite Whether to force index the result of a query by site ID.
* @param bool $forceIndexedByDate Whether to force index the result of a query by period.
*/
- protected function __construct(Parameters $params, $forceIndexedBySite = false,
+ public function __construct(Parameters $params, $forceIndexedBySite = false,
$forceIndexedByDate = false)
{
$this->params = $params;
@@ -203,30 +204,12 @@ class Archive
* or date range (ie, 'YYYY-MM-DD,YYYY-MM-DD').
* @param bool|false|string $segment Segment definition or false if no segment should be used. {@link Piwik\Segment}
* @param bool|false|string $_restrictSitesToLogin Used only when running as a scheduled task.
- * @return static
+ * @return ArchiveQuery
*/
public static function build($idSites, $period, $strDate, $segment = false, $_restrictSitesToLogin = false)
{
- $websiteIds = Site::getIdSitesFromIdSitesString($idSites, $_restrictSitesToLogin);
-
- $timezone = false;
- if (count($websiteIds) == 1) {
- $timezone = Site::getTimezoneFor($websiteIds[0]);
- }
-
- if (Period::isMultiplePeriod($strDate, $period)) {
- $oPeriod = PeriodFactory::build($period, $strDate, $timezone);
- $allPeriods = $oPeriod->getSubperiods();
- } else {
- $oPeriod = PeriodFactory::makePeriodFromQueryParams($timezone, $period, $strDate);
- $allPeriods = array($oPeriod);
- }
-
- $segment = new Segment($segment, $websiteIds);
- $idSiteIsAll = $idSites == self::REQUEST_ALL_WEBSITES_FLAG;
- $isMultipleDate = Period::isMultiplePeriod($strDate, $period);
-
- return static::factory($segment, $allPeriods, $websiteIds, $idSiteIsAll, $isMultipleDate);
+ return StaticContainer::get(ArchiveQueryFactory::class)->build($idSites, $period, $strDate, $segment,
+ $_restrictSitesToLogin);
}
/**
@@ -249,24 +232,13 @@ class Archive
* the result of querying functions will be indexed by period,
* regardless of whether `count($periods) == 1`.
*
- * @return Archive
+ * @return ArchiveQuery
*/
- public static function factory(Segment $segment, array $periods, array $idSites, $idSiteIsAll = false, $isMultipleDate = false)
+ public static function factory(Segment $segment, array $periods, array $idSites, $idSiteIsAll = false,
+ $isMultipleDate = false)
{
- $forceIndexedBySite = false;
- $forceIndexedByDate = false;
-
- if ($idSiteIsAll || count($idSites) > 1) {
- $forceIndexedBySite = true;
- }
-
- if (count($periods) > 1 || $isMultipleDate) {
- $forceIndexedByDate = true;
- }
-
- $params = new Parameters($idSites, $periods, $segment);
-
- return new static($params, $forceIndexedBySite, $forceIndexedByDate);
+ return StaticContainer::get(ArchiveQueryFactory::class)->factory($segment, $periods, $idSites, $idSiteIsAll,
+ $isMultipleDate);
}
/**
@@ -838,7 +810,7 @@ class Archive
* @throws \Exception If a plugin cannot be found or if the plugin for the report isn't
* activated.
*/
- private static function getPluginForReport($report)
+ public static function getPluginForReport($report)
{
// Core metrics are always processed in Core, for the requested date/period/segment
if (in_array($report, Metrics::getVisitsMetricNames())) {
diff --git a/core/Archive/ArchiveQuery.php b/core/Archive/ArchiveQuery.php
new file mode 100644
index 0000000000..acd6bbf29e
--- /dev/null
+++ b/core/Archive/ArchiveQuery.php
@@ -0,0 +1,49 @@
+<?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\Archive;
+
+
+use Piwik\DataTable;
+
+interface ArchiveQuery
+{
+ /**
+ * @param string|string[] $names
+ * @return false|number|array
+ */
+ public function getNumeric($names);
+
+ /**
+ * @param string|string[] $names
+ * @return DataTable|DataTable\Map
+ */
+ public function getDataTableFromNumeric($names);
+
+ /**
+ * @param $names
+ * @return mixed
+ */
+ public function getDataTableFromNumericAndMergeChildren($names);
+
+ /**
+ * @param string $name
+ * @param int|string|null $idSubtable
+ * @return DataTable|DataTable\Map
+ */
+ public function getDataTable($name, $idSubtable = null);
+
+ /**
+ * @param string $name
+ * @param int|string|null $idSubtable
+ * @param int|null $depth
+ * @param bool $addMetadataSubtableId
+ * @return DataTable|DataTable\Map
+ */
+ public function getDataTableExpanded($name, $idSubtable = null, $depth = null, $addMetadataSubtableId = true);
+} \ No newline at end of file
diff --git a/core/Archive/ArchiveQueryFactory.php b/core/Archive/ArchiveQueryFactory.php
new file mode 100644
index 0000000000..ddf3db6b87
--- /dev/null
+++ b/core/Archive/ArchiveQueryFactory.php
@@ -0,0 +1,127 @@
+<?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\Archive;
+
+use Piwik\Archive;
+use Piwik\Period;
+use Piwik\Segment;
+use Piwik\Site;
+use Piwik\Period\Factory as PeriodFactory;
+
+class ArchiveQueryFactory
+{
+ public function __construct()
+ {
+ // empty
+ }
+
+ /**
+ * @see \Piwik\Archive::build()
+ */
+ public function build($idSites, $strPeriod, $strDate, $strSegment = false, $_restrictSitesToLogin = false)
+ {
+ list($websiteIds, $timezone, $idSiteIsAll) = $this->getSiteInfoFromQueryParam($idSites, $_restrictSitesToLogin);
+ list($allPeriods, $isMultipleDate) = $this->getPeriodInfoFromQueryParam($strDate, $strPeriod, $timezone);
+ $segment = $this->getSegmentFromQueryParam($strSegment, $websiteIds);
+
+ return $this->factory($segment, $allPeriods, $websiteIds, $idSiteIsAll, $isMultipleDate);
+ }
+
+ /**
+ * @see \Piwik\Archive::factory()
+ */
+ public function factory(Segment $segment, array $periods, array $idSites, $idSiteIsAll = false, $isMultipleDate = false)
+ {
+ $forceIndexedBySite = false;
+ $forceIndexedByDate = false;
+
+ if ($idSiteIsAll || count($idSites) > 1) {
+ $forceIndexedBySite = true;
+ }
+
+ if (count($periods) > 1 || $isMultipleDate) {
+ $forceIndexedByDate = true;
+ }
+
+ $params = new Parameters($idSites, $periods, $segment);
+
+ return $this->newInstance($params, $forceIndexedBySite, $forceIndexedByDate);
+ }
+
+ public function newInstance(Parameters $params, $forceIndexedBySite, $forceIndexedByDate)
+ {
+ return new Archive($params, $forceIndexedBySite, $forceIndexedByDate);
+ }
+
+ /**
+ * Parses the site ID string provided in the 'idSite' query parameter to a list of
+ * website IDs.
+ *
+ * @param string $idSites the value of the 'idSite' query parameter
+ * @param bool $_restrictSitesToLogin
+ * @return array an array containing three elements:
+ * - an array of website IDs
+ * - string timezone to use (or false to use no timezone) when creating periods.
+ * - true if the request was for all websites (this forces the archive result to
+ * be indexed by site, even if there is only one site in Piwik)
+ */
+ protected function getSiteInfoFromQueryParam($idSites, $_restrictSitesToLogin)
+ {
+ $websiteIds = Site::getIdSitesFromIdSitesString($idSites, $_restrictSitesToLogin);
+
+ $timezone = false;
+ if (count($websiteIds) == 1) {
+ $timezone = Site::getTimezoneFor($websiteIds[0]);
+ }
+
+ $idSiteIsAll = $idSites == Archive::REQUEST_ALL_WEBSITES_FLAG;
+
+ return [$websiteIds, $timezone, $idSiteIsAll];
+ }
+
+ /**
+ * Parses the date & period query parameters into a list of periods.
+ *
+ * @param string $strDate the value of the 'date' query parameter
+ * @param string $strPeriod the value of the 'period' query parameter
+ * @param string $timezone the timezone to use when constructing periods.
+ * @return array an array containing two elements:
+ * - the list of period objects to query archive data for
+ * - true if the request was for multiple periods (ie, two months, two weeks, etc.), false if otherwise.
+ * (this forces the archive result to be indexed by period, even if the list of periods
+ * has only one period).
+ */
+ protected function getPeriodInfoFromQueryParam($strDate, $strPeriod, $timezone)
+ {
+ if (Period::isMultiplePeriod($strDate, $strPeriod)) {
+ $oPeriod = PeriodFactory::build($strPeriod, $strDate, $timezone);
+ $allPeriods = $oPeriod->getSubperiods();
+ } else {
+ $oPeriod = PeriodFactory::makePeriodFromQueryParams($timezone, $strPeriod, $strDate);
+ $allPeriods = array($oPeriod);
+ }
+
+ $isMultipleDate = Period::isMultiplePeriod($strDate, $strPeriod);
+
+ return [$allPeriods, $isMultipleDate];
+ }
+
+ /**
+ * Parses the segment query parameter into a Segment object.
+ *
+ * @param string $strSegment the value of the 'segment' query parameter.
+ * @param int[] $websiteIds the list of sites being queried.
+ * @return Segment
+ */
+ protected function getSegmentFromQueryParam($strSegment, $websiteIds)
+ {
+ return new Segment($strSegment, $websiteIds);
+ }
+} \ No newline at end of file
diff --git a/core/ArchiveProcessor/Parameters.php b/core/ArchiveProcessor/Parameters.php
index 4edd1f4b58..752557a868 100644
--- a/core/ArchiveProcessor/Parameters.php
+++ b/core/ArchiveProcessor/Parameters.php
@@ -154,12 +154,36 @@ class Parameters
}
/**
+ * Returns the start day of the period in the site's timezone (includes the time of day).
+ *
+ * @return Date
+ */
+ public function getDateTimeStart()
+ {
+ return $this->getPeriod()->getDateTimeStart()->setTimezone($this->getSite()->getTimezone());
+ }
+
+ /**
+ * Returns the end day of the period in the site's timezone (includes the time of day).
+ *
+ * @return Date
+ */
+ public function getDateTimeEnd()
+ {
+ return $this->getPeriod()->getDateTimeEnd()->setTimezone($this->getSite()->getTimezone());
+ }
+
+ /**
* @return bool
*/
public function isSingleSiteDayArchive()
{
$oneSite = $this->isSingleSite();
- $oneDay = $this->getPeriod()->getLabel() == 'day';
+
+ $period = $this->getPeriod();
+ $secondsInPeriod = $period->getDateEnd()->getTimestampUTC() - $period->getDateStart()->getTimestampUTC();
+ $oneDay = $secondsInPeriod <= Date::NUM_SECONDS_IN_DAY;
+
return $oneDay && $oneSite;
}
diff --git a/core/ArchiveProcessor/PluginsArchiver.php b/core/ArchiveProcessor/PluginsArchiver.php
index f251551a6d..2b46ea29f6 100644
--- a/core/ArchiveProcessor/PluginsArchiver.php
+++ b/core/ArchiveProcessor/PluginsArchiver.php
@@ -49,11 +49,11 @@ class PluginsArchiver
*/
public static $archivers = array();
- public function __construct(Parameters $params, $isTemporaryArchive)
+ public function __construct(Parameters $params, $isTemporaryArchive, ArchiveWriter $archiveWriter = null)
{
$this->params = $params;
$this->isTemporaryArchive = $isTemporaryArchive;
- $this->archiveWriter = new ArchiveWriter($this->params, $this->isTemporaryArchive);
+ $this->archiveWriter = $archiveWriter ?: new ArchiveWriter($this->params, $this->isTemporaryArchive);
$this->archiveWriter->initNewArchive();
$this->logAggregator = new LogAggregator($params);
@@ -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/ArchiveProcessor/Rules.php b/core/ArchiveProcessor/Rules.php
index 4f8391c281..09be79eea2 100644
--- a/core/ArchiveProcessor/Rules.php
+++ b/core/ArchiveProcessor/Rules.php
@@ -115,8 +115,10 @@ class Rules
public static function getMinTimeProcessedForTemporaryArchive(
Date $dateStart, \Piwik\Period $period, Segment $segment, Site $site)
{
+ $todayArchiveTimeToLive = self::getPeriodArchiveTimeToLiveDefault($period->getLabel());
+
$now = time();
- $minimumArchiveTime = $now - Rules::getTodayArchiveTimeToLive();
+ $minimumArchiveTime = $now - $todayArchiveTimeToLive;
$idSites = array($site->getId());
$isArchivingDisabled = Rules::isArchivingDisabledFor($idSites, $segment, $period->getLabel());
@@ -158,6 +160,23 @@ class Rules
return self::getTodayArchiveTimeToLiveDefault();
}
+ public static function getPeriodArchiveTimeToLiveDefault($periodLabel)
+ {
+ if (empty($periodLabel) || strtolower($periodLabel) === 'day') {
+ return self::getTodayArchiveTimeToLive();
+ }
+
+ $config = Config::getInstance();
+ $general = $config->General;
+
+ $key = sprintf('time_before_%s_archive_considered_outdated', $periodLabel);
+ if (isset($general[$key]) && is_numeric($general[$key]) && $general[$key] > 0) {
+ return $general[$key];
+ }
+
+ return self::getTodayArchiveTimeToLive();
+ }
+
public static function getTodayArchiveTimeToLiveDefault()
{
return Config::getInstance()->General['time_before_today_archive_considered_outdated'];
diff --git a/core/AssetManager/UIAssetCacheBuster.php b/core/AssetManager/UIAssetCacheBuster.php
index fcbd0720a9..15b55943b9 100644
--- a/core/AssetManager/UIAssetCacheBuster.php
+++ b/core/AssetManager/UIAssetCacheBuster.php
@@ -27,20 +27,32 @@ class UIAssetCacheBuster extends Singleton
*/
public function piwikVersionBasedCacheBuster($pluginNames = false)
{
- $masterFile = PIWIK_INCLUDE_PATH . '/.git/refs/heads/master';
- $currentGitHash = file_exists($masterFile) ? @file_get_contents($masterFile) : null;
+ static $cachedCacheBuster = null;
- $pluginNames = !$pluginNames ? Manager::getInstance()->getLoadedPluginsName() : $pluginNames;
- sort($pluginNames);
+ if (empty($cachedCacheBuster) || $pluginNames !== false) {
- $pluginsInfo = '';
- foreach ($pluginNames as $pluginName) {
- $plugin = Manager::getInstance()->getLoadedPlugin($pluginName);
- $pluginsInfo .= $plugin->getPluginName() . $plugin->getVersion() . ',';
+ $masterFile = PIWIK_INCLUDE_PATH . '/.git/refs/heads/master';
+ $currentGitHash = file_exists($masterFile) ? @file_get_contents($masterFile) : null;
+
+ $plugins = !$pluginNames ? Manager::getInstance()->getLoadedPluginsName() : $pluginNames;
+ sort($plugins);
+
+ $pluginsInfo = '';
+ foreach ($plugins as $pluginName) {
+ $plugin = Manager::getInstance()->getLoadedPlugin($pluginName);
+ $pluginsInfo .= $plugin->getPluginName() . $plugin->getVersion() . ',';
+ }
+
+ $cacheBuster = md5($pluginsInfo . PHP_VERSION . Version::VERSION . trim($currentGitHash));
+
+ if ($pluginNames !== false) {
+ return $cacheBuster;
+ }
+
+ $cachedCacheBuster = $cacheBuster;
}
- $cacheBuster = md5($pluginsInfo . PHP_VERSION . Version::VERSION . trim($currentGitHash));
- return $cacheBuster;
+ return $cachedCacheBuster;
}
/**
diff --git a/core/AssetManager/UIAssetFetcher.php b/core/AssetManager/UIAssetFetcher.php
index 6d710e1387..0b8e9b3a55 100644
--- a/core/AssetManager/UIAssetFetcher.php
+++ b/core/AssetManager/UIAssetFetcher.php
@@ -106,7 +106,7 @@ abstract class UIAssetFetcher
private function getBaseDirectory()
{
// served by web server directly, so must be a public path
- return PIWIK_USER_PATH;
+ return PIWIK_DOCUMENT_ROOT;
}
/**
diff --git a/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php b/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php
index 3d5cad72b6..1d4fb4d4d2 100644
--- a/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php
+++ b/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php
@@ -38,7 +38,7 @@ class StylesheetUIAssetMerger extends UIAssetMerger
protected function getMergedAssets()
{
// note: we're using setImportDir on purpose (not addImportDir)
- $this->lessCompiler->setImportDir(PIWIK_USER_PATH);
+ $this->lessCompiler->setImportDir(PIWIK_DOCUMENT_ROOT);
$concatenatedAssets = $this->getConcatenatedAssets();
$this->lessCompiler->setFormatter('classic');
@@ -183,7 +183,7 @@ class StylesheetUIAssetMerger extends UIAssetMerger
$baseDirectory = dirname($uiAsset->getRelativeLocation());
return function ($matches) use ($baseDirectory) {
- $absolutePath = PIWIK_USER_PATH . "/$baseDirectory/" . $matches[2];
+ $absolutePath = PIWIK_DOCUMENT_ROOT . "/$baseDirectory/" . $matches[2];
// Allow to import extension less file
if (strpos($matches[2], '.') === false) {
diff --git a/core/CliMulti/Process.php b/core/CliMulti/Process.php
index db2d5b2cd0..c95c5af40e 100644
--- a/core/CliMulti/Process.php
+++ b/core/CliMulti/Process.php
@@ -245,7 +245,7 @@ class Process
*/
public static function getRunningProcesses()
{
- $ids = explode("\n", trim(`ps ex 2>/dev/null | awk '{print $1}' 2>/dev/null`));
+ $ids = explode("\n", trim(`ps ex 2>/dev/null | awk '! /defunct/ {print $1}' 2>/dev/null`));
$ids = array_map('intval', $ids);
$ids = array_filter($ids, function ($id) {
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/Common.php b/core/Common.php
index 1616e56505..7430cc6662 100644
--- a/core/Common.php
+++ b/core/Common.php
@@ -232,7 +232,27 @@ class Common
return mb_strtolower($string, 'UTF-8');
}
- return strtolower($string);
+ // return unchanged string as using `strtolower` might cause unicode problems
+ return $string;
+ }
+
+ /**
+ * Multi-byte strtoupper() - works with UTF-8.
+ *
+ * Calls `mb_strtoupper` if available and falls back to `strtoupper` if not.
+ *
+ * @param string $string
+ * @return string
+ * @api
+ */
+ public static function mb_strtoupper($string)
+ {
+ if (function_exists('mb_strtoupper')) {
+ return mb_strtoupper($string, 'UTF-8');
+ }
+
+ // return unchanged string as using `strtoupper` might cause unicode problems
+ return $string;
}
/*
@@ -1026,6 +1046,10 @@ class Common
$countryList = $dataProvider->getCountryList();
+ if ($country == 'ti') {
+ $country = 'cn';
+ }
+
return isset($countryList[$country]) ? $countryList[$country] : 'unk';
}
diff --git a/core/Config.php b/core/Config.php
index f9a95feed0..3da4d2541d 100644
--- a/core/Config.php
+++ b/core/Config.php
@@ -157,6 +157,7 @@ class Config
return array(
'action_url_category_delimiter' => $general['action_url_category_delimiter'],
+ 'action_title_category_delimiter' => $general['action_title_category_delimiter'],
'autocomplete_min_sites' => $general['autocomplete_min_sites'],
'datatable_export_range_as_day' => $general['datatable_export_range_as_day'],
'datatable_row_limits' => $this->getDatatableRowLimits(),
@@ -380,17 +381,25 @@ class Config
if ($output !== null
&& $output !== false
) {
+ $localPath = $this->getLocalPath();
if ($this->doNotWriteConfigInTests) {
// simulate whether it would be successful
- $success = is_writable($this->getLocalPath());
+ $success = is_writable($localPath);
} else {
- $success = @file_put_contents($this->getLocalPath(), $output);
+ $success = @file_put_contents($localPath, $output);
}
if ($success === false) {
throw $this->getConfigNotWritableException();
}
+
+ /**
+ * Triggered when a INI config file is changed on disk.
+ *
+ * @param string $localPath Absolute path to the changed file on the server.
+ */
+ Piwik::postEvent('Core.configFileChanged', [$localPath]);
}
if ($clear) {
diff --git a/core/CronArchive.php b/core/CronArchive.php
index d9ed8c6311..d9c536a8c3 100644
--- a/core/CronArchive.php
+++ b/core/CronArchive.php
@@ -1275,13 +1275,23 @@ class CronArchive
// Recommend to disable browser archiving when using this script
if (Rules::isBrowserTriggerEnabled()) {
$this->logger->info("- If you execute this script at least once per hour (or more often) in a crontab, you may disable 'Browser trigger archiving' in Piwik UI > Settings > General Settings.");
- $this->logger->info(" See the doc at: http://piwik.org/docs/setup-auto-archiving/");
+ $this->logger->info(" See the doc at: https://piwik.org/docs/setup-auto-archiving/");
}
$this->logger->info("- Reports for today will be processed at most every " . $this->todayArchiveTimeToLive
. " seconds. You can change this value in Piwik UI > Settings > General Settings.");
- $this->logger->info("- Reports for the current week/month/year will be refreshed at most every "
+
+ $this->logger->info("- Reports for the current week/month/year will be requested at most every "
. $this->processPeriodsMaximumEverySeconds . " seconds.");
+ foreach (array('week', 'month', 'year', 'range') as $period) {
+ $ttl = Rules::getPeriodArchiveTimeToLiveDefault($period);
+
+ if (!empty($ttl) && $ttl !== $this->todayArchiveTimeToLive) {
+ $this->logger->info("- Reports for the current $period will be processed at most every " . $ttl
+ . " seconds. You can change this value in config/config.ini.php by editing 'time_before_" . $period . "_archive_considered_outdated' in the '[General]' section.");
+ }
+ }
+
// Try and not request older data we know is already archived
if ($this->lastSuccessRunTimestamp !== false) {
$dateLast = time() - $this->lastSuccessRunTimestamp;
diff --git a/core/DataAccess/ArchiveSelector.php b/core/DataAccess/ArchiveSelector.php
index 7be972bdb8..123b6c51ee 100644
--- a/core/DataAccess/ArchiveSelector.php
+++ b/core/DataAccess/ArchiveSelector.php
@@ -178,8 +178,7 @@ class ArchiveSelector
$bind = array();
if ($firstPeriod instanceof Range) {
- $dateCondition = "period = ? AND date1 = ? AND date2 = ?";
- $bind[] = $firstPeriod->getId();
+ $dateCondition = "date1 = ? AND date2 = ?";
$bind[] = $firstPeriod->getDateStart()->toString('Y-m-d');
$bind[] = $firstPeriod->getDateEnd()->toString('Y-m-d');
} else {
diff --git a/core/DataAccess/LogAggregator.php b/core/DataAccess/LogAggregator.php
index 4651394556..ede9f81526 100644
--- a/core/DataAccess/LogAggregator.php
+++ b/core/DataAccess/LogAggregator.php
@@ -10,10 +10,14 @@ namespace Piwik\DataAccess;
use Piwik\ArchiveProcessor\Parameters;
use Piwik\Common;
+use Piwik\Container\StaticContainer;
use Piwik\DataArray;
+use Piwik\Date;
use Piwik\Db;
use Piwik\Metrics;
+use Piwik\Period;
use Piwik\Tracker\GoalManager;
+use Psr\Log\LoggerInterface;
/**
* Contains methods that calculate metrics by aggregating log data (visits, actions, conversions,
@@ -141,16 +145,28 @@ class LogAggregator
private $queryOriginHint = '';
/**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+
+ /**
* Constructor.
*
* @param \Piwik\ArchiveProcessor\Parameters $params
*/
- public function __construct(Parameters $params)
+ public function __construct(Parameters $params, LoggerInterface $logger = null)
{
- $this->dateStart = $params->getDateStart();
- $this->dateEnd = $params->getDateEnd();
+ $this->dateStart = $params->getDateTimeStart();
+ $this->dateEnd = $params->getDateTimeEnd();
$this->segment = $params->getSegment();
$this->sites = $params->getIdSites();
+ $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
+ }
+
+ public function getSegment()
+ {
+ return $this->segment;
}
public function setQueryOriginHint($nameOfOrigiin)
@@ -169,6 +185,9 @@ class LogAggregator
$query['sql'] = 'SELECT /* ' . $this->queryOriginHint . ' */' . substr($query['sql'], strlen($select));
}
+ // Uncomment to log on DEBUG level all archiving queries
+ // $this->logger->debug($query['sql']);
+
return $query;
}
@@ -501,9 +520,9 @@ class LogAggregator
*
* @return array
*/
- protected function getGeneralQueryBindParams()
+ public function getGeneralQueryBindParams()
{
- $bind = array($this->dateStart->getDateStartUTC(), $this->dateEnd->getDateEndUTC());
+ $bind = array($this->dateStart->toString(Date::DATE_TIME_FORMAT), $this->dateEnd->toString(Date::DATE_TIME_FORMAT));
$bind = array_merge($bind, $this->sites);
return $bind;
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..ba5da08696 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;
@@ -267,10 +272,25 @@ class JoinGenerator
);
if (is_array($tA) && is_array($tB)) {
- if (isset($tB['tableAlias']) && isset($tA['joinOn']) && strpos($tA['joinOn'], $tB['tableAlias']) !== false) {
+ $tAName = '';
+ if (isset($tA['tableAlias'])) {
+ $tAName = $tA['tableAlias'];
+ } elseif (isset($tA['table'])) {
+ $tAName = $tA['table'];
+ }
+
+ $tBName = '';
+ if (isset($tB['tableAlias'])) {
+ $tBName = $tB['tableAlias'];
+ } elseif (isset($tB['table'])) {
+ $tBName = $tB['table'];
+ }
+
+ if ($tBName && isset($tA['joinOn']) && strpos($tA['joinOn'], $tBName) !== false) {
return 1;
}
- if (isset($tA['tableAlias']) && isset($tB['joinOn']) && strpos($tB['joinOn'], $tA['tableAlias']) !== false) {
+
+ if ($tAName && isset($tB['joinOn']) && strpos($tB['joinOn'], $tAName) !== false) {
return -1;
}
@@ -278,11 +298,11 @@ class JoinGenerator
}
if (is_array($tA)) {
- return -1;
+ return 1;
}
if (is_array($tB)) {
- return 1;
+ return -1;
}
if (isset($coreSort[$tA])) {
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/DataAccess/RawLogDao.php b/core/DataAccess/RawLogDao.php
index c933d17f63..27bb33563b 100644
--- a/core/DataAccess/RawLogDao.php
+++ b/core/DataAccess/RawLogDao.php
@@ -12,6 +12,7 @@ use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\Db;
use Piwik\Plugin\Dimension\DimensionMetadataProvider;
+use Piwik\Plugin\LogTablesProvider;
/**
* DAO that queries log tables.
@@ -25,9 +26,15 @@ class RawLogDao
*/
private $dimensionMetadataProvider;
- public function __construct(DimensionMetadataProvider $provider = null)
+ /**
+ * @var LogTablesProvider
+ */
+ private $logTablesProvider;
+
+ public function __construct(DimensionMetadataProvider $provider = null, LogTablesProvider $logTablesProvider = null)
{
$this->dimensionMetadataProvider = $provider ?: StaticContainer::get('Piwik\Plugin\Dimension\DimensionMetadataProvider');
+ $this->logTablesProvider = $logTablesProvider ?: StaticContainer::get('Piwik\Plugin\LogTablesProvider');
}
/**
@@ -226,22 +233,15 @@ class RawLogDao
return Db::query($sql, array_merge(array_values($values), array($idVisit)));
}
- private function getIdFieldForLogTable($logTable)
+ protected function getIdFieldForLogTable($logTable)
{
- switch ($logTable) {
- case 'log_visit':
- return 'idvisit';
- case 'log_link_visit_action':
- return 'idlink_va';
- case 'log_conversion':
- return 'idvisit';
- case 'log_conversion_item':
- return 'idvisit';
- case 'log_action':
- return 'idaction';
- default:
- throw new \InvalidArgumentException("Unknown log table '$logTable'.");
+ $idColumns = $this->getTableIdColumns();
+
+ if (isset($idColumns[$logTable])) {
+ return $idColumns[$logTable];
}
+
+ throw new \InvalidArgumentException("Unknown log table '$logTable'.");
}
// TODO: instead of creating a log query like this, we should re-use segments. to do this, however, there must be a 1-1
@@ -291,11 +291,10 @@ class RawLogDao
return $sql;
}
-
- private function getMaxIdsInLogTables()
+ protected function getMaxIdsInLogTables()
{
- $tables = array('log_conversion', 'log_link_visit_action', 'log_visit', 'log_conversion_item');
$idColumns = $this->getTableIdColumns();
+ $tables = array_keys($idColumns);
$result = array();
foreach ($tables as $table) {
@@ -349,8 +348,17 @@ class RawLogDao
private function lockLogTables()
{
+ $tables = $this->getTableIdColumns();
+ unset($tables['log_action']); // we write lock it
+ $tableNames = array_keys($tables);
+
+ $readLocks = array();
+ foreach ($tableNames as $tableName) {
+ $readLocks[] = Common::prefixTable($tableName);
+ }
+
Db::lockTables(
- $readLocks = Common::prefixTables('log_conversion', 'log_link_visit_action', 'log_visit', 'log_conversion_item'),
+ $readLocks,
$writeLocks = Common::prefixTables('log_action')
);
}
@@ -367,13 +375,18 @@ class RawLogDao
Db::query($deleteSql);
}
- private function getTableIdColumns()
+ protected function getTableIdColumns()
{
- return array(
- 'log_link_visit_action' => 'idlink_va',
- 'log_conversion' => 'idvisit',
- 'log_visit' => 'idvisit',
- 'log_conversion_item' => 'idvisit'
- );
+ $columns = array();
+
+ foreach ($this->logTablesProvider->getAllLogTables() as $logTable) {
+ $idColumn = $logTable->getIdColumn();
+
+ if (!empty($idColumn)) {
+ $columns[$logTable->getName()] = $idColumn;
+ }
+ }
+
+ return $columns;
}
}
diff --git a/core/DataTable/Renderer/Console.php b/core/DataTable/Renderer/Console.php
index bd16e93c4f..7a796b7dae 100644
--- a/core/DataTable/Renderer/Console.php
+++ b/core/DataTable/Renderer/Console.php
@@ -146,6 +146,9 @@ class Console extends Renderer
$output .= $prefix . " <b>$id</b><br />";
if (is_array($metadataIn)) {
foreach ($metadataIn as $name => $value) {
+ if (is_object($value) && !method_exists( $value, '__toString' )) {
+ $value = 'Object [' . get_class($value) . ']';
+ }
$output .= $prefix . $prefix . "$name => $value";
}
}
diff --git a/core/DataTable/Renderer/Csv.php b/core/DataTable/Renderer/Csv.php
index 4d55979347..eaa80675d6 100644
--- a/core/DataTable/Renderer/Csv.php
+++ b/core/DataTable/Renderer/Csv.php
@@ -293,7 +293,7 @@ class Csv extends Renderer
*/
protected function renderHeader()
{
- $fileName = 'Piwik ' . Piwik::translate('General_Export');
+ $fileName = Piwik::translate('General_Export');
$period = Common::getRequestVar('period', false);
$date = Common::getRequestVar('date', false);
diff --git a/core/DataTable/Renderer/Html.php b/core/DataTable/Renderer/Html.php
index f27dc51768..cf887b8167 100644
--- a/core/DataTable/Renderer/Html.php
+++ b/core/DataTable/Renderer/Html.php
@@ -117,6 +117,8 @@ class Html extends Renderer
foreach ($row->getMetadata() as $name => $value) {
if (is_string($value)) {
$value = "'$value'";
+ } else if (is_array($value)) {
+ $value = var_export($value, true);
}
$metadata[] = "'$name' => $value";
}
diff --git a/core/Date.php b/core/Date.php
index dd2051a1a0..c33c0ed188 100644
--- a/core/Date.php
+++ b/core/Date.php
@@ -178,17 +178,34 @@ class Date
}
/**
+ * @return string
+ * @deprecated
+ */
+ public function getDateStartUTC()
+ {
+ return $this->getStartOfDay()->toString(self::DATE_TIME_FORMAT);
+ }
+
+ /**
* Returns the start of the day of the current timestamp in UTC. For example,
* if the current timestamp is `'2007-07-24 14:04:24'` in UTC, the result will
- * be `'2007-07-24'`.
+ * be `'2007-07-24'` as a Date.
*
- * @return string
+ * @return Date
*/
- public function getDateStartUTC()
+ public function getStartOfDay()
{
$dateStartUTC = gmdate('Y-m-d', $this->timestamp);
- $date = Date::factory($dateStartUTC)->setTimezone($this->timezone);
- return $date->toString(self::DATE_TIME_FORMAT);
+ return Date::factory($dateStartUTC)->setTimezone($this->timezone);
+ }
+
+ /**
+ * @return string
+ * @deprecated
+ */
+ public function getDateEndUTC()
+ {
+ return $this->getEndOfDay()->toString(self::DATE_TIME_FORMAT);
}
/**
@@ -196,13 +213,12 @@ class Date
* if the current timestamp is `'2007-07-24 14:03:24'` in UTC, the result will
* be `'2007-07-24 23:59:59'`.
*
- * @return string
+ * @return Date
*/
- public function getDateEndUTC()
+ public function getEndOfDay()
{
$dateEndUTC = gmdate('Y-m-d 23:59:59', $this->timestamp);
- $date = Date::factory($dateEndUTC)->setTimezone($this->timezone);
- return $date->toString(self::DATE_TIME_FORMAT);
+ return Date::factory($dateEndUTC)->setTimezone($this->timezone);
}
/**
@@ -222,8 +238,8 @@ class Date
/**
* Returns the offset to UTC time for the given timezone
*
- * @param $timezone
- * @return int offest in minutes
+ * @param string $timezone
+ * @return int offset in seconds
*/
public static function getUtcOffset($timezone)
{
@@ -725,6 +741,14 @@ class Date
return $this->toString('h');
case "h":
return $this->toString('g');
+ case "KK": // 00 .. 11
+ return str_pad($this->toString('g') - 1, 2, '0');
+ case "K": // 0 .. 11
+ return $this->toString('g') - 1;
+ case "kk": // 01 .. 24
+ return str_pad($this->toString('G') + 1, 2, '0');
+ case "k": // 1 .. 24
+ return $this->toString('G') + 1;
// minute
case "mm":
case "m":
@@ -755,7 +779,7 @@ class Date
}
protected static $tokens = array(
- 'G', 'y', 'M', 'L', 'd', 'h', 'H', 'm', 's', 'E', 'c', 'e', 'D', 'F', 'w', 'W', 'a', 'z', 'Z', 'v',
+ 'G', 'y', 'M', 'L', 'd', 'h', 'H', 'k', 'K', 'm', 's', 'E', 'c', 'e', 'D', 'F', 'w', 'W', 'a', 'z', 'Z', 'v',
);
/**
diff --git a/core/ErrorHandler.php b/core/ErrorHandler.php
index 6694d2cba3..8b1897c1fa 100644
--- a/core/ErrorHandler.php
+++ b/core/ErrorHandler.php
@@ -108,7 +108,7 @@ class ErrorHandler
private static function createLogMessage($errno, $errstr, $errfile, $errline)
{
return sprintf(
- "%s(%d): %s - %s - Piwik " . (class_exists('Piwik\Version') ? Version::VERSION : '') . " - Please report this message in the Piwik forums: http://forum.piwik.org (please do a search first as it might have been reported already)",
+ "%s(%d): %s - %s - Piwik " . (class_exists('Piwik\Version') ? Version::VERSION : '') . " - Please report this message in the Piwik forums: https://forum.piwik.org (please do a search first as it might have been reported already)",
$errfile,
$errline,
ErrorHandler::getErrNoString($errno),
@@ -123,7 +123,7 @@ class ErrorHandler
$message = ErrorHandler::getErrNoString($errno) . ' - ' . $errstr;
$html = "<p>There is an error. Please report the message (Piwik " . (class_exists('Piwik\Version') ? Version::VERSION : '') . ")
- and full backtrace in the <a href='?module=Proxy&action=redirect&url=http://forum.piwik.org' target='_blank'>Piwik forums</a> (please do a search first as it might have been reported already!).</p>";
+ and full backtrace in the <a href='?module=Proxy&action=redirect&url=https://forum.piwik.org' target='_blank'>Piwik forums</a> (please do a search first as it might have been reported already!).</p>";
$html .= "<p><strong>{$message}</strong> in <em>{$errfile}</em>";
$html .= " on line {$errline}</p>";
$html .= "Backtrace:<pre>";
diff --git a/core/FileIntegrity.php b/core/FileIntegrity.php
index ee37ca9887..abe3bc31fe 100644
--- a/core/FileIntegrity.php
+++ b/core/FileIntegrity.php
@@ -73,13 +73,19 @@ class FileIntegrity
'misc/*.dat.gz',
'misc/*.bin',
'misc/user/*png',
+ 'misc/user/*js',
'misc/package',
'misc/package/WebAppGallery/*.xml',
'misc/package/WebAppGallery/install.sql',
'plugins/ImageGraph/fonts/unifont.ttf',
'vendor/autoload.php',
'vendor/composer/autoload_real.php',
+ 'vendor/szymach/c-pchart/app/*',
'tmp/*',
+ // Search engine sites verification
+ 'google*.html',
+ 'BingSiteAuth.xml',
+ 'yandex*.html',
// Files below are not expected but they used to be present in older Piwik versions and may be still here
// As they are not going to cause any trouble we won't report them as 'File to delete'
'*.coveralls.yml',
@@ -111,8 +117,14 @@ class FileIntegrity
$deleteAllAtOnce = array();
$chunks = array_chunk($directories, 50);
+ $command = 'rm -Rf';
+
+ if (SettingsServer::isWindows()) {
+ $command = 'rmdir /s /q';
+ }
+
foreach ($chunks as $directories) {
- $deleteAllAtOnce[] = sprintf('rm -Rf %s', implode(' ', $directories));
+ $deleteAllAtOnce[] = sprintf('%s %s', $command, implode(' ', $directories));
}
$messages[] = Piwik::translate('General_ExceptionUnexpectedDirectory')
@@ -153,8 +165,14 @@ class FileIntegrity
$deleteAllAtOnce = array();
$chunks = array_chunk($files, 50);
+ $command = 'rm';
+
+ if (SettingsServer::isWindows()) {
+ $command = 'del';
+ }
+
foreach ($chunks as $files) {
- $deleteAllAtOnce[] = sprintf('rm %s', implode(' ', $files));
+ $deleteAllAtOnce[] = sprintf('%s %s', $command, implode(' ', $files));
}
$messages[] = Piwik::translate('General_ExceptionUnexpectedFile')
diff --git a/core/Filechecks.php b/core/Filechecks.php
index 5f98fd228d..73d641e090 100644
--- a/core/Filechecks.php
+++ b/core/Filechecks.php
@@ -94,7 +94,7 @@ class Filechecks
. "<blockquote>$directoryList</blockquote>"
. "<p>If this doesn't work, you can try to create the directories with your FTP software, and set the CHMOD to 0755 (or 0777 if 0755 is not enough). To do so with your FTP software, right click on the directories then click permissions.</p>"
. "<p>After applying the modifications, you can <a href='index.php'>refresh the page</a>.</p>"
- . "<p>If you need more help, try <a href='?module=Proxy&action=redirect&url=http://piwik.org'>Piwik.org</a>.</p>";
+ . "<p>If you need more help, try <a href='?module=Proxy&action=redirect&url=https://piwik.org'>Piwik.org</a>.</p>";
$ex = new MissingFilePermissionException($directoryMessage);
$ex->setIsHtmlMessage();
diff --git a/core/Filesystem.php b/core/Filesystem.php
index 513dcd68a8..7fbd63be39 100644
--- a/core/Filesystem.php
+++ b/core/Filesystem.php
@@ -31,6 +31,12 @@ class Filesystem
TrackerCache::deleteTrackerCache();
PiwikCache::flushAll();
self::clearPhpCaches();
+
+ $pluginManager = Plugin\Manager::getInstance();
+ $plugins = $pluginManager->getLoadedPlugins();
+ foreach ($plugins as $plugin) {
+ $plugin->reloadPluginInformation();
+ }
}
/**
diff --git a/core/Http.php b/core/Http.php
index 3ad1b2ce39..59628852ab 100644
--- a/core/Http.php
+++ b/core/Http.php
@@ -213,7 +213,7 @@ class Http
throw new Exception('Invalid protocol/scheme: ' . $url['scheme']);
}
$host = $url['host'];
- $port = isset($url['port']) ? $url['port'] : 80;
+ $port = isset($url['port']) ? $url['port'] : ('https' == $url['scheme'] ? 443 : 80);
$path = isset($url['path']) ? $url['path'] : '/';
if (isset($url['query'])) {
$path .= '?' . $url['query'];
@@ -241,6 +241,10 @@ class Http
$connectHost = $host;
$connectPort = $port;
$requestHeader = "$httpMethod $path HTTP/$httpVer\r\n";
+
+ if ('https' == $url['scheme']) {
+ $connectHost = 'ssl://' . $connectHost;
+ }
}
// connection attempt
@@ -258,7 +262,7 @@ class Http
// send HTTP request header
$requestHeader .=
- "Host: $host" . ($port != 80 ? ':' . $port : '') . "\r\n"
+ "Host: $host" . ($port != 80 && ('https' == $url['scheme'] && $port != 443) ? ':' . $port : '') . "\r\n"
. ($httpAuth ? $httpAuth : '')
. ($proxyAuth ? $proxyAuth : '')
. 'User-Agent: ' . $userAgent . "\r\n"
diff --git a/core/Intl/Data/Provider/DateTimeFormatProvider.php b/core/Intl/Data/Provider/DateTimeFormatProvider.php
index 90ffad609f..daf22b0b8c 100644
--- a/core/Intl/Data/Provider/DateTimeFormatProvider.php
+++ b/core/Intl/Data/Provider/DateTimeFormatProvider.php
@@ -65,6 +65,16 @@ class DateTimeFormatProvider
}
/**
+ * Returns if time is present as 12 hour clock (eg am/pm)
+ *
+ * @return bool
+ */
+ public function uses12HourClock()
+ {
+ return false;
+ }
+
+ /**
* Returns interval format pattern for the given format type
*
* @param bool $short whether to return short or long format pattern
diff --git a/core/Intl/Data/Resources/countries.php b/core/Intl/Data/Resources/countries.php
index 9048509525..e000900296 100644
--- a/core/Intl/Data/Resources/countries.php
+++ b/core/Intl/Data/Resources/countries.php
@@ -237,7 +237,6 @@ return array(
'tf' => 'ant',
'tg' => 'afr',
'th' => 'asi',
- 'ti' => 'asi', // Tibet (no iso 3166 code)
'tj' => 'asi',
'tk' => 'oce',
'tl' => 'asi',
diff --git a/core/Intl/Data/Resources/languages-to-countries.php b/core/Intl/Data/Resources/languages-to-countries.php
index 91ab0940c5..5e50cd4ea3 100644
--- a/core/Intl/Data/Resources/languages-to-countries.php
+++ b/core/Intl/Data/Resources/languages-to-countries.php
@@ -54,7 +54,7 @@ return array(
'sr' => 'rs', // Serbian => Serbia
'sv' => 'se', // Swedish => Sweden
'th' => 'th', // Thai => Thailand
- 'bo' => 'ti', // Tibetan => Tibet
+ 'bo' => 'cn', // Tibetan => China
'tr' => 'tr', // Turkish => Turkey
'uk' => 'ua', // Ukrainian => Ukraine
);
diff --git a/core/Mail.php b/core/Mail.php
index c6c8623cfd..a3771f536d 100644
--- a/core/Mail.php
+++ b/core/Mail.php
@@ -111,11 +111,14 @@ class Mail extends Zend_Mail
if (!empty($mailConfig['encryption'])) {
$smtpConfig['ssl'] = $mailConfig['encryption'];
}
+
+ if (!empty($mailConfig['port'])) {
+ $smtpConfig['port'] = $mailConfig['port'];
+ }
$host = trim($mailConfig['host']);
$tr = new \Zend_Mail_Transport_Smtp($host, $smtpConfig);
Mail::setDefaultTransport($tr);
- @ini_set("smtp_port", $mailConfig['port']);
}
public function send($transport = null)
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/Period.php b/core/Period.php
index 0abd1f4ba3..ebcad4896d 100644
--- a/core/Period.php
+++ b/core/Period.php
@@ -135,6 +135,26 @@ abstract class Period
}
/**
+ * Returns the start date & time of this period.
+ *
+ * @return Date
+ */
+ public function getDateTimeStart()
+ {
+ return $this->getDateStart()->getStartOfDay();
+ }
+
+ /**
+ * Returns the end date & time of this period.
+ *
+ * @return Date
+ */
+ public function getDateTimeEnd()
+ {
+ return $this->getDateEnd()->getEndOfDay();
+ }
+
+ /**
* Returns the last day of the period.
*
* @return Date
diff --git a/core/Period/Factory.php b/core/Period/Factory.php
index 88c29a92e2..60e5bc9b07 100644
--- a/core/Period/Factory.php
+++ b/core/Period/Factory.php
@@ -9,12 +9,55 @@
namespace Piwik\Period;
use Exception;
+use Piwik\Container\StaticContainer;
use Piwik\Date;
use Piwik\Period;
use Piwik\Piwik;
+use Piwik\Plugin;
-class Factory
+/**
+ * Creates Period instances using the values used for the 'period' and 'date'
+ * query parameters.
+ *
+ * ## Custom Periods
+ *
+ * Plugins can define their own period factories all plugins to define new period types, in addition
+ * to "day", "week", "month", "year" and "range".
+ *
+ * To define a new period type:
+ *
+ * 1. create a new period class that derives from {@see \Piwik\Period}.
+ * 2. extend this class in a new PeriodFactory class and put it in /path/to/piwik/plugins/MyPlugin/PeriodFactory.php
+ *
+ * Period name collisions:
+ *
+ * If two plugins try to handle the same period label, the first one encountered will
+ * be used. In other words, avoid using another plugin's period label.
+ */
+abstract class Factory
{
+ public function __construct()
+ {
+ // empty
+ }
+
+ /**
+ * Returns true if this factory should handle the period/date string combination.
+ *
+ * @return bool
+ */
+ public abstract function shouldHandle($strPeriod, $strDate);
+
+ /**
+ * Creates a period using the value of the 'date' query parameter.
+ *
+ * @param string $strPeriod
+ * @param string|Date $date
+ * @param string $timezone
+ * @return Period
+ */
+ public abstract function make($strPeriod, $date, $timezone);
+
/**
* Creates a new Period instance with a period ID and {@link Date} instance.
*
@@ -35,26 +78,33 @@ class Factory
|| $period == 'range') {
return new Range($period, $date, $timezone);
}
- $date = Date::factory($date);
+
+ $dateObject = Date::factory($date);
+ } else {
+ $dateObject = $date;
}
switch ($period) {
case 'day':
- return new Day($date);
- break;
-
+ return new Day($dateObject);
case 'week':
- return new Week($date);
- break;
-
+ return new Week($dateObject);
case 'month':
- return new Month($date);
- break;
-
+ return new Month($dateObject);
case 'year':
- return new Year($date);
- break;
+ return new Year($dateObject);
}
+
+ /** @var string[] $customPeriodFactories */
+ $customPeriodFactories = Plugin\Manager::getInstance()->findComponents('PeriodFactory', self::class);
+ foreach ($customPeriodFactories as $customPeriodFactoryClass) {
+ $customPeriodFactory = StaticContainer::get($customPeriodFactoryClass);
+ if ($customPeriodFactory->shouldHandle($period, $date)) {
+ return $customPeriodFactory->make($period, $date, $timezone);
+ }
+ }
+
+ throw new \Exception("Don't know how to create a '$period' period!");
}
public static function checkPeriodIsEnabled($period)
diff --git a/core/Period/PeriodValidator.php b/core/Period/PeriodValidator.php
index b29077b302..1850edde34 100644
--- a/core/Period/PeriodValidator.php
+++ b/core/Period/PeriodValidator.php
@@ -36,8 +36,9 @@ class PeriodValidator
public function getPeriodsAllowedForUI()
{
$periodsAllowed = Config::getInstance()->General['enabled_periods_UI'];
-
- return array_map('trim', explode(',', $periodsAllowed));
+ $periodsAllowed = array_map('trim', explode(',', $periodsAllowed));
+ $periodsAllowed = array_unique($periodsAllowed);
+ return $periodsAllowed;
}
/**
@@ -46,7 +47,8 @@ class PeriodValidator
public function getPeriodsAllowedForAPI()
{
$periodsAllowed = Config::getInstance()->General['enabled_periods_API'];
-
- return array_map('trim', explode(',', $periodsAllowed));
+ $periodsAllowed = array_map('trim', explode(',', $periodsAllowed));
+ $periodsAllowed = array_unique($periodsAllowed);
+ return $periodsAllowed;
}
}
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/Controller.php b/core/Plugin/Controller.php
index 4d186ab15f..3fe7f1f75e 100644
--- a/core/Plugin/Controller.php
+++ b/core/Plugin/Controller.php
@@ -841,6 +841,7 @@ abstract class Controller
$view->period = $currentPeriod;
$view->otherPeriods = $availablePeriods;
+ $view->enabledPeriods = self::getEnabledPeriodsInUI();
$view->periodsNames = self::getEnabledPeriodsNames();
}
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/MetadataLoader.php b/core/Plugin/MetadataLoader.php
index b3d552cf80..7522e3ab1d 100644
--- a/core/Plugin/MetadataLoader.php
+++ b/core/Plugin/MetadataLoader.php
@@ -82,8 +82,8 @@ class MetadataLoader
$descriptionKey = $this->pluginName . '_PluginDescription';
return array(
'description' => $descriptionKey,
- 'homepage' => 'http://piwik.org/',
- 'authors' => array(array('name' => 'Piwik', 'homepage' => 'http://piwik.org/')),
+ 'homepage' => 'https://piwik.org/',
+ 'authors' => array(array('name' => 'Piwik', 'homepage' => 'https://piwik.org/')),
'license' => 'GPL v3+',
'version' => Version::VERSION,
'theme' => false,
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 df28f450c9..c300ad11a3 100644
--- a/core/Plugin/Report.php
+++ b/core/Plugin/Report.php
@@ -318,6 +318,26 @@ class Report
}
/**
+ *
+ * Processing a uniqueId for each report, can be used by UIs as a key to match a given report
+ * @return string
+ */
+ public function getId()
+ {
+ $params = $this->getParameters();
+
+ $paramsKey = $this->getModule() . '.' . $this->getAction();
+
+ if (!empty($params)) {
+ foreach ($params as $key => $value) {
+ $paramsKey .= '_' . $key . '--' . $value;
+ }
+ }
+
+ return $paramsKey;
+ }
+
+ /**
* lets you add any amount of widgets for this report. If a report defines a {@link $categoryId} and a
* {@link $subcategoryId} a widget will be generated automatically.
*
@@ -390,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);
@@ -840,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/ReportsProvider.php b/core/Plugin/ReportsProvider.php
index f134009881..c1ceb3c377 100644
--- a/core/Plugin/ReportsProvider.php
+++ b/core/Plugin/ReportsProvider.php
@@ -8,11 +8,14 @@
*/
namespace Piwik\Plugin;
+use Piwik\Cache;
use Piwik\CacheId;
use Piwik\Category\CategoryList;
+use Piwik\Common;
use Piwik\Piwik;
use Piwik\Plugin;
use Piwik\Cache as PiwikCache;
+use Piwik\Site;
/**
* Get reports that are defined by plugins.
@@ -44,11 +47,44 @@ class ReportsProvider
private static function getMapOfModuleActionsToReport()
{
- $cacheId = CacheId::pluginAware('ReportFactoryMap');
+ $cacheKey = 'ReportFactoryMap';
+ $idSite = Common::getRequestVar('idSite', 0, 'int');
- $cache = PiwikCache::getEagerCache();
- if ($cache->contains($cacheId)) {
- $mapApiToReport = $cache->fetch($cacheId);
+ if (!empty($idSite)) {
+ // some reports may be per site!
+ $cacheKey .= '_' . (int) $idSite;
+ }
+
+ // fallback eg fror API.getReportMetadata and API.getSegmentsMetadata
+ $idSites = Common::getRequestVar('idSites', '', $type = null);
+ if (!empty($idSites)) {
+
+ $transientCache = Cache::getTransientCache();
+ $transientCacheKey = 'ReportIdSitesParam';
+ if ($transientCache->contains($transientCacheKey)) {
+ $idSites = $transientCache->fetch($transientCacheKey);
+ } else {
+ // this may be called 100 times during one page request and may go to DB, therefore have to cache
+ $idSites = Site::getIdSitesFromIdSitesString($idSites);
+ sort($idSites);// we sort to reuse the cache key as often as possible
+ $transientCache->save($transientCacheKey, $idSites);
+ }
+
+ // it is important to not use either idsite, or idsites in the cache key but to include both for security reasons
+ // otherwise someone may specify idSite=5&idSites=7 and if then a plugin is eg only looking at idSites param
+ // we could return a wrong result (eg API.getSegmentsMetadata)
+ if (count($idSites) <= 5) {
+ $cacheKey .= '_' . implode('_', $idSites); // we keep the cache key readable when possible
+ } else {
+ $cacheKey .= '_' . md5(implode('_', $idSites)); // we need to shorten it
+ }
+ }
+
+ $lazyCacheId = CacheId::pluginAware($cacheKey);
+
+ $cache = PiwikCache::getLazyCache();
+ if ($cache->contains($lazyCacheId)) {
+ $mapApiToReport = $cache->fetch($lazyCacheId);
} else {
$reports = new static();
$reports = $reports->getAllReports();
@@ -66,7 +102,7 @@ class ReportsProvider
$mapApiToReport[$key] = get_class($report);
}
- $cache->save($cacheId, $mapApiToReport);
+ $cache->save($lazyCacheId, $mapApiToReport);
}
return $mapApiToReport;
@@ -222,4 +258,4 @@ class ReportsProvider
{
return Plugin\Manager::getInstance()->findMultipleComponents('Reports', '\\Piwik\\Plugin\\Report');
}
-} \ No newline at end of file
+}
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..bd46547e64 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;
@@ -37,6 +38,7 @@ use Piwik\API\Request as ApiRequest;
* itself:
*
* - report documentation,
+ * - a header message (if {@link Piwik\ViewDataTable\Config::$show_header_message} is set),
* - a footer message (if {@link Piwik\ViewDataTable\Config::$show_footer_message} is set),
* - a list of links to related reports (if {@link Piwik\ViewDataTable\Config::$related_reports} is set),
* - a button that allows users to switch visualizations,
@@ -279,13 +281,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/Session.php b/core/Session.php
index 965553b521..60bf464278 100644
--- a/core/Session.php
+++ b/core/Session.php
@@ -121,7 +121,7 @@ class Session extends Zend_Session
$enableDbSessions = '';
if (DbHelper::isInstalled()) {
$enableDbSessions = "<br/>If you still experience issues after trying these changes,
- we recommend that you <a href='http://piwik.org/faq/how-to-install/#faq_133' rel='noreferrer' target='_blank'>enable database session storage</a>.";
+ we recommend that you <a href='https://piwik.org/faq/how-to-install/#faq_133' rel='noreferrer' target='_blank'>enable database session storage</a>.";
}
$pathToSessions = Filechecks::getErrorMessageMissingPermissions(self::getSessionsDirectory());
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/Settings/Storage/Backend/MeasurableSettingsTable.php b/core/Settings/Storage/Backend/MeasurableSettingsTable.php
index 22452288c8..e91a7ac9cd 100644
--- a/core/Settings/Storage/Backend/MeasurableSettingsTable.php
+++ b/core/Settings/Storage/Backend/MeasurableSettingsTable.php
@@ -150,8 +150,15 @@ class MeasurableSettingsTable implements BackendInterface
*/
public static function removeAllSettingsForSite($idSite)
{
- $query = sprintf('DELETE FROM %s WHERE idsite = ?', Common::prefixTable('site_setting'));
- Db::query($query, array($idSite));
+ try {
+ $query = sprintf('DELETE FROM %s WHERE idsite = ?', Common::prefixTable('site_setting'));
+ Db::query($query, array($idSite));
+ } catch (Exception $e) {
+ if ($e->getCode() != 42) {
+ // ignore table not found error, which might occur when updating from an older version of Piwik
+ throw $e;
+ }
+ }
}
/**
@@ -161,7 +168,14 @@ class MeasurableSettingsTable implements BackendInterface
*/
public static function removeAllSettingsForPlugin($pluginName)
{
- $query = sprintf('DELETE FROM %s WHERE plugin_name = ?', Common::prefixTable('site_setting'));
- Db::query($query, array($pluginName));
+ try {
+ $query = sprintf('DELETE FROM %s WHERE plugin_name = ?', Common::prefixTable('site_setting'));
+ Db::query($query, array($pluginName));
+ } catch (Exception $e) {
+ if ($e->getCode() != 42) {
+ // ignore table not found error, which might occur when updating from an older version of Piwik
+ throw $e;
+ }
+ }
}
}
diff --git a/core/Settings/Storage/Backend/PluginSettingsTable.php b/core/Settings/Storage/Backend/PluginSettingsTable.php
index 87476ff8fb..0f49d6b68e 100644
--- a/core/Settings/Storage/Backend/PluginSettingsTable.php
+++ b/core/Settings/Storage/Backend/PluginSettingsTable.php
@@ -155,8 +155,15 @@ class PluginSettingsTable implements BackendInterface
throw new Exception('No userLogin specified. Cannot remove all settings for this user');
}
- $table = Common::prefixTable('plugin_setting');
- Db::get()->query(sprintf('DELETE FROM %s WHERE user_login = ?', $table), array($userLogin));
+ try {
+ $table = Common::prefixTable('plugin_setting');
+ Db::get()->query(sprintf('DELETE FROM %s WHERE user_login = ?', $table), array($userLogin));
+ } catch (Exception $e) {
+ if ($e->getCode() != 42) {
+ // ignore table not found error, which might occur when updating from an older version of Piwik
+ throw $e;
+ }
+ }
}
/**
@@ -169,7 +176,14 @@ class PluginSettingsTable implements BackendInterface
*/
public static function removeAllSettingsForPlugin($pluginName)
{
- $table = Common::prefixTable('plugin_setting');
- Db::get()->query(sprintf('DELETE FROM %s WHERE plugin_name = ?', $table), array($pluginName));
+ try {
+ $table = Common::prefixTable('plugin_setting');
+ Db::get()->query(sprintf('DELETE FROM %s WHERE plugin_name = ?', $table), array($pluginName));
+ } catch (Exception $e) {
+ if ($e->getCode() != 42) {
+ // ignore table not found error, which might occur when updating from an older version of Piwik
+ throw $e;
+ }
+ }
}
}
diff --git a/core/SettingsPiwik.php b/core/SettingsPiwik.php
index f8f5d37c68..d8a04ec27a 100644
--- a/core/SettingsPiwik.php
+++ b/core/SettingsPiwik.php
@@ -235,6 +235,17 @@ class SettingsPiwik
}
/**
+ * Check if outgoing internet connections are enabled
+ * This is often disable in an intranet environment
+ *
+ * @return bool
+ */
+ public static function isInternetEnabled()
+ {
+ return (bool) Config::getInstance()->General['enable_internet_features'];
+ }
+
+ /**
* Detect whether user has enabled auto updates. Please note this config is a bit misleading. It is currently
* actually used for 2 things: To disable making any connections back to Piwik, and to actually disable the auto
* update of core and plugins.
@@ -242,7 +253,12 @@ class SettingsPiwik
*/
public static function isAutoUpdateEnabled()
{
- return (bool) Config::getInstance()->General['enable_auto_update'];
+ $enableAutoUpdate = (bool) Config::getInstance()->General['enable_auto_update'];
+ if(self::isInternetEnabled() === true && $enableAutoUpdate === true){
+ return true;
+ }
+
+ return false;
}
/**
diff --git a/core/Tracker.php b/core/Tracker.php
index 3ed757c638..43310c06a6 100644
--- a/core/Tracker.php
+++ b/core/Tracker.php
@@ -264,6 +264,11 @@ class Tracker
TrackerConfig::setConfigValue('enable_fingerprinting_across_websites', 1);
}
+ // Tests can simulate the tracker API maintenance mode
+ if (Common::getRequestVar('forceEnableTrackerMaintenanceMode', false, null, $args) == 1) {
+ TrackerConfig::setConfigValue('record_statistics', 0);
+ }
+
// Tests can force the use of 3rd party cookie for ID visitor
if (Common::getRequestVar('forceUseThirdPartyCookie', false, null, $args) == 1) {
TrackerConfig::setConfigValue('use_third_party_id_cookie', 1);
diff --git a/core/Tracker/LogTable.php b/core/Tracker/LogTable.php
index 8e66f43463..091258e1e0 100644
--- a/core/Tracker/LogTable.php
+++ b/core/Tracker/LogTable.php
@@ -21,6 +21,16 @@ abstract class LogTable {
abstract public function getName();
/**
+ * Get the name of the column that represents the primary key. For example "idvisit" or "idlink_va". If the table
+ * does not have a unique ID for each row, you may choose a column that comes closest to it, for example "idvisit".
+ * @return string
+ */
+ public function getIdColumn()
+ {
+ return '';
+ }
+
+ /**
* Get the name of the column that can be used to join a visit with another table. This is the name of the column
* that represents the "idvisit".
* @return string
@@ -72,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/Tracker/Response.php b/core/Tracker/Response.php
index 7927666511..d5a01d7c24 100644
--- a/core/Tracker/Response.php
+++ b/core/Tracker/Response.php
@@ -71,6 +71,7 @@ class Response
public function outputResponse(Tracker $tracker)
{
if (!$tracker->shouldRecordStatistics()) {
+ Common::sendResponseCode(503);
$this->outputApiResponse($tracker);
Common::printDebug("Logging disabled, display transparent logo");
} elseif (!$tracker->hasLoggedRequests()) {
diff --git a/core/Tracker/TrackerCodeGenerator.php b/core/Tracker/TrackerCodeGenerator.php
index 79b80e8bf1..fcfe889661 100644
--- a/core/Tracker/TrackerCodeGenerator.php
+++ b/core/Tracker/TrackerCodeGenerator.php
@@ -169,14 +169,14 @@ class TrackerCodeGenerator
$setTrackerUrl = 'var u=((document.location.protocol === "https:") ? "https://{$httpsPiwikUrl}/" : "http://{$piwikUrl}/");';
$codeImpl['httpsPiwikUrl'] = rtrim($codeImpl['httpsPiwikUrl'], "/");
}
- $codeImpl = array('setTrackerUrl' => htmlentities($setTrackerUrl)) + $codeImpl;
+ $codeImpl = array('setTrackerUrl' => htmlentities($setTrackerUrl, ENT_COMPAT | ENT_HTML401, 'UTF-8')) + $codeImpl;
$view = new View('@Morpheus/javascriptCode');
$view->disableCacheBuster();
$view->loadAsync = $codeImpl['loadAsync'];
$view->trackNoScript = $codeImpl['trackNoScript'];
$jsCode = $view->render();
- $jsCode = htmlentities($jsCode);
+ $jsCode = htmlentities($jsCode, ENT_COMPAT | ENT_HTML401, 'UTF-8');
foreach ($codeImpl as $keyToReplace => $replaceWith) {
$jsCode = str_replace('{$' . $keyToReplace . '}', $replaceWith, $jsCode);
diff --git a/core/Tracker/VisitorRecognizer.php b/core/Tracker/VisitorRecognizer.php
index 498329b51c..7727156727 100644
--- a/core/Tracker/VisitorRecognizer.php
+++ b/core/Tracker/VisitorRecognizer.php
@@ -101,6 +101,12 @@ class VisitorRecognizer
$isNewVisitForced = $request->getParam('new_visit');
$isNewVisitForced = !empty($isNewVisitForced);
$enforceNewVisit = $isNewVisitForced || $this->trackerAlwaysNewVisitor;
+ if($isNewVisitForced) {
+ Common::printDebug("-> New visit forced: &new_visit=1 in request");
+ }
+ if($this->trackerAlwaysNewVisitor) {
+ Common::printDebug("-> New visit forced: Debug.tracker_always_new_visitor = 1 in config.ini.php");
+ }
if (!$enforceNewVisit
&& $visitRow
diff --git a/core/Translation/Translator.php b/core/Translation/Translator.php
index 98b3759f3b..9ed366c7ba 100644
--- a/core/Translation/Translator.php
+++ b/core/Translation/Translator.php
@@ -194,7 +194,7 @@ class Translator
public function reset()
{
$this->currentLanguage = $this->getDefaultLanguage();
- $this->directories = array();
+ $this->directories = array(PIWIK_INCLUDE_PATH . '/lang');
$this->translations = array();
}
diff --git a/core/Twig.php b/core/Twig.php
index ce8085f23c..c36452055b 100755
--- a/core/Twig.php
+++ b/core/Twig.php
@@ -356,7 +356,7 @@ class Twig
$template .= '>';
if (!empty($options['raw'])) {
- $template .= $message;
+ $template .= piwik_fix_lbrace($message);
} else {
$template .= twig_escape_filter($twigEnv, $message, 'html');
}
@@ -373,7 +373,7 @@ class Twig
{
$rawSafeDecoded = new Twig_SimpleFilter('rawSafeDecoded', function ($string) {
$string = str_replace('+', '%2B', $string);
- $string = str_replace('&nbsp;', html_entity_decode('&nbsp;'), $string);
+ $string = str_replace('&nbsp;', html_entity_decode('&nbsp;', ENT_COMPAT | ENT_HTML401, 'UTF-8'), $string);
$string = SafeDecodeLabel::decodeLabelSafe($string);
diff --git a/core/Updates/1.7.2-rc7.php b/core/Updates/1.7.2-rc7.php
index 41ddbd342c..4ba26ae3d3 100755
--- a/core/Updates/1.7.2-rc7.php
+++ b/core/Updates/1.7.2-rc7.php
@@ -50,7 +50,7 @@ class Updates_1_7_2_rc7 extends Updates
$idDashboard = $dashboard['iddashboard'];
$login = $dashboard['login'];
$layout = $dashboard['layout'];
- $layout = html_entity_decode($layout);
+ $layout = html_entity_decode($layout, ENT_COMPAT | ENT_HTML401, 'UTF-8');
$layout = str_replace("\\\"", "\"", $layout);
$migrations[] = $this->migration->db->boundSql($updateQuery, array($layout, $idDashboard, $login));
diff --git a/core/Version.php b/core/Version.php
index a04ecb88f4..d19a150db9 100644
--- a/core/Version.php
+++ b/core/Version.php
@@ -20,7 +20,7 @@ final class Version
* The current Piwik version.
* @var string
*/
- const VERSION = '3.0.4';
+ const VERSION = '3.2.0';
public function isStableVersion($version)
{
diff --git a/core/View.php b/core/View.php
index e74eae6201..3c90b5f9a7 100644
--- a/core/View.php
+++ b/core/View.php
@@ -212,10 +212,15 @@ class View implements ViewInterface
return isset($this->templateVars[$name]);
}
+ /** @var Twig */
+ static $twigCached = null;
+
private function initializeTwig()
{
- $piwikTwig = new Twig();
- $this->twig = $piwikTwig->getTwigEnvironment();
+ if (empty(static::$twigCached)) {
+ static::$twigCached = new Twig();
+ }
+ $this->twig = static::$twigCached->getTwigEnvironment();
}
/**
diff --git a/core/ViewDataTable/Config.php b/core/ViewDataTable/Config.php
index 150c3f42d7..2a2a927da4 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;
@@ -363,7 +372,16 @@ class Config
public $show_ecommerce = false;
/**
+ * Stores an HTML message (if any) to display above the datatable view.
+ *
+ * Attention: Message will be printed raw. Don't forget to escape where needed!
+ */
+ public $show_header_message = false;
+
+ /**
* Stores an HTML message (if any) to display under the datatable view.
+ *
+ * Attention: Message will be printed raw. Don't forget to escape where needed!
*/
public $show_footer_message = false;
@@ -518,13 +536,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);
diff --git a/core/ViewDataTable/Factory.php b/core/ViewDataTable/Factory.php
index 90a44281e4..b9b166b963 100644
--- a/core/ViewDataTable/Factory.php
+++ b/core/ViewDataTable/Factory.php
@@ -106,12 +106,17 @@ class Factory
$params = array();
- if(is_null($loadViewDataTableParametersForUser)) {
+ if (!isset($loadViewDataTableParametersForUser)) {
$loadViewDataTableParametersForUser = ('0' == Common::getRequestVar('widget', '0', 'string'));
}
+
if ($loadViewDataTableParametersForUser) {
$login = Piwik::getCurrentUserLogin();
- $params = Manager::getViewDataTableParameters($login, $controllerAction);
+ $paramsKey = $controllerAction;
+ if (!empty($report) && $controllerAction === $apiAction) {
+ $paramsKey = $report->getId();
+ }
+ $params = Manager::getViewDataTableParameters($login, $paramsKey);
}
if (!self::isDefaultViewTypeForReportFixed($report)) {
diff --git a/core/testMinimumPhpVersion.php b/core/testMinimumPhpVersion.php
index ca276b1de3..9f4bffd57a 100644
--- a/core/testMinimumPhpVersion.php
+++ b/core/testMinimumPhpVersion.php
@@ -29,7 +29,7 @@ if ($minimumPhpInvalid) {
<p>Unfortunately it seems your webserver is using PHP version $piwik_currentPHPVersion. </p>
<p>Please try to update your PHP version, Piwik is really worth it! Nowadays most web hosts
support PHP $piwik_minimumPHPVersion.</p>
- <p>Also see the FAQ: <a href='http://piwik.org/faq/how-to-install/#faq_77'>My Web host supports PHP4 by default. How can I enable PHP5?</a></p>";
+ <p>Also see the FAQ: <a href='https://piwik.org/faq/how-to-install/#faq_77'>My Web host supports PHP4 by default. How can I enable PHP5?</a></p>";
} else {
if (!extension_loaded('session')) {
$piwik_errorMessage .= "<p><strong>Piwik and Zend_Session require the session extension</strong></p>
@@ -66,9 +66,9 @@ if ($minimumPhpInvalid) {
"<br/>" . $composerInstall.
" This will initialize composer for Piwik and download libraries we use in vendor/* directory.".
"\n\n<br/><br/>Then reload this page to access your analytics reports." .
- "\n\n<br/><br/>For more information check out this FAQ: <a href='http://piwik.org/faq/how-to-install/faq_18271/' rel='noreferrer' target='_blank'>How do I use Piwik from the Git repository?</a>." .
+ "\n\n<br/><br/>For more information check out this FAQ: <a href='https://piwik.org/faq/how-to-install/faq_18271/' rel='noreferrer' target='_blank'>How do I use Piwik from the Git repository?</a>." .
"\n\n<br/><br/>Note: if for some reasons you cannot install composer, instead install the latest Piwik release from ".
- "<a href='http://builds.piwik.org/piwik.zip'>builds.piwik.org</a>.</p>";
+ "<a href='https://builds.piwik.org/piwik.zip'>builds.piwik.org</a>.</p>";
}
}
@@ -133,7 +133,7 @@ if (!function_exists('Piwik_GetErrorMessagePage')) {
}
if ($optionalTrace) {
- $optionalTrace = '<h2>Stack trace</h2><pre>' . htmlentities($optionalTrace) . '</pre>';
+ $optionalTrace = '<h2>Stack trace</h2><pre>' . htmlentities($optionalTrace, ENT_COMPAT | ENT_HTML401, 'UTF-8') . '</pre>';
}
if ($isCli === null) {
@@ -142,10 +142,10 @@ if (!function_exists('Piwik_GetErrorMessagePage')) {
if ($optionalLinks) {
$optionalLinks = '<ul>
- <li><a rel="noreferrer" target="_blank" href="http://piwik.org">Piwik.org homepage</a></li>
- <li><a rel="noreferrer" target="_blank" href="http://piwik.org/faq/">Piwik Frequently Asked Questions</a></li>
- <li><a rel="noreferrer" target="_blank" href="http://piwik.org/docs/">Piwik Documentation</a></li>
- <li><a rel="noreferrer" target="_blank" href="http://forum.piwik.org/">Piwik Forums</a></li>
+ <li><a rel="noreferrer" target="_blank" href="https://piwik.org">Piwik.org homepage</a></li>
+ <li><a rel="noreferrer" target="_blank" href="https://piwik.org/faq/">Piwik Frequently Asked Questions</a></li>
+ <li><a rel="noreferrer" target="_blank" href="https://piwik.org/docs/">Piwik Documentation</a></li>
+ <li><a rel="noreferrer" target="_blank" href="https://forum.piwik.org/">Piwik Forums</a></li>
<li><a rel="noreferrer" target="_blank" href="https://piwik.org/support/?pk_campaign=App_AnErrorOccured&pk_source=Piwik_App&pk_medium=ProfessionalServicesLink">Professional help for Piwik</a></li>
</ul>';
}