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:
authordiosmosis <benaka@piwik.pro>2015-03-07 04:38:24 +0300
committerdiosmosis <benaka@piwik.pro>2015-03-07 04:38:24 +0300
commite2cfb6128db69cb04209e3dedd01bae79009fa7e (patch)
tree2374c2eba3d33a8f27fb42df45a012c414a15a81 /core
parentdd88afa60014b2fc23f11cce1146d34b55264919 (diff)
parent71cacb0aca6aeb557e536c2b5b5b687f58f5465f (diff)
Merge branch 'master' into geo-attribution-task
Conflicts: misc/others/geoipUpdateRows.php
Diffstat (limited to 'core')
-rw-r--r--core/API/DataTableManipulator.php12
-rw-r--r--core/API/DataTablePostProcessor.php12
-rw-r--r--core/API/Proxy.php2
-rw-r--r--core/API/Request.php52
-rw-r--r--core/Archive.php109
-rw-r--r--core/Archive/Parameters.php14
-rw-r--r--core/ArchiveProcessor.php76
-rw-r--r--core/ArchiveProcessor/Parameters.php10
-rw-r--r--core/ArchiveProcessor/PluginsArchiver.php13
-rw-r--r--core/ArchiveProcessor/Rules.php89
-rw-r--r--core/AssetManager.php3
-rw-r--r--core/AssetManager/UIAsset/OnDiskUIAsset.php10
-rw-r--r--core/Auth.php6
-rw-r--r--core/Cache.php117
-rw-r--r--core/Cache/CacheDecorator.php54
-rw-r--r--core/Cache/CacheInterface.php29
-rw-r--r--core/Cache/LanguageAwareStaticCache.php26
-rw-r--r--core/Cache/PersistentCache.php159
-rw-r--r--core/Cache/PluginAwareStaticCache.php31
-rw-r--r--core/Cache/StaticCache.php94
-rw-r--r--core/CacheFile.php237
-rw-r--r--core/CacheId.php30
-rw-r--r--core/CliMulti.php8
-rw-r--r--core/CliMulti/Process.php4
-rw-r--r--core/CliMulti/RequestCommand.php37
-rw-r--r--core/Columns/Updater.php21
-rw-r--r--core/Common.php165
-rw-r--r--core/Config.php116
-rw-r--r--core/Console.php22
-rw-r--r--core/Container/ContainerFactory.php100
-rw-r--r--core/Container/IniConfigDefinitionSource.php36
-rw-r--r--core/Container/StaticContainer.php68
-rw-r--r--core/Cookie.php3
-rw-r--r--core/CronArchive.php281
-rw-r--r--core/DataAccess/Actions.php34
-rw-r--r--core/DataAccess/ArchiveInvalidator.php104
-rw-r--r--core/DataAccess/ArchivePurger.php8
-rw-r--r--core/DataAccess/ArchiveSelector.php28
-rw-r--r--core/DataAccess/ArchiveWriter.php2
-rw-r--r--core/DataAccess/LogAggregator.php27
-rw-r--r--core/DataAccess/LogQueryBuilder.php284
-rw-r--r--core/DataAccess/Model.php30
-rw-r--r--core/DataAccess/TableMetadata.php56
-rw-r--r--core/DataFiles/Countries.php326
-rw-r--r--core/DataFiles/Currencies.php186
-rw-r--r--core/DataFiles/LanguageToCountry.php63
-rw-r--r--core/DataFiles/Languages.php203
-rw-r--r--core/DataFiles/SearchEngines.php46
-rw-r--r--core/DataTable.php69
-rw-r--r--core/DataTable/BaseFilter.php4
-rw-r--r--core/DataTable/Filter/AddSegmentByLabel.php107
-rw-r--r--core/DataTable/Filter/AddSegmentByLabelMapping.php63
-rw-r--r--core/DataTable/Filter/AddSegmentBySegmentValue.php78
-rw-r--r--core/DataTable/Filter/AddSegmentValue.php33
-rw-r--r--core/DataTable/Filter/ColumnCallbackDeleteMetadata.php55
-rw-r--r--core/DataTable/Filter/ColumnCallbackReplace.php1
-rw-r--r--core/DataTable/Filter/PatternRecursive.php11
-rw-r--r--core/DataTable/Filter/PrependSegment.php34
-rw-r--r--core/DataTable/Filter/PrependValueToMetadata.php65
-rw-r--r--core/DataTable/Filter/ReplaceSummaryRowLabel.php4
-rw-r--r--core/DataTable/Filter/Sort.php52
-rw-r--r--core/DataTable/Filter/Truncate.php3
-rw-r--r--core/DataTable/Manager.php2
-rw-r--r--core/DataTable/Renderer/Console.php10
-rw-r--r--core/DataTable/Renderer/Json.php3
-rw-r--r--core/DataTable/Renderer/Php.php5
-rw-r--r--core/DataTable/Row.php12
-rw-r--r--core/DataTable/Row/DataTableSummaryRow.php5
-rw-r--r--core/Date.php45
-rw-r--r--core/Db.php38
-rw-r--r--core/Db/Adapter.php8
-rw-r--r--core/Db/BatchInsert.php11
-rw-r--r--core/DeviceDetectorCache.php50
-rw-r--r--core/DeviceDetectorFactory.php2
-rw-r--r--core/Error.php226
-rw-r--r--core/ErrorHandler.php131
-rw-r--r--core/Exception/DatabaseSchemaIsNewerThanCodebaseException.php14
-rw-r--r--core/Exception/ErrorException.php16
-rw-r--r--core/ExceptionHandler.php96
-rw-r--r--core/Filesystem.php32
-rw-r--r--core/FrontController.php90
-rw-r--r--core/Http.php5
-rw-r--r--core/Intl/Data/Provider/CurrencyDataProvider.php33
-rw-r--r--core/Intl/Data/Provider/LanguageDataProvider.php50
-rw-r--r--core/Intl/Data/Provider/RegionDataProvider.php57
-rw-r--r--core/Intl/Data/Resources/continents.php24
-rw-r--r--core/Intl/Data/Resources/countries-extra.php53
-rw-r--r--core/Intl/Data/Resources/countries.php272
-rw-r--r--core/Intl/Data/Resources/currencies.php183
-rw-r--r--core/Intl/Data/Resources/languages-to-countries.php60
-rw-r--r--core/Intl/Data/Resources/languages.php201
-rw-r--r--core/Intl/Locale.php19
-rw-r--r--core/Loader.php38
-rw-r--r--core/Log.php638
-rw-r--r--core/Mail.php12
-rw-r--r--core/Menu/MenuAdmin.php25
-rwxr-xr-xcore/Menu/MenuUser.php14
-rw-r--r--core/Metrics.php43
-rw-r--r--core/Metrics/Formatter.php63
-rw-r--r--core/Notification/Manager.php20
-rw-r--r--core/Period.php11
-rw-r--r--core/Period/Day.php4
-rw-r--r--core/Period/Month.php4
-rw-r--r--core/Period/Range.php80
-rw-r--r--core/Period/Week.php8
-rw-r--r--core/Piwik.php33
-rw-r--r--core/Plugin.php71
-rw-r--r--core/Plugin/ComponentFactory.php4
-rw-r--r--core/Plugin/Controller.php76
-rw-r--r--core/Plugin/ControllerAdmin.php25
-rw-r--r--core/Plugin/Dimension/ActionDimension.php12
-rw-r--r--core/Plugin/Dimension/ConversionDimension.php12
-rw-r--r--core/Plugin/Dimension/VisitDimension.php31
-rw-r--r--core/Plugin/Manager.php313
-rw-r--r--core/Plugin/Menu.php2
-rw-r--r--core/Plugin/PluginException.php21
-rw-r--r--core/Plugin/Report.php68
-rw-r--r--core/Plugin/Segment.php4
-rw-r--r--core/Plugin/Settings.php17
-rw-r--r--core/Plugin/Tasks.php38
-rw-r--r--core/Plugin/Visualization.php18
-rw-r--r--core/Profiler.php2
-rw-r--r--core/RankingQuery.php4
-rw-r--r--core/Registry.php31
-rw-r--r--core/ReportRenderer.php18
-rw-r--r--core/ReportRenderer/Html.php7
-rw-r--r--core/ReportRenderer/Pdf.php11
-rw-r--r--core/ScheduledTask.php188
-rw-r--r--core/Scheduler/Schedule/Daily.php (renamed from core/ScheduledTime/Daily.php)8
-rw-r--r--core/Scheduler/Schedule/Hourly.php (renamed from core/ScheduledTime/Hourly.php)9
-rw-r--r--core/Scheduler/Schedule/Monthly.php (renamed from core/ScheduledTime/Monthly.php)11
-rw-r--r--core/Scheduler/Schedule/Schedule.php (renamed from core/ScheduledTime.php)18
-rw-r--r--core/Scheduler/Schedule/Weekly.php (renamed from core/ScheduledTime/Weekly.php)9
-rw-r--r--core/Scheduler/Scheduler.php192
-rw-r--r--core/Scheduler/Task.php200
-rw-r--r--core/Scheduler/TaskLoader.php40
-rw-r--r--core/Scheduler/Timetable.php (renamed from core/ScheduledTaskTimetable.php)20
-rw-r--r--core/Segment.php255
-rw-r--r--core/Segment/SegmentExpression.php (renamed from core/SegmentExpression.php)9
-rw-r--r--core/Session/SessionNamespace.php3
-rw-r--r--core/Settings/Manager.php43
-rw-r--r--core/Settings/Storage/StaticStorage.php34
-rw-r--r--core/SettingsPiwik.php114
-rw-r--r--core/SettingsServer.php16
-rw-r--r--core/Site.php18
-rw-r--r--core/TaskScheduler.php145
-rw-r--r--core/Tracker.php873
-rw-r--r--core/Tracker/Cache.php54
-rw-r--r--core/Tracker/Db.php64
-rw-r--r--core/Tracker/Db/Mysqli.php12
-rw-r--r--core/Tracker/Db/Pdo/Mysql.php10
-rw-r--r--core/Tracker/GoalManager.php3
-rw-r--r--core/Tracker/Handler.php118
-rw-r--r--core/Tracker/Handler/Factory.php43
-rw-r--r--core/Tracker/Model.php58
-rw-r--r--core/Tracker/PageUrl.php55
-rw-r--r--core/Tracker/Request.php120
-rw-r--r--core/Tracker/RequestSet.php258
-rw-r--r--core/Tracker/Response.php175
-rw-r--r--core/Tracker/ScheduledTasksRunner.php103
-rw-r--r--core/Tracker/Settings.php13
-rw-r--r--core/Tracker/SettingsStorage.php34
-rw-r--r--core/Tracker/TableLogAction.php2
-rw-r--r--core/Tracker/TrackerConfig.php39
-rw-r--r--core/Tracker/Visit.php130
-rw-r--r--core/Tracker/Visit/Factory.php48
-rw-r--r--core/Tracker/VisitExcluded.php15
-rw-r--r--core/Translate.php178
-rw-r--r--core/Translate/Filter/ByBaseTranslations.php64
-rw-r--r--core/Translate/Filter/ByParameterCount.php87
-rw-r--r--core/Translate/Filter/EmptyTranslations.php46
-rw-r--r--core/Translate/Filter/EncodedEntities.php43
-rw-r--r--core/Translate/Filter/FilterAbstract.php36
-rw-r--r--core/Translate/Filter/UnnecassaryWhitespaces.php64
-rw-r--r--core/Translate/Validate/CoreTranslations.php94
-rw-r--r--core/Translate/Validate/NoScripts.php41
-rw-r--r--core/Translate/Validate/ValidateAbstract.php37
-rw-r--r--core/Translate/Writer.php385
-rw-r--r--core/Translation/Loader/DevelopmentLoader.php72
-rw-r--r--core/Translation/Loader/JsonFileLoader.php65
-rw-r--r--core/Translation/Loader/LoaderCache.php65
-rw-r--r--core/Translation/Loader/LoaderInterface.php23
-rw-r--r--core/Translation/Translator.php255
-rwxr-xr-xcore/Twig.php17
-rw-r--r--core/Updater.php21
-rw-r--r--core/Updates.php3
-rw-r--r--core/Updates/2.10.0-b10.php49
-rw-r--r--core/Updates/2.10.0-b4.php29
-rw-r--r--core/Updates/2.10.0-b5.php (renamed from core/Updates/2.10.0-b1.php)58
-rw-r--r--core/Updates/2.10.0-b7.php43
-rw-r--r--core/Updates/2.10.0-b8.php26
-rw-r--r--core/Updates/2.11.0-b2.php66
-rw-r--r--core/Updates/2.11.0-b4.php49
-rw-r--r--core/Updates/2.11.0-b5.php23
-rw-r--r--core/Updates/2.11.1-b4.php39
-rw-r--r--core/Url.php29
-rw-r--r--core/UrlHelper.php8
-rw-r--r--core/Version.php18
-rw-r--r--core/View.php22
-rw-r--r--core/ViewDataTable/Config.php6
-rw-r--r--core/ViewDataTable/Factory.php77
-rw-r--r--core/ViewDataTable/Manager.php11
-rw-r--r--core/WidgetsList.php23
-rw-r--r--core/bootstrap.php49
-rw-r--r--core/dispatch.php19
205 files changed, 7329 insertions, 6023 deletions
diff --git a/core/API/DataTableManipulator.php b/core/API/DataTableManipulator.php
index a1acc6e5b5..862f2db087 100644
--- a/core/API/DataTableManipulator.php
+++ b/core/API/DataTableManipulator.php
@@ -124,7 +124,7 @@ abstract class DataTableManipulator
}
}
- $method = $this->getApiMethodForSubtable();
+ $method = $this->getApiMethodForSubtable($request);
return $this->callApiAndReturnDataTable($this->apiModule, $method, $request);
}
@@ -144,10 +144,16 @@ abstract class DataTableManipulator
* @throws Exception
* @return string
*/
- private function getApiMethodForSubtable()
+ private function getApiMethodForSubtable($request)
{
if (!$this->apiMethodForSubtable) {
- $meta = API::getInstance()->getMetadata('all', $this->apiModule, $this->apiMethod);
+ if (!empty($request['idSite'])) {
+ $idSite = $request['idSite'];
+ } else {
+ $idSite = 'all';
+ }
+
+ $meta = API::getInstance()->getMetadata($idSite, $this->apiModule, $this->apiMethod);
if (empty($meta)) {
throw new Exception(sprintf(
diff --git a/core/API/DataTablePostProcessor.php b/core/API/DataTablePostProcessor.php
index 352771464e..999b3fe339 100644
--- a/core/API/DataTablePostProcessor.php
+++ b/core/API/DataTablePostProcessor.php
@@ -95,15 +95,23 @@ class DataTablePostProcessor
// we automatically safe decode all datatable labels (against xss)
$dataTable->queueFilter('SafeDecodeLabel');
+ $dataTable = $this->convertSegmentValueToSegment($dataTable);
$dataTable = $this->applyQueuedFilters($dataTable);
$dataTable = $this->applyRequestedColumnDeletion($dataTable);
$dataTable = $this->applyLabelFilter($dataTable);
-
$dataTable = $this->applyMetricsFormatting($dataTable);
return $dataTable;
}
+ private function convertSegmentValueToSegment(DataTableInterface $dataTable)
+ {
+ $dataTable->filter('AddSegmentBySegmentValue', array($this->report));
+ $dataTable->filter('ColumnCallbackDeleteMetadata', array('segmentValue'));
+
+ return $dataTable;
+ }
+
/**
* @param DataTableInterface $dataTable
* @return DataTableInterface
@@ -118,6 +126,8 @@ class DataTablePostProcessor
$pivotByColumn = Common::getRequestVar('pivotByColumn', false, 'string', $this->request);
$pivotByColumnLimit = Common::getRequestVar('pivotByColumnLimit', false, 'int', $this->request);
+ $dataTable->filter('ColumnCallbackDeleteMetadata', array('segmentValue'));
+ $dataTable->filter('ColumnCallbackDeleteMetadata', array('segment'));
$dataTable->filter('PivotByDimension', array($reportId, $pivotBy, $pivotByColumn, $pivotByColumnLimit,
PivotByDimension::isSegmentFetchingEnabledInConfig()));
}
diff --git a/core/API/Proxy.php b/core/API/Proxy.php
index f442949a3c..0345693b62 100644
--- a/core/API/Proxy.php
+++ b/core/API/Proxy.php
@@ -415,7 +415,7 @@ class Proxy extends Singleton
}
/**
- * Includes the class API by looking up plugins/UserSettings/API.php
+ * Includes the class API by looking up plugins/xxx/API.php
*
* @param string $fileName api class name eg. "API"
* @throws Exception
diff --git a/core/API/Request.php b/core/API/Request.php
index 23677e45fb..1db4009606 100644
--- a/core/API/Request.php
+++ b/core/API/Request.php
@@ -46,7 +46,7 @@ use Piwik\Log;
*
* **Basic Usage**
*
- * $request = new Request('method=UserSettings.getLanguage&idSite=1&date=yesterday&period=week'
+ * $request = new Request('method=UserLanguage.getLanguage&idSite=1&date=yesterday&period=week'
* . '&format=xml&filter_limit=5&filter_offset=0')
* $result = $request->process();
* echo $result;
@@ -54,7 +54,7 @@ use Piwik\Log;
* **Getting a unrendered DataTable**
*
* // use the convenience method 'processRequest'
- * $dataTable = Request::processRequest('UserSettings.getLanguage', array(
+ * $dataTable = Request::processRequest('UserLanguage.getLanguage', array(
* 'idSite' => 1,
* 'date' => 'yesterday',
* 'period' => 'week',
@@ -77,8 +77,8 @@ class Request
* mappings. The current query parameters (everything in `$_GET` and `$_POST`) are
* forwarded to request array before it is returned.
*
- * @param string|array $request The base request string or array, eg,
- * `'module=UserSettings&action=getLanguage'`.
+ * @param string|array|null $request The base request string or array, eg,
+ * `'module=UserLanguage&action=getLanguage'`.
* @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded
* from this. Defaults to `$_GET + $_POST`.
* @return array
@@ -125,7 +125,7 @@ class Request
* Constructor.
*
* @param string|array $request Query string that defines the API call (must at least contain a **method** parameter),
- * eg, `'method=UserSettings.getLanguage&idSite=1&date=yesterday&period=week&format=xml'`
+ * eg, `'method=UserLanguage.getLanguage&idSite=1&date=yesterday&period=week&format=xml'`
* If a request is not provided, then we use the values in the `$_GET` and `$_POST`
* superglobals.
* @param array $defaultRequest Default query parameters. If a query parameter is absent in `$request`, it will be loaded
@@ -135,6 +135,7 @@ class Request
{
$this->request = self::getRequestArrayFromString($request, $defaultRequest);
$this->sanitizeRequest();
+ $this->renameModuleAndActionInRequest();
}
/**
@@ -142,19 +143,23 @@ class Request
* we rewrite to correct renamed plugin: Referrers
*
* @param $module
- * @return string
+ * @param $action
+ * @return array( $module, $action )
* @ignore
*/
- public static function renameModule($module)
+ public static function getRenamedModuleAndAction($module, $action)
{
- $moduleToRedirect = array(
- 'Referers' => 'Referrers',
- 'PDFReports' => 'ScheduledReports',
- );
- if (isset($moduleToRedirect[$module])) {
- return $moduleToRedirect[$module];
- }
- return $module;
+ /**
+ * This event is posted in the Request dispatcher and can be used
+ * to overwrite the Module and Action to dispatch.
+ * This is useful when some Controller methods or API methods have been renamed or moved to another plugin.
+ *
+ * @param $module string
+ * @param $action string
+ */
+ Piwik::postEvent('Request.getRenamedModuleAndAction', array(&$module, &$action));
+
+ return array($module, $action);
}
/**
@@ -213,12 +218,12 @@ class Request
list($module, $method) = $this->extractModuleAndMethod($moduleMethod);
- $module = $this->renameModule($module);
+ list($module, $method) = self::getRenamedModuleAndAction($module, $method);
if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated($module)) {
throw new PluginDeactivatedException($module);
}
- $apiClassName = $this->getClassNameAPI($module);
+ $apiClassName = self::getClassNameAPI($module);
self::reloadAuthUsingTokenAuth($this->request);
@@ -265,7 +270,7 @@ class Request
* query parameter is found in the request.
*
* Plugins that provide authentication capabilities should subscribe to this event
- * and make sure the global authentication object (the object returned by `Registry::get('auth')`)
+ * and make sure the global authentication object (the object returned by `StaticContainer::get('Piwik\Auth')`)
* is setup to use `$token_auth` when its `authenticate()` method is executed.
*
* @param string $token_auth The value of the **token_auth** query parameter.
@@ -412,4 +417,15 @@ class Request
}
return $segmentRaw;
}
+
+ private function renameModuleAndActionInRequest()
+ {
+ if (empty($this->request['apiModule'])) {
+ return;
+ }
+ if (empty($this->request['apiAction'])) {
+ $this->request['apiAction'] = null;
+ }
+ list($this->request['apiModule'], $this->request['apiAction']) = $this->getRenamedModuleAndAction($this->request['apiModule'], $this->request['apiAction']);
+ }
}
diff --git a/core/Archive.php b/core/Archive.php
index b407465a45..1931b6a343 100644
--- a/core/Archive.php
+++ b/core/Archive.php
@@ -10,6 +10,7 @@ namespace Piwik;
use Piwik\Archive\Parameters;
use Piwik\ArchiveProcessor\Rules;
+use Piwik\DataAccess\ArchiveInvalidator;
use Piwik\DataAccess\ArchiveSelector;
use Piwik\Period\Factory as PeriodFactory;
@@ -161,6 +162,11 @@ class Archive
private $params;
/**
+ * @var \Piwik\Cache\Cache
+ */
+ private static $cache;
+
+ /**
* @param Parameters $params
* @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.
@@ -190,10 +196,9 @@ 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.
- * @param bool $skipAggregationOfSubTables Whether the archive, when it is processed, should also aggregate all sub-tables
* @return Archive
*/
- public static function build($idSites, $period, $strDate, $segment = false, $_restrictSitesToLogin = false, $skipAggregationOfSubTables = false)
+ public static function build($idSites, $period, $strDate, $segment = false, $_restrictSitesToLogin = false)
{
$websiteIds = Site::getIdSitesFromIdSitesString($idSites, $_restrictSitesToLogin);
@@ -214,7 +219,7 @@ class Archive
$idSiteIsAll = $idSites == self::REQUEST_ALL_WEBSITES_FLAG;
$isMultipleDate = Period::isMultiplePeriod($strDate, $period);
- return Archive::factory($segment, $allPeriods, $websiteIds, $idSiteIsAll, $isMultipleDate, $skipAggregationOfSubTables);
+ return Archive::factory($segment, $allPeriods, $websiteIds, $idSiteIsAll, $isMultipleDate);
}
/**
@@ -236,11 +241,10 @@ class Archive
* @param bool $isMultipleDate Whether multiple dates are being queried or not. If true, then
* the result of querying functions will be indexed by period,
* regardless of whether `count($periods) == 1`.
- * @param bool $skipAggregationOfSubTables Whether the archive should skip aggregation of all sub-tables
*
* @return Archive
*/
- public static function factory(Segment $segment, array $periods, array $idSites, $idSiteIsAll = false, $isMultipleDate = false, $skipAggregationOfSubTables = false)
+ public static function factory(Segment $segment, array $periods, array $idSites, $idSiteIsAll = false, $isMultipleDate = false)
{
$forceIndexedBySite = false;
$forceIndexedByDate = false;
@@ -253,7 +257,7 @@ class Archive
$forceIndexedByDate = true;
}
- $params = new Parameters($idSites, $periods, $segment, $skipAggregationOfSubTables);
+ $params = new Parameters($idSites, $periods, $segment);
return new Archive($params, $forceIndexedBySite, $forceIndexedByDate);
}
@@ -443,7 +447,6 @@ class Archive
* @param string $segment @see {@link build()}
* @param bool $expanded If true, loads all subtables. See {@link getDataTableExpanded()}
* @param int|null $idSubtable See {@link getDataTableExpanded()}
- * @param bool $skipAggregationOfSubTables Whether or not we should skip the aggregation of all sub-tables and only aggregate parent DataTable.
* @param int|null $depth See {@link getDataTableExpanded()}
* @throws \Exception
* @return DataTable|DataTable\Map See {@link getDataTable()} and
@@ -451,15 +454,11 @@ class Archive
* information
*/
public static function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded,
- $idSubtable = null, $skipAggregationOfSubTables = false, $depth = null)
+ $idSubtable = null, $depth = null)
{
Piwik::checkUserHasViewAccess($idSite);
- if ($skipAggregationOfSubTables && ($expanded || $idSubtable)) {
- throw new \Exception("Not expected to skipAggregationOfSubTables when expanded=1 or idSubtable is set.");
- }
-
- $archive = Archive::build($idSite, $period, $date, $segment, $_restrictSitesToLogin = false, $skipAggregationOfSubTables);
+ $archive = Archive::build($idSite, $period, $date, $segment, $_restrictSitesToLogin = false);
if ($idSubtable === false) {
$idSubtable = null;
}
@@ -480,6 +479,69 @@ class Archive
return $recordName . "_" . $id;
}
+ private function getSiteIdsThatAreRequestedInThisArchiveButWereNotInvalidatedYet()
+ {
+ if (is_null(self::$cache)) {
+ self::$cache = Cache::getTransientCache();
+ }
+
+ $id = 'Archive.SiteIdsOfRememberedReportsInvalidated';
+
+ if (!self::$cache->contains($id)) {
+ self::$cache->save($id, array());
+ }
+
+ $siteIdsAlreadyHandled = self::$cache->fetch($id);
+ $siteIdsRequested = $this->params->getIdSites();
+
+ foreach ($siteIdsRequested as $index => $siteIdRequested) {
+ $siteIdRequested = (int) $siteIdRequested;
+
+ if (in_array($siteIdRequested, $siteIdsAlreadyHandled)) {
+ unset($siteIdsRequested[$index]); // was already handled previously, do not do it again
+ } else {
+ $siteIdsAlreadyHandled[] = $siteIdRequested; // we will handle this id this time
+ }
+ }
+
+ self::$cache->save($id, $siteIdsAlreadyHandled);
+
+ return $siteIdsRequested;
+ }
+
+ private function invalidatedReportsIfNeeded()
+ {
+ $siteIdsRequested = $this->getSiteIdsThatAreRequestedInThisArchiveButWereNotInvalidatedYet();
+
+ if (empty($siteIdsRequested)) {
+ return; // all requested site ids were already handled
+ }
+
+ $invalidator = new ArchiveInvalidator();
+ $sitesPerDays = $invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
+
+ foreach ($sitesPerDays as $date => $siteIds) {
+ if (empty($siteIds)) {
+ continue;
+ }
+
+ $siteIdsToActuallyInvalidate = array_intersect($siteIds, $siteIdsRequested);
+
+ if (empty($siteIdsToActuallyInvalidate)) {
+ continue; // all site ids that should be handled are already handled
+ }
+
+ try {
+ $invalidator->markArchivesAsInvalidated($siteIdsToActuallyInvalidate, $date, false);
+ } catch (\Exception $e) {
+ Site::clearCache();
+ throw $e;
+ }
+ }
+
+ Site::clearCache();
+ }
+
/**
* Queries archive tables for data and returns the result.
* @param array|string $archiveNames
@@ -489,6 +551,7 @@ class Archive
*/
private function get($archiveNames, $archiveDataType, $idSubtable = null)
{
+
if (!is_array($archiveNames)) {
$archiveNames = array($archiveNames);
}
@@ -537,6 +600,9 @@ class Archive
* queried. This function will use the idarchive cache if it has the right data,
* query archive tables for IDs w/o launching archiving, or launch archiving and
* get the idarchive from ArchiveProcessor instances.
+ *
+ * @param string $archiveNames
+ * @return array
*/
private function getArchiveIds($archiveNames)
{
@@ -586,6 +652,8 @@ class Archive
*/
private function cacheArchiveIdsAfterLaunching($archiveGroups, $plugins)
{
+ $this->invalidatedReportsIfNeeded();
+
$today = Date::today();
foreach ($this->params->getPeriods() as $period) {
@@ -599,14 +667,14 @@ class Archive
// we already know there are no stats for this period
// we add one day to make sure we don't miss the day of the website creation
if ($twoDaysAfterPeriod->isEarlier($site->getCreationDate())) {
- Log::verbose("Archive site %s, %s (%s) skipped, archive is before the website was created.",
+ Log::debug("Archive site %s, %s (%s) skipped, archive is before the website was created.",
$idSite, $period->getLabel(), $period->getPrettyString());
continue;
}
// if the starting date is in the future we know there is no visiidsite = ?t
if ($twoDaysBeforePeriod->isLater($today)) {
- Log::verbose("Archive site %s, %s (%s) skipped, archive is after today.",
+ Log::debug("Archive site %s, %s (%s) skipped, archive is after today.",
$idSite, $period->getLabel(), $period->getPrettyString());
continue;
}
@@ -626,7 +694,7 @@ class Archive
private function cacheArchiveIdsWithoutLaunching($plugins)
{
$idarchivesByReport = ArchiveSelector::getArchiveIds(
- $this->params->getIdSites(), $this->params->getPeriods(), $this->params->getSegment(), $plugins, $this->params->isSkipAggregationOfSubTables());
+ $this->params->getIdSites(), $this->params->getPeriods(), $this->params->getSegment(), $plugins);
// initialize archive ID cache for each report
foreach ($plugins as $plugin) {
@@ -654,8 +722,7 @@ class Archive
$this->params->getIdSites(),
$this->params->getSegment(),
$this->getPeriodLabel(),
- $plugin,
- $this->params->isSkipAggregationOfSubTables()
+ $plugin
);
}
@@ -722,6 +789,8 @@ class Archive
* If this function is not called, then periods with no visits will not add
* entries to the cache. If the archive is used again, SQL will be executed to
* try and find the archive IDs even though we know there are none.
+ *
+ * @param string $doneFlag
*/
private function initializeArchiveIdCache($doneFlag)
{
@@ -757,7 +826,7 @@ class Archive
/**
* Returns the name of the plugin that archives a given report.
*
- * @param string $report Archive data name, eg, `'nb_visits'`, `'UserSettings_...'`, etc.
+ * @param string $report Archive data name, eg, `'nb_visits'`, `'DevicesDetection_...'`, etc.
* @return string Plugin name.
* @throws \Exception If a plugin cannot be found or if the plugin for the report isn't
* activated.
@@ -791,7 +860,7 @@ class Archive
*/
private function prepareArchive(array $archiveGroups, Site $site, Period $period)
{
- $parameters = new ArchiveProcessor\Parameters($site, $period, $this->params->getSegment(), $this->params->isSkipAggregationOfSubTables());
+ $parameters = new ArchiveProcessor\Parameters($site, $period, $this->params->getSegment());
$archiveLoader = new ArchiveProcessor\Loader($parameters);
$periodString = $period->getRangeString();
diff --git a/core/Archive/Parameters.php b/core/Archive/Parameters.php
index d1a540dce0..ad7bef9eb6 100644
--- a/core/Archive/Parameters.php
+++ b/core/Archive/Parameters.php
@@ -35,22 +35,16 @@ class Parameters
*/
private $segment;
- /**
- * @var bool
- */
- private $skipAggregationOfSubTables;
-
public function getSegment()
{
return $this->segment;
}
- public function __construct($idSites, $periods, Segment $segment, $skipAggregationOfSubTables)
+ public function __construct($idSites, $periods, Segment $segment)
{
$this->idSites = $idSites;
$this->periods = $periods;
$this->segment = $segment;
- $this->skipAggregationOfSubTables = $skipAggregationOfSubTables;
}
public function getPeriods()
@@ -62,11 +56,5 @@ class Parameters
{
return $this->idSites;
}
-
- public function isSkipAggregationOfSubTables()
- {
- return $this->skipAggregationOfSubTables;
- }
-
}
diff --git a/core/ArchiveProcessor.php b/core/ArchiveProcessor.php
index c93e913e0c..58282e72d1 100644
--- a/core/ArchiveProcessor.php
+++ b/core/ArchiveProcessor.php
@@ -112,8 +112,6 @@ class ArchiveProcessor
*/
private $skipUniqueVisitorsCalculationForMultipleSites = true;
- const SKIP_UNIQUE_VISITORS_FOR_MULTIPLE_SITES = 'enable_processing_unique_visitors_multiple_sites';
-
public function __construct(Parameters $params, ArchiveWriter $archiveWriter)
{
$this->params = $params;
@@ -216,14 +214,9 @@ class ArchiveProcessor
$table = $this->aggregateDataTableRecord($recordName, $columnsAggregationOperation, $columnsToRenameAfterAggregation);
- $rowsCount = $table->getRowsCount();
- $nameToCount[$recordName]['level0'] = $rowsCount;
+ $nameToCount[$recordName]['level0'] = $table->getRowsCount();
- $rowsCountRecursive = $rowsCount;
- if ($this->isAggregateSubTables()) {
- $rowsCountRecursive = $table->getRowsCountRecursive();
- }
- $nameToCount[$recordName]['recursive'] = $rowsCountRecursive;
+ $nameToCount[$recordName]['recursive'] = $table->getRowsCountRecursive();
$blob = $table->getSerialized($maximumRowsInDataTableLevelZero, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation);
Common::destroy($table);
@@ -348,14 +341,8 @@ class ArchiveProcessor
*/
protected function aggregateDataTableRecord($name, $columnsAggregationOperation = null, $columnsToRenameAfterAggregation = null)
{
- if ($this->isAggregateSubTables()) {
- // By default we shall aggregate all sub-tables.
- $dataTable = $this->getArchive()->getDataTableExpanded($name, $idSubTable = null, $depth = null, $addMetadataSubtableId = false);
- } else {
- // In some cases (eg. Actions plugin when period=range),
- // for better performance we will only aggregate the parent table
- $dataTable = $this->getArchive()->getDataTable($name, $idSubTable = null);
- }
+ // By default we shall aggregate all sub-tables.
+ $dataTable = $this->getArchive()->getDataTableExpanded($name, $idSubTable = null, $depth = null, $addMetadataSubtableId = false);
if ($dataTable instanceof Map) {
// see https://github.com/piwik/piwik/issues/4377
@@ -391,19 +378,38 @@ class ArchiveProcessor
) {
return;
}
- if ($row->getColumn('nb_uniq_visitors') !== false
- || $row->getColumn('nb_users') !== false
+
+ if ($row->getColumn('nb_uniq_visitors') === false
+ && $row->getColumn('nb_users') === false
) {
- if (SettingsPiwik::isUniqueVisitorsEnabled($this->getParams()->getPeriod()->getLabel())) {
- $metrics = array(Metrics::INDEX_NB_UNIQ_VISITORS, Metrics::INDEX_NB_USERS);
- $uniques = $this->computeNbUniques( $metrics );
- $row->setColumn('nb_uniq_visitors', $uniques[Metrics::INDEX_NB_UNIQ_VISITORS]);
- $row->setColumn('nb_users', $uniques[Metrics::INDEX_NB_USERS]);
- } else {
- $row->deleteColumn('nb_uniq_visitors');
- $row->deleteColumn('nb_users');
+ return;
+ }
+
+ if (!SettingsPiwik::isUniqueVisitorsEnabled($this->getParams()->getPeriod()->getLabel())) {
+ $row->deleteColumn('nb_uniq_visitors');
+ $row->deleteColumn('nb_users');
+ return;
+ }
+
+ $metrics = array(
+ Metrics::INDEX_NB_USERS
+ );
+
+ if($this->getParams()->isSingleSite()) {
+ $uniqueVisitorsMetric = Metrics::INDEX_NB_UNIQ_VISITORS;
+ } else {
+ if(!SettingsPiwik::isSameFingerprintAcrossWebsites()) {
+ throw new Exception("Processing unique visitors across websites is enabled for this instance,
+ but to process this metric you must first set enable_fingerprinting_across_websites=1
+ in the config file, under the [Tracker] section.");
}
+ $uniqueVisitorsMetric = Metrics::INDEX_NB_UNIQ_FINGERPRINTS;
}
+ $metrics[] = $uniqueVisitorsMetric;
+
+ $uniques = $this->computeNbUniques( $metrics );
+ $row->setColumn('nb_uniq_visitors', $uniques[$uniqueVisitorsMetric]);
+ $row->setColumn('nb_users', $uniques[Metrics::INDEX_NB_USERS]);
}
protected function guessOperationForColumn($column)
@@ -424,7 +430,7 @@ class ArchiveProcessor
* since unique visitors cannot be summed like other metrics.
*
* @param array Metrics Ids for which to aggregates count of values
- * @return int
+ * @return array of metrics, where the key is metricid and the value is the metric value
*/
protected function computeNbUniques($metrics)
{
@@ -454,7 +460,7 @@ class ArchiveProcessor
// as $date => $tableToSum
$this->aggregatedDataTableMapsAsOne($data, $table);
} else {
- $table->addDataTable($data, $this->isAggregateSubTables());
+ $table->addDataTable($data);
}
return $table;
@@ -471,7 +477,7 @@ class ArchiveProcessor
if ($tableToAggregate instanceof Map) {
$this->aggregatedDataTableMapsAsOne($tableToAggregate, $aggregated);
} else {
- $aggregated->addDataTable($tableToAggregate, $this->isAggregateSubTables());
+ $aggregated->addDataTable($tableToAggregate);
}
}
}
@@ -487,7 +493,7 @@ class ArchiveProcessor
}
foreach ($columnsToRenameAfterAggregation as $oldName => $newName) {
- $table->renameColumn($oldName, $newName, $this->isAggregateSubTables());
+ $table->renameColumn($oldName, $newName);
}
}
@@ -523,12 +529,4 @@ class ArchiveProcessor
return $metrics;
}
-
- /**
- * @return bool
- */
- protected function isAggregateSubTables()
- {
- return !$this->getParams()->isSkipAggregationOfSubTables();
- }
}
diff --git a/core/ArchiveProcessor/Parameters.php b/core/ArchiveProcessor/Parameters.php
index 1528d8210c..4edd1f4b58 100644
--- a/core/ArchiveProcessor/Parameters.php
+++ b/core/ArchiveProcessor/Parameters.php
@@ -48,12 +48,11 @@ class Parameters
*
* @ignore
*/
- public function __construct(Site $site, Period $period, Segment $segment, $skipAggregationOfSubTables = false)
+ public function __construct(Site $site, Period $period, Segment $segment)
{
$this->site = $site;
$this->period = $period;
$this->segment = $segment;
- $this->skipAggregationOfSubTables = $skipAggregationOfSubTables;
}
/**
@@ -169,18 +168,13 @@ class Parameters
return count($this->getIdSites()) == 1;
}
- public function isSkipAggregationOfSubTables()
- {
- return $this->skipAggregationOfSubTables;
- }
-
public function logStatusDebug($isTemporary)
{
$temporary = 'definitive archive';
if ($isTemporary) {
$temporary = 'temporary archive';
}
- Log::verbose(
+ Log::debug(
"%s archive, idSite = %d (%s), segment '%s', report = '%s', UTC datetime [%s -> %s]",
$this->getPeriod()->getLabel(),
$this->getSite()->getId(),
diff --git a/core/ArchiveProcessor/PluginsArchiver.php b/core/ArchiveProcessor/PluginsArchiver.php
index d1bb950d38..7487128728 100644
--- a/core/ArchiveProcessor/PluginsArchiver.php
+++ b/core/ArchiveProcessor/PluginsArchiver.php
@@ -16,6 +16,7 @@ use Piwik\DataTable\Manager;
use Piwik\Metrics;
use Piwik\Plugin\Archiver;
use Piwik\Log;
+use Piwik\Timer;
/**
* This class creates the Archiver objects found in plugins and will trigger aggregation,
@@ -96,11 +97,12 @@ class PluginsArchiver
$archiver = new $archiverClass($this->archiveProcessor);
if (!$archiver->isEnabled()) {
- Log::verbose("PluginsArchiver::%s: Skipping archiving for plugin '%s'.", __FUNCTION__, $pluginName);
+ Log::debug("PluginsArchiver::%s: Skipping archiving for plugin '%s'.", __FUNCTION__, $pluginName);
continue;
}
if ($this->shouldProcessReportsForPlugin($pluginName)) {
+ $timer = new Timer();
if ($this->isSingleSiteDayArchive) {
Log::debug("PluginsArchiver::%s: Archiving day reports for plugin '%s'.", __FUNCTION__, $pluginName);
@@ -110,8 +112,15 @@ class PluginsArchiver
$archiver->aggregateMultipleReports();
}
+
+ Log::debug("PluginsArchiver::%s: %s while archiving %s reports for plugin '%s'.",
+ __FUNCTION__,
+ $timer->getMemoryLeak(),
+ $this->params->getPeriod()->getLabel(),
+ $pluginName
+ );
} else {
- Log::verbose("PluginsArchiver::%s: Not archiving reports for plugin '%s'.", __FUNCTION__, $pluginName);
+ Log::debug("PluginsArchiver::%s: Not archiving reports for plugin '%s'.", __FUNCTION__, $pluginName);
}
Manager::getInstance()->deleteAll($latestUsedTableId);
diff --git a/core/ArchiveProcessor/Rules.php b/core/ArchiveProcessor/Rules.php
index 1334ad1f9a..06b38aa27d 100644
--- a/core/ArchiveProcessor/Rules.php
+++ b/core/ArchiveProcessor/Rules.php
@@ -10,6 +10,7 @@ namespace Piwik\ArchiveProcessor;
use Exception;
use Piwik\Config;
+use Piwik\Container\StaticContainer;
use Piwik\DataAccess\ArchiveWriter;
use Piwik\Date;
use Piwik\Log;
@@ -45,13 +46,12 @@ class Rules
* @param Segment $segment
* @param string $periodLabel
* @param string $plugin
- * @param bool $isSkipAggregationOfSubTables
* @return string
*/
- public static function getDoneStringFlagFor(array $idSites, $segment, $periodLabel, $plugin, $isSkipAggregationOfSubTables)
+ public static function getDoneStringFlagFor(array $idSites, $segment, $periodLabel, $plugin)
{
if (!self::shouldProcessReportsAllPlugins($idSites, $segment, $periodLabel)) {
- return self::getDoneFlagArchiveContainsOnePlugin($segment, $plugin, $isSkipAggregationOfSubTables);
+ return self::getDoneFlagArchiveContainsOnePlugin($segment, $plugin);
}
return self::getDoneFlagArchiveContainsAllPlugins($segment);
}
@@ -82,10 +82,9 @@ class Rules
return $segmentsToProcess;
}
- public static function getDoneFlagArchiveContainsOnePlugin(Segment $segment, $plugin, $isSkipAggregationOfSubTables = false)
+ public static function getDoneFlagArchiveContainsOnePlugin(Segment $segment, $plugin)
{
- $partial = self::isFlagArchivePartial($plugin, $isSkipAggregationOfSubTables);
- return 'done' . $segment->getHash() . '.' . $plugin . $partial ;
+ return 'done' . $segment->getHash() . '.' . $plugin ;
}
private static function getDoneFlagArchiveContainsAllPlugins(Segment $segment)
@@ -94,29 +93,13 @@ class Rules
}
/**
- * @param $plugin
- * @param $isSkipAggregationOfSubTables
- * @return string
- */
- private static function isFlagArchivePartial($plugin, $isSkipAggregationOfSubTables)
- {
- $partialArchive = '';
- if ($plugin != "VisitsSummary" // VisitsSummary is always called when segmenting and should not have its own .partial archive
- && $isSkipAggregationOfSubTables
- ) {
- $partialArchive = '.partial';
- }
- return $partialArchive;
- }
-
- /**
* Return done flags used to tell how the archiving process for a specific archive was completed,
*
* @param array $plugins
* @param $segment
* @return array
*/
- public static function getDoneFlags(array $plugins, Segment $segment, $isSkipAggregationOfSubTables)
+ public static function getDoneFlags(array $plugins, Segment $segment)
{
$doneFlags = array();
$doneAllPlugins = self::getDoneFlagArchiveContainsAllPlugins($segment);
@@ -124,7 +107,7 @@ class Rules
$plugins = array_unique($plugins);
foreach ($plugins as $plugin) {
- $doneOnePlugin = self::getDoneFlagArchiveContainsOnePlugin($segment, $plugin, $isSkipAggregationOfSubTables);
+ $doneOnePlugin = self::getDoneFlagArchiveContainsOnePlugin($segment, $plugin);
$doneFlags[$plugin] = $doneOnePlugin;
}
return $doneFlags;
@@ -141,38 +124,24 @@ class Rules
*/
public static function shouldPurgeOutdatedArchives(Date $date)
{
- $key = self::FLAG_TABLE_PURGED . "blob_" . $date->toString('Y_m');
- $timestamp = Option::get($key);
-
- // we shall purge temporary archives after their timeout is finished, plus an extra 6 hours
- // in case archiving is disabled or run once a day, we give it this extra time to run
- // and re-process more recent records...
- $temporaryArchivingTimeout = self::getTodayArchiveTimeToLive();
- $hoursBetweenPurge = 6;
- $purgeEveryNSeconds = max($temporaryArchivingTimeout, $hoursBetweenPurge * 3600);
-
// we only delete archives if we are able to process them, otherwise, the browser might process reports
// when &segment= is specified (or custom date range) and would below, delete temporary archives that the
// browser is not able to process until next cron run (which could be more than 1 hour away)
- if (self::isRequestAuthorizedToArchive()
- && (!$timestamp
- || $timestamp < time() - $purgeEveryNSeconds)
- ) {
- Option::set($key, time());
-
- if (self::isBrowserTriggerEnabled()) {
- // If Browser Archiving is enabled, it is likely there are many more temporary archives
- // We delete more often which is safe, since reports are re-processed on demand
- $purgeArchivesOlderThan = Date::factory(time() - 2 * $temporaryArchivingTimeout)->getDateTime();
- } else {
- // If cron core:archive command is building the reports, we should keep all temporary reports from today
- $purgeArchivesOlderThan = Date::factory('yesterday')->getDateTime();
- }
- return $purgeArchivesOlderThan;
+ if (! self::isRequestAuthorizedToArchive()){
+ Log::info("Purging temporary archives: skipped (no authorization)");
+ return false;
}
- Log::info("Purging temporary archives: skipped.");
- return false;
+ $temporaryArchivingTimeout = self::getTodayArchiveTimeToLive();
+
+ if (self::isBrowserTriggerEnabled()) {
+ // If Browser Archiving is enabled, it is likely there are many more temporary archives
+ // We delete more often which is safe, since reports are re-processed on demand
+ return Date::factory(time() - 2 * $temporaryArchivingTimeout)->getDateTime();
+ }
+
+ // If cron core:archive command is building the reports, we should keep all temporary reports from today
+ return Date::factory('yesterday')->getDateTime();
}
public static function getMinTimeProcessedForTemporaryArchive(
@@ -228,18 +197,28 @@ class Rules
public static function isArchivingDisabledFor(array $idSites, Segment $segment, $periodLabel)
{
+ $generalConfig = Config::getInstance()->General;
+
if ($periodLabel == 'range') {
- return false;
+ if (!isset($generalConfig['archiving_range_force_on_browser_request'])
+ || $generalConfig['archiving_range_force_on_browser_request'] != false
+ ) {
+ return false;
+ } else {
+ Log::debug("Not forcing archiving for range period.");
+ }
}
+
$processOneReportOnly = !self::shouldProcessReportsAllPlugins($idSites, $segment, $periodLabel);
$isArchivingDisabled = !self::isRequestAuthorizedToArchive() || self::$archivingDisabledByTests;
- if ($processOneReportOnly) {
-
+ if ($processOneReportOnly
+ && $periodLabel != 'range'
+ ) {
// When there is a segment, we disable archiving when browser_archiving_disabled_enforce applies
if (!$segment->isEmpty()
&& $isArchivingDisabled
- && Config::getInstance()->General['browser_archiving_disabled_enforce']
+ && $generalConfig['browser_archiving_disabled_enforce']
&& !SettingsServer::isArchivePhpTriggered() // Only applies when we are not running core:archive command
) {
Log::debug("Archiving is disabled because of config setting browser_archiving_disabled_enforce=1");
diff --git a/core/AssetManager.php b/core/AssetManager.php
index 8c7d2adfa7..28f8407b79 100644
--- a/core/AssetManager.php
+++ b/core/AssetManager.php
@@ -225,7 +225,6 @@ class AssetManager extends Singleton
if ($this->pluginContainsJScriptAssets($pluginName)) {
- PiwikConfig::getInstance()->init();
if (Manager::getInstance()->isPluginBundledWithCore($pluginName)) {
$assetsToRemove[] = $this->getMergedCoreJSAsset();
@@ -253,7 +252,7 @@ class AssetManager extends Singleton
*/
public function getAssetDirectory()
{
- $mergedFileDirectory = StaticContainer::getContainer()->get('path.tmp') . '/assets';
+ $mergedFileDirectory = StaticContainer::get('path.tmp') . '/assets';
if (!is_dir($mergedFileDirectory)) {
Filesystem::mkdir($mergedFileDirectory);
diff --git a/core/AssetManager/UIAsset/OnDiskUIAsset.php b/core/AssetManager/UIAsset/OnDiskUIAsset.php
index 1c24420770..fc4b834342 100644
--- a/core/AssetManager/UIAsset/OnDiskUIAsset.php
+++ b/core/AssetManager/UIAsset/OnDiskUIAsset.php
@@ -10,6 +10,7 @@ namespace Piwik\AssetManager\UIAsset;
use Exception;
use Piwik\AssetManager\UIAsset;
+use Piwik\Filesystem;
class OnDiskUIAsset extends UIAsset
{
@@ -58,12 +59,15 @@ class OnDiskUIAsset extends UIAsset
{
if ($this->exists()) {
- if (!unlink($this->getAbsoluteLocation()))
+ try {
+ Filesystem::remove($this->getAbsoluteLocation());
+ } catch (Exception $e) {
throw new Exception("Unable to delete merged file : " . $this->getAbsoluteLocation() . ". Please delete the file and refresh");
+ }
// try to remove compressed version of the merged file.
- @unlink($this->getAbsoluteLocation() . ".deflate");
- @unlink($this->getAbsoluteLocation() . ".gz");
+ Filesystem::remove($this->getAbsoluteLocation() . ".deflate", true);
+ Filesystem::remove($this->getAbsoluteLocation() . ".gz", true);
}
}
diff --git a/core/Auth.php b/core/Auth.php
index 8e3a1e28e9..45bc22062b 100644
--- a/core/Auth.php
+++ b/core/Auth.php
@@ -16,7 +16,7 @@ use Exception;
*
* Plugins that provide Auth implementations must provide a class that implements
* this interface. Additionally, an instance of that class must be set in the
- * {@link \Piwik\Registry} class with the 'auth' key during the
+ * container with the 'Piwik\Auth' key during the
* [Request.initAuthenticationObject](http://developer.piwik.org/api-reference/events#requestinitauthenticationobject)
* event.
*
@@ -34,13 +34,13 @@ use Exception;
* **How an Auth implementation will be used**
*
* // authenticating by password
- * $auth = \Piwik\Registry::get('auth');
+ * $auth = StaticContainer::get('Piwik\Auth');
* $auth->setLogin('user');
* $auth->setPassword('password');
* $result = $auth->authenticate();
*
* // authenticating by token auth
- * $auth = \Piwik\Registry::get('auth');
+ * $auth = StaticContainer::get('Piwik\Auth');
* $auth->setLogin('user');
* $auth->setTokenAuth('...');
* $result = $auth->authenticate();
diff --git a/core/Cache.php b/core/Cache.php
new file mode 100644
index 0000000000..3036d05006
--- /dev/null
+++ b/core/Cache.php
@@ -0,0 +1,117 @@
+<?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;
+
+use Piwik\Cache\Backend;
+use Piwik\Container\StaticContainer;
+
+class Cache
+{
+
+ /**
+ * This can be considered as the default cache to use in case you don't know which one to pick. It does not support
+ * the caching of any objects though. Only boolean, numbers, strings and arrays are supported. Whenever you request
+ * an entry from the cache it will fetch the entry. Cache entries might be persisted but not necessarily. It
+ * depends on the configured backend.
+ *
+ * @return Cache\Lazy
+ */
+ public static function getLazyCache()
+ {
+ return StaticContainer::get('Piwik\Cache\Lazy');
+ }
+
+ /**
+ * This class is used to cache any data during one request. It won't be persisted between requests and it can
+ * cache all kind of data, even objects or resources. This cache is very fast.
+ *
+ * @return Cache\Transient
+ */
+ public static function getTransientCache()
+ {
+ return StaticContainer::get('Piwik\Cache\Transient');
+ }
+
+ /**
+ * This cache stores all its cache entries under one "cache" entry in a configurable backend.
+ *
+ * This comes handy for things that you need very often, nearly in every request. For example plugin metadata, the
+ * list of tracker plugins, the list of available languages, ...
+ * Instead of having to read eg. a hundred cache entries from files (or any other backend) it only loads one cache
+ * entry which contains the hundred keys. Should be used only for things that you need very often and only for
+ * cache entries that are not too large to keep loading and parsing the single cache entry fast.
+ * All cache entries it contains have the same life time. For fast performance it won't validate any cache ids.
+ * It is not possible to cache any objects using this cache.
+ *
+ * @return Cache\Eager
+ */
+ public static function getEagerCache()
+ {
+ return StaticContainer::get('Piwik\Cache\Eager');
+ }
+
+ public static function flushAll()
+ {
+ self::getLazyCache()->flushAll();
+ self::getTransientCache()->flushAll();
+ self::getEagerCache()->flushAll();
+ }
+
+ /**
+ * @param $type
+ * @return Cache\Backend
+ */
+ public static function buildBackend($type)
+ {
+ $factory = new Cache\Backend\Factory();
+ $options = self::getOptions($type);
+
+ $backend = $factory->buildBackend($type, $options);
+
+ return $backend;
+ }
+
+ private static function getOptions($type)
+ {
+ $options = self::getBackendOptions($type);
+
+ switch ($type) {
+ case 'file':
+
+ $options = array('directory' => StaticContainer::get('path.cache'));
+ break;
+
+ case 'chained':
+
+ foreach ($options['backends'] as $backend) {
+ $options[$backend] = self::getOptions($backend);
+ }
+
+ break;
+
+ case 'redis':
+
+ if (!empty($options['timeout'])) {
+ $options['timeout'] = (float)Common::forceDotAsSeparatorForDecimalPoint($options['timeout']);
+ }
+
+ break;
+ }
+
+ return $options;
+ }
+
+ private static function getBackendOptions($backend)
+ {
+ $key = ucfirst($backend) . 'Cache';
+ $options = Config::getInstance()->$key;
+
+ return $options;
+ }
+}
diff --git a/core/Cache/CacheDecorator.php b/core/Cache/CacheDecorator.php
deleted file mode 100644
index f35154ba72..0000000000
--- a/core/Cache/CacheDecorator.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?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\Cache;
-
-use Piwik\Tracker;
-use Piwik\Translate;
-
-/**
- * Caching class used for static caching.
- */
-class CacheDecorator implements CacheInterface
-{
- /**
- * @var StaticCache
- */
- protected $staticCache;
-
- public function __construct(CacheInterface $cache)
- {
- $this->staticCache = $cache;
- }
-
- public function get()
- {
- return $this->staticCache->get();
- }
-
- public function has()
- {
- return $this->staticCache->has();
- }
-
- public function setCacheKey($cacheKey)
- {
- $this->staticCache->setCacheKey($cacheKey);
- }
-
- public function getCacheKey()
- {
- return $this->staticCache->getCacheKey();
- }
-
- public function set($content)
- {
- $this->staticCache->set($content);
- }
-
-}
diff --git a/core/Cache/CacheInterface.php b/core/Cache/CacheInterface.php
deleted file mode 100644
index 65039e5436..0000000000
--- a/core/Cache/CacheInterface.php
+++ /dev/null
@@ -1,29 +0,0 @@
-<?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\Cache;
-
-use Piwik\Tracker;
-use Piwik\Translate;
-
-/**
- * Caching class used for static caching.
- */
-interface CacheInterface
-{
- public function get();
-
- public function has();
-
- public function setCacheKey($cacheKey);
-
- public function getCacheKey();
-
- public function set($content);
-
-}
diff --git a/core/Cache/LanguageAwareStaticCache.php b/core/Cache/LanguageAwareStaticCache.php
deleted file mode 100644
index 62b4773282..0000000000
--- a/core/Cache/LanguageAwareStaticCache.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?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\Cache;
-
-use Piwik\Translate;
-
-/**
- * Caching class used for static caching which is language aware. It'll cache the given content depending on the
- * current loaded language. This prevents you from having to invalidate the cache during tests in case the loaded
- * language changes etc.
- *
- * TODO convert this to a decorator... see {@link StaticCache}
- */
-class LanguageAwareStaticCache extends StaticCache
-{
- protected function completeKey($cacheKey)
- {
- return $cacheKey . Translate::getLanguageLoaded();
- }
-}
diff --git a/core/Cache/PersistentCache.php b/core/Cache/PersistentCache.php
deleted file mode 100644
index 5a98621a4c..0000000000
--- a/core/Cache/PersistentCache.php
+++ /dev/null
@@ -1,159 +0,0 @@
-<?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\Cache;
-
-use Piwik\CacheFile;
-use Piwik\Development;
-use Piwik\Piwik;
-use Piwik\SettingsServer;
-use Piwik\Version;
-
-/**
- * Caching class that persists all cached values between requests. Meaning whatever you cache will be stored on the
- * file system. It differs from other caches such as {@link CacheFile} that it does not create a file for each cacheKey.
- * Reading and writing new values does not cause multiple reads / writes on the file system and is therefore faster.
- * The cache won't be invalidated after any time by default but when the tracker cache is cleared. This is usually the
- * case when a new plugin is installed or an existing plugin or the core is updated.
- * You should be careful when caching any data since we won't modify the cache key. So if your data depends on which
- * plugins are activated or should not be available to each user than make sure to include unique names in the cache
- * key such as the names of all loaded plugin names.
- * If development mode is enabled in the config this cache acts as a {@link StaticCache}. Meaning it won't persist any
- * data between requests.
- */
-class PersistentCache
-{
- /**
- * @var CacheFile
- */
- private static $storage = null;
- private static $content = null;
- private static $isDirty = false;
-
- private $cacheKey;
-
- /**
- * Initializes the cache.
- * @param string $cacheKey
- */
- public function __construct($cacheKey)
- {
- $this->cacheKey = $cacheKey;
-
- if (is_null(self::$content)) {
- self::$content = array();
- self::populateCache();
- }
- }
-
- /**
- * Overwrites a previously set cache key. Useful if you want to reuse the same instance for different cache keys
- * for performance reasons.
- * @param string $cacheKey
- */
- public function setCacheKey($cacheKey)
- {
- $this->cacheKey = $cacheKey;
- }
-
- /**
- * Get the content related to the current cache key. Make sure to call the method {@link has()} to verify whether
- * there is actually any content set under this cache key.
- * @return mixed
- */
- public function get()
- {
- return self::$content[$this->cacheKey];
- }
-
- /**
- * Check whether any content was actually stored for the current cache key.
- * @return bool
- */
- public function has()
- {
- return array_key_exists($this->cacheKey, self::$content);
- }
-
- /**
- * Set (overwrite) any content related to the current set cache key.
- * @param $content
- */
- public function set($content)
- {
- self::$content[$this->cacheKey] = $content;
- self::$isDirty = true;
- }
-
- private static function populateCache()
- {
- if (Development::isEnabled()) {
- return;
- }
-
- if (SettingsServer::isTrackerApiRequest()) {
- $eventToPersist = 'Tracker.end';
- $mode = '-tracker';
- } else {
- $eventToPersist = 'Request.dispatch.end';
- $mode = '-ui';
- }
-
- $cache = self::getStorage()->get(self::getCacheFilename() . $mode);
-
- if (is_array($cache)) {
- self::$content = $cache;
- }
-
- Piwik::addAction($eventToPersist, array(__CLASS__, 'persistCache'));
- }
-
- private static function getCacheFilename()
- {
- return 'StaticCache-' . str_replace(array('.', '-'), '', Version::VERSION);
- }
-
- /**
- * @ignore
- */
- public static function persistCache()
- {
- if (self::$isDirty) {
- if (SettingsServer::isTrackerApiRequest()) {
- $mode = '-tracker';
- } else {
- $mode = '-ui';
- }
-
- self::getStorage()->set(self::getCacheFilename() . $mode, self::$content);
- }
- }
-
- /**
- * @ignore
- */
- public static function _reset()
- {
- self::$content = array();
- }
-
- /**
- * @return CacheFile
- */
- private static function getStorage()
- {
- if (is_null(self::$storage)) {
- self::$storage = new CacheFile('tracker', 43200);
- self::$storage->addOnDeleteCallback(function () {
- PersistentCache::_reset();
- });
- }
-
- return self::$storage;
- }
-}
diff --git a/core/Cache/PluginAwareStaticCache.php b/core/Cache/PluginAwareStaticCache.php
deleted file mode 100644
index 1bc3a25bd5..0000000000
--- a/core/Cache/PluginAwareStaticCache.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?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\Cache;
-
-use Piwik\Plugin\Manager as PluginManager;
-use Piwik\Translate;
-
-/**
- * Caching class used for static caching which is plugin aware. It'll cache the given content depending on the plugins
- * that are installed. This prevents you from having to invalidate the cache during tests in case the loaded plugins
- * changes etc. The key is language aware as well.
- *
- * TODO convert this to a decorator... see {@link StaticCache}
- */
-class PluginAwareStaticCache extends StaticCache
-{
- protected function completeKey($cacheKey)
- {
- $pluginManager = PluginManager::getInstance();
- $pluginNames = $pluginManager->getLoadedPluginsName();
- $cacheKey = $cacheKey . md5(implode('', $pluginNames)) . Translate::getLanguageLoaded();
-
- return $cacheKey;
- }
-}
diff --git a/core/Cache/StaticCache.php b/core/Cache/StaticCache.php
deleted file mode 100644
index 0c102e18d3..0000000000
--- a/core/Cache/StaticCache.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?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\Cache;
-
-/**
- * Caching class used for static caching. Any content that is set here won't be cached between requests. If you do want
- * to persist any content between requests have a look at {@link PersistentCache}
- *
- * TODO the default static cache should actually not be language aware. Especially since we would end up in classes like
- * LanguageAwareStaticCache, PluginAwareStaticCache, PluginAwareLanguageAwareStaticCache, PluginAwareXYZStaticCache,...
- * once we have dependency injection we should "build" all the caches we need removing duplicated code and extend the
- * static cache by using decorators which "enrich" the cache key depending on their awareness.
- */
-class StaticCache
-{
- protected static $staticCache = array();
-
- private $cacheKey;
-
- /**
- * Initializes the cache.
- * @param string $cacheKey
- */
- public function __construct($cacheKey)
- {
- $this->setCacheKey($cacheKey);
- }
-
- /**
- * Overwrites a previously set cache key. Useful if you want to reuse the same instance for different cache keys
- * for performance reasons.
- * @param string $cacheKey
- */
- public function setCacheKey($cacheKey)
- {
- $this->cacheKey = $this->completeKey($cacheKey);
- }
-
- /**
- * Get the content related to the current cache key. Make sure to call the method {@link has()} to verify whether
- * there is actually any content set under this cache key.
- * @return mixed
- */
- public function get()
- {
- return self::$staticCache[$this->cacheKey];
- }
-
- /**
- * Check whether any content was actually stored for the current cache key.
- * @return bool
- */
- public function has()
- {
- return array_key_exists($this->cacheKey, self::$staticCache);
- }
-
- /**
- * Reset the stored content of the current cache key.
- */
- public function clear()
- {
- unset(self::$staticCache[$this->cacheKey]);
- }
-
- /**
- * Reset the stored content of the current cache key.
- * @ignore
- */
- public static function clearAll()
- {
- self::$staticCache = array();
- }
-
- /**
- * Set (overwrite) any content related to the current set cache key.
- * @param $content
- */
- public function set($content)
- {
- self::$staticCache[$this->cacheKey] = $content;
- }
-
- protected function completeKey($cacheKey)
- {
- return $cacheKey;
- }
-} \ No newline at end of file
diff --git a/core/CacheFile.php b/core/CacheFile.php
deleted file mode 100644
index b8ec14c575..0000000000
--- a/core/CacheFile.php
+++ /dev/null
@@ -1,237 +0,0 @@
-<?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;
-
-use Exception;
-use Piwik\Container\StaticContainer;
-
-/**
- * This class is used to cache data on the filesystem.
- *
- * It is for example used by the Tracker process to cache various settings and websites attributes in tmp/cache/tracker/*
- *
- */
-class CacheFile
-{
- // for testing purposes since tests run on both CLI/FPM (changes in CLI can't invalidate
- // opcache in FPM, so we have to invalidate before reading)
- public static $invalidateOpCacheBeforeRead = false;
-
- /**
- * @var string
- */
- private $cachePath;
-
- /**
- * Minimum enforced TTL in seconds
- */
- const MINIMUM_TTL = 60;
-
- /**
- * @var \Callable[]
- */
- private static $onDeleteCallback = array();
-
- /**
- * @param string $directory directory to use
- * @param int $timeToLiveInSeconds TTL
- */
- public function __construct($directory, $timeToLiveInSeconds = 300)
- {
- $this->cachePath = StaticContainer::getContainer()->get('path.tmp') . '/cache/' . $directory . '/';
-
- if ($timeToLiveInSeconds < self::MINIMUM_TTL) {
- $timeToLiveInSeconds = self::MINIMUM_TTL;
- }
- $this->ttl = $timeToLiveInSeconds;
- }
-
- /**
- * Function to fetch a cache entry
- *
- * @param string $id The cache entry ID
- * @return array|bool False on error, or array the cache content
- */
- public function get($id)
- {
- if (empty($id)) {
- return false;
- }
-
- $id = $this->cleanupId($id);
-
- $cache_complete = false;
- $content = '';
- $expires_on = false;
-
- // We are assuming that most of the time cache will exists
- $cacheFilePath = $this->cachePath . $id . '.php';
- if (self::$invalidateOpCacheBeforeRead) {
- $this->opCacheInvalidate($cacheFilePath);
- }
-
- $ok = @include($cacheFilePath);
-
- if ($ok && $cache_complete == true) {
-
- if (empty($expires_on)
- || $expires_on < time()
- ) {
- return false;
- }
-
- return $content;
- }
-
- return false;
- }
-
- private function getExpiresTime()
- {
- return time() + $this->ttl;
- }
-
- protected function cleanupId($id)
- {
- if (!Filesystem::isValidFilename($id)) {
- throw new Exception("Invalid cache ID request $id");
- }
-
- return $id;
- }
-
- /**
- * A function to store content a cache entry.
- *
- * @param string $id The cache entry ID
- * @param array $content The cache content
- * @throws \Exception
- * @return bool True if the entry was succesfully stored
- */
- public function set($id, $content)
- {
- if (empty($id)) {
- return false;
- }
-
- if (!is_dir($this->cachePath)) {
- Filesystem::mkdir($this->cachePath);
- }
-
- if (!is_writable($this->cachePath)) {
- return false;
- }
-
- $id = $this->cleanupId($id);
- $id = $this->cachePath . $id . '.php';
-
- if (is_object($content)) {
- throw new \Exception('You cannot use the CacheFile to cache an object, only arrays, strings and numbers.');
- }
-
- $cache_literal = $this->buildCacheLiteral($content);
-
- // Write cache to a temp file, then rename it, overwriting the old cache
- // On *nix systems this should guarantee atomicity
- $tmp_filename = tempnam($this->cachePath, 'tmp_');
- @chmod($tmp_filename, 0640);
- if ($fp = @fopen($tmp_filename, 'wb')) {
- @fwrite($fp, $cache_literal, strlen($cache_literal));
- @fclose($fp);
-
- if (!@rename($tmp_filename, $id)) {
- // On some systems rename() doesn't overwrite destination
- @unlink($id);
- if (!@rename($tmp_filename, $id)) {
- // Make sure that no temporary file is left over
- // if the destination is not writable
- @unlink($tmp_filename);
- }
- }
-
- $this->opCacheInvalidate($id);
-
- return true;
- }
-
- return false;
- }
-
- /**
- * A function to delete a single cache entry
- *
- * @param string $id The cache entry ID
- * @return bool True if the entry was succesfully deleted
- */
- public function delete($id)
- {
- if (empty($id)) {
- return false;
- }
-
- $id = $this->cleanupId($id);
-
- $filename = $this->cachePath . $id . '.php';
-
- if (file_exists($filename)) {
- $this->opCacheInvalidate($filename);
- @unlink($filename);
- return true;
- }
-
- return false;
- }
-
- public function addOnDeleteCallback($onDeleteCallback)
- {
- self::$onDeleteCallback[] = $onDeleteCallback;
- }
-
- /**
- * A function to delete all cache entries in the directory
- */
- public function deleteAll()
- {
- $self = $this;
- $beforeUnlink = function ($path) use ($self) {
- $self->opCacheInvalidate($path);
- };
-
- Filesystem::unlinkRecursive($this->cachePath, $deleteRootToo = false, $beforeUnlink);
-
- if (!empty(self::$onDeleteCallback)) {
- foreach (self::$onDeleteCallback as $callback) {
- $callback();
- }
- }
- }
-
- public function opCacheInvalidate($filepath)
- {
- if (is_file($filepath)) {
- if (function_exists('opcache_invalidate')) {
- @opcache_invalidate($filepath, $force = true);
- }
- if (function_exists('apc_delete_file')) {
- @apc_delete_file($filepath);
- }
- }
- }
-
- private function buildCacheLiteral($content)
- {
- $cache_literal = "<" . "?php\n";
- $cache_literal .= "$" . "content = " . var_export($content, true) . ";\n";
- $cache_literal .= "$" . "expires_on = " . $this->getExpiresTime() . ";\n";
- $cache_literal .= "$" . "cache_complete = true;\n";
- $cache_literal .= "?" . ">";
-
- return $cache_literal;
- }
-}
diff --git a/core/CacheId.php b/core/CacheId.php
new file mode 100644
index 0000000000..e445424009
--- /dev/null
+++ b/core/CacheId.php
@@ -0,0 +1,30 @@
+<?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;
+
+use Piwik\Translate;
+use Piwik\Plugin\Manager;
+
+class CacheId
+{
+ public static function languageAware($cacheId)
+ {
+ return $cacheId . '-' . Translate::getLanguageLoaded();
+ }
+
+ public static function pluginAware($cacheId)
+ {
+ $pluginManager = Manager::getInstance();
+ $pluginNames = $pluginManager->getLoadedPluginsName();
+ $cacheId = $cacheId . '-' . md5(implode('', $pluginNames));
+ $cacheId = self::languageAware($cacheId);
+
+ return $cacheId;
+ }
+}
diff --git a/core/CliMulti.php b/core/CliMulti.php
index 8a1bffd773..3fb8dcdeba 100644
--- a/core/CliMulti.php
+++ b/core/CliMulti.php
@@ -25,7 +25,7 @@ class CliMulti {
public $supportsAsync = null;
/**
- * @var \Piwik\CliMulti\Process[]
+ * @var Process[]
*/
private $processes = array();
@@ -36,7 +36,7 @@ class CliMulti {
private $concurrentProcessesLimit = null;
/**
- * @var \Piwik\CliMulti\Output[]
+ * @var Output[]
*/
private $outputs = array();
@@ -118,7 +118,7 @@ class CliMulti {
{
$bin = $this->findPhpBinary();
- return sprintf('%s %s/console climulti:request --piwik-domain=%s %s > %s 2>&1 &',
+ return sprintf('%s %s/console climulti:request -q --piwik-domain=%s %s > %s 2>&1 &',
$bin, PIWIK_INCLUDE_PATH, escapeshellarg($hostname), escapeshellarg($query), $outputFile);
}
@@ -229,7 +229,7 @@ class CliMulti {
public static function getTmpPath()
{
- return StaticContainer::getContainer()->get('path.tmp') . '/climulti';
+ return StaticContainer::get('path.tmp') . '/climulti';
}
private function executeAsyncCli($url, Output $output, $cmdId)
diff --git a/core/CliMulti/Process.php b/core/CliMulti/Process.php
index 62c8be895d..118482c854 100644
--- a/core/CliMulti/Process.php
+++ b/core/CliMulti/Process.php
@@ -177,7 +177,7 @@ class Process
return false;
}
- if (static::commandExists('ps') && self::returnsSuccessCode('ps') && self::commandExists('awk')) {
+ if (self::commandExists('ps') && self::returnsSuccessCode('ps') && self::commandExists('awk')) {
return true;
}
@@ -203,7 +203,7 @@ class Process
$command = 'shell_exec';
$disabled = explode(',', ini_get('disable_functions'));
$disabled = array_map('trim', $disabled);
- return in_array($command, $disabled);
+ return in_array($command, $disabled) || !function_exists($command);
}
private static function returnsSuccessCode($command)
diff --git a/core/CliMulti/RequestCommand.php b/core/CliMulti/RequestCommand.php
index a13b10c488..4db6ccef22 100644
--- a/core/CliMulti/RequestCommand.php
+++ b/core/CliMulti/RequestCommand.php
@@ -9,12 +9,15 @@
namespace Piwik\CliMulti;
use Piwik\Config;
+use Piwik\Container\StaticContainer;
+use Piwik\Db;
+use Piwik\Log;
+use Piwik\Option;
use Piwik\Plugin\ConsoleCommand;
use Piwik\Url;
use Piwik\UrlHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
@@ -26,20 +29,26 @@ class RequestCommand extends ConsoleCommand
{
$this->setName('climulti:request');
$this->setDescription('Parses and executes the given query. See Piwik\CliMulti. Intended only for system usage.');
- $this->addArgument('url-query', null, InputOption::VALUE_REQUIRED, 'Piwik URL query string, for instance: "module=API&method=API.getPiwikVersion&token_auth=123456789"');
+ $this->addArgument('url-query', InputArgument::REQUIRED, 'Piwik URL query string, for instance: "module=API&method=API.getPiwikVersion&token_auth=123456789"');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
+ $this->recreateContainerWithWebEnvironment();
+
$this->initHostAndQueryString($input);
if ($this->isTestModeEnabled()) {
Config::getInstance()->setTestEnvironment();
- $indexFile = '/tests/PHPUnit/proxy/index.php';
+ $indexFile = '/tests/PHPUnit/proxy/';
+
+ $this->resetDatabase();
} else {
- $indexFile = '/index.php';
+ $indexFile = '/';
}
+ $indexFile .= 'index.php';
+
if (!empty($_GET['pid'])) {
$process = new Process($_GET['pid']);
@@ -79,4 +88,22 @@ class RequestCommand extends ConsoleCommand
}
}
-} \ No newline at end of file
+ /**
+ * We will be simulating an HTTP request here (by including index.php).
+ *
+ * To avoid weird side-effects (e.g. the logging output messing up the HTTP response on the CLI output)
+ * we need to recreate the container with the default environment instead of the CLI environment.
+ */
+ private function recreateContainerWithWebEnvironment()
+ {
+ StaticContainer::setEnvironment(null);
+ StaticContainer::clearContainer();
+ Log::unsetInstance();
+ }
+
+ private function resetDatabase()
+ {
+ Option::clearCache();
+ Db::destroyDatabaseObject();
+ }
+}
diff --git a/core/Columns/Updater.php b/core/Columns/Updater.php
index 6c42ba0dff..4272f46f56 100644
--- a/core/Columns/Updater.php
+++ b/core/Columns/Updater.php
@@ -14,14 +14,16 @@ use Piwik\Plugin\Dimension\VisitDimension;
use Piwik\Plugin\Dimension\ConversionDimension;
use Piwik\Db;
use Piwik\Updater as PiwikUpdater;
-use Piwik\Cache\PersistentCache;
use Piwik\Filesystem;
+use Piwik\Cache as PiwikCache;
/**
* Class that handles dimension updates
*/
class Updater extends \Piwik\Updates
{
+ private static $cacheId = 'AllDimensionModifyTime';
+
/**
* @var Updater
*/
@@ -315,15 +317,22 @@ class Updater extends \Piwik\Updates
private static function cacheCurrentDimensionFileChanges()
{
$changes = self::getCurrentDimensionFileChanges();
- $persistentCache = new PersistentCache('AllDimensionModifyTime');
- $persistentCache->set($changes);
+
+ $cache = self::buildCache();
+ $cache->save(self::$cacheId, $changes);
+ }
+
+ private static function buildCache()
+ {
+ return PiwikCache::getEagerCache();
}
private static function getCachedDimensionFileChanges()
{
- $persistentCache = new PersistentCache('AllDimensionModifyTime');
- if ($persistentCache->has()) {
- return $persistentCache->get();
+ $cache = self::buildCache();
+
+ if ($cache->contains(self::$cacheId)) {
+ return $cache->fetch(self::$cacheId);
}
return array();
diff --git a/core/Common.php b/core/Common.php
index b5ed581d82..7e3bcbc7af 100644
--- a/core/Common.php
+++ b/core/Common.php
@@ -9,9 +9,12 @@
namespace Piwik;
use Exception;
+use Piwik\Container\StaticContainer;
+use Piwik\Intl\Data\Provider\LanguageDataProvider;
+use Piwik\Intl\Data\Provider\RegionDataProvider;
use Piwik\Plugins\UserCountry\LocationProvider\DefaultProvider;
use Piwik\Tracker;
-use Piwik\Tracker\Cache;
+use Piwik\Tracker\Cache as TrackerCache;
/**
* Contains helper methods used by both Piwik Core and the Piwik Tracking engine.
@@ -518,7 +521,13 @@ class Common
*/
public static function generateUniqId()
{
- return md5(uniqid(rand(), true));
+ if (function_exists('mt_rand')) {
+ $rand = mt_rand();
+ } else {
+ $rand = rand();
+ }
+
+ return md5(uniqid($rand, true));
}
/**
@@ -743,14 +752,16 @@ class Common
*
* @see core/DataFiles/Countries.php
*
- * @return array Array of 3 letter continent codes
+ * @return array Array of 3 letter continent codes
+ *
+ * @deprecated Use Piwik\Intl\Data\Provider\RegionDataProvider instead.
+ * @see \Piwik\Intl\Data\Provider\RegionDataProvider::getContinentList()
*/
public static function getContinentsList()
{
- require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php';
-
- $continentsList = $GLOBALS['Piwik_ContinentList'];
- return $continentsList;
+ /** @var RegionDataProvider $dataProvider */
+ $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
+ return $dataProvider->getContinentList();
}
/**
@@ -759,20 +770,16 @@ class Common
* @see core/DataFiles/Countries.php
*
* @param bool $includeInternalCodes
- * @return array Array of (2 letter ISO codes => 3 letter continent code)
+ * @return array Array of (2 letter ISO codes => 3 letter continent code)
+ *
+ * @deprecated Use Piwik\Intl\Data\Provider\RegionDataProvider instead.
+ * @see \Piwik\Intl\Data\Provider\RegionDataProvider::getCountryList()
*/
public static function getCountriesList($includeInternalCodes = false)
{
- require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Countries.php';
-
- $countriesList = $GLOBALS['Piwik_CountryList'];
- $extras = $GLOBALS['Piwik_CountryList_Extras'];
-
- if ($includeInternalCodes) {
- return array_merge($countriesList, $extras);
- }
-
- return $countriesList;
+ /** @var RegionDataProvider $dataProvider */
+ $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
+ return $dataProvider->getCountryList($includeInternalCodes);
}
/**
@@ -783,13 +790,15 @@ class Common
* @return array Array of two letter ISO codes mapped with their associated language names (in English). E.g.
* `array('en' => 'English', 'ja' => 'Japanese')`.
* @api
+ *
+ * @deprecated Use Piwik\Intl\Data\Provider\LanguageDataProvider instead.
+ * @see \Piwik\Intl\Data\Provider\LanguageDataProvider::getLanguageList()
*/
public static function getLanguagesList()
{
- require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Languages.php';
-
- $languagesList = $GLOBALS['Piwik_LanguageList'];
- return $languagesList;
+ /** @var LanguageDataProvider $dataProvider */
+ $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
+ return $dataProvider->getLanguageList();
}
/**
@@ -800,13 +809,15 @@ class Common
* @return array Array of two letter ISO language codes mapped with two letter ISO country codes:
* `array('fr' => 'fr') // French => France`
* @api
+ *
+ * @deprecated Use Piwik\Intl\Data\Provider\LanguageDataProvider instead.
+ * @see \Piwik\Intl\Data\Provider\LanguageDataProvider::getLanguageToCountryList()
*/
public static function getLanguageToCountryList()
{
- require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/LanguageToCountry.php';
-
- $languagesList = $GLOBALS['Piwik_LanguageToCountry'];
- return $languagesList;
+ /** @var LanguageDataProvider $dataProvider */
+ $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
+ return $dataProvider->getLanguageToCountryList();
}
/**
@@ -818,11 +829,20 @@ class Common
*/
public static function getSearchEngineUrls()
{
- require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php';
+ $cacheId = 'Common.getSearchEngineUrls';
+ $cache = Cache::getTransientCache();
+ $searchEngines = $cache->fetch($cacheId);
+
+ if (empty($searchEngines)) {
- $searchEngines = $GLOBALS['Piwik_SearchEngines'];
+ require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/SearchEngines.php';
- Piwik::postEvent('Referrer.addSearchEngineUrls', array(&$searchEngines));
+ $searchEngines = $GLOBALS['Piwik_SearchEngines'];
+
+ Piwik::postEvent('Referrer.addSearchEngineUrls', array(&$searchEngines));
+
+ $cache->save($cacheId, $searchEngines);
+ }
return $searchEngines;
}
@@ -836,13 +856,21 @@ class Common
*/
public static function getSearchEngineNames()
{
- $searchEngines = self::getSearchEngineUrls();
+ $cacheId = 'Common.getSearchEngineNames';
+ $cache = Cache::getTransientCache();
+ $nameToUrl = $cache->fetch($cacheId);
+
+ if (empty($nameToUrl)) {
- $nameToUrl = array();
- foreach ($searchEngines as $url => $info) {
- if (!isset($nameToUrl[$info[0]])) {
- $nameToUrl[$info[0]] = $url;
+ $searchEngines = self::getSearchEngineUrls();
+
+ $nameToUrl = array();
+ foreach ($searchEngines as $url => $info) {
+ if (!isset($nameToUrl[$info[0]])) {
+ $nameToUrl[$info[0]] = $url;
+ }
}
+ $cache->save($cacheId, $nameToUrl);
}
return $nameToUrl;
@@ -857,11 +885,20 @@ class Common
*/
public static function getSocialUrls()
{
- require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Socials.php';
+ $cacheId = 'Common.getSocialUrls';
+ $cache = Cache::getTransientCache();
+ $socialUrls = $cache->fetch($cacheId);
+
+ if (empty($socialUrls)) {
- $socialUrls = $GLOBALS['Piwik_socialUrl'];
+ require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Socials.php';
- Piwik::postEvent('Referrer.addSocialUrls', array(&$socialUrls));
+ $socialUrls = $GLOBALS['Piwik_socialUrl'];
+
+ Piwik::postEvent('Referrer.addSocialUrls', array(&$socialUrls));
+
+ $cache->save($cacheId, $socialUrls);
+ }
return $socialUrls;
}
@@ -948,7 +985,11 @@ class Common
return self::LANGUAGE_CODE_INVALID;
}
- $validCountries = self::getCountriesList();
+ /** @var RegionDataProvider $dataProvider */
+ $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
+
+ $validCountries = $dataProvider->getCountryList();
+
return self::extractCountryCodeFromBrowserLanguage($lang, $validCountries, $enableLanguageToCountryGuess);
}
@@ -962,7 +1003,10 @@ class Common
*/
public static function extractCountryCodeFromBrowserLanguage($browserLanguage, $validCountries, $enableLanguageToCountryGuess)
{
- $langToCountry = self::getLanguageToCountryList();
+ /** @var LanguageDataProvider $dataProvider */
+ $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
+
+ $langToCountry = $dataProvider->getLanguageToCountryList();
if ($enableLanguageToCountryGuess) {
if (preg_match('/^([a-z]{2,3})(?:,|;|$)/', $browserLanguage, $matches)) {
@@ -1059,11 +1103,12 @@ class Common
*/
public static function getContinent($country)
{
- $countryList = self::getCountriesList();
- if (isset($countryList[$country])) {
- return $countryList[$country];
- }
- return 'unk';
+ /** @var RegionDataProvider $dataProvider */
+ $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
+
+ $countryList = $dataProvider->getCountryList();
+
+ return isset($countryList[$country]) ? $countryList[$country] : 'unk';
}
/*
@@ -1182,10 +1227,12 @@ class Common
}
if (strpos(PHP_SAPI, '-fcgi') === false) {
- $key = $_SERVER['SERVER_PROTOCOL'];
+ $key = 'HTTP/1.1';
- if (strlen($key) > 15 || empty($key)) {
- $key = 'HTTP/1.1';
+ if (array_key_exists('SERVER_PROTOCOL', $_SERVER)
+ && strlen($_SERVER['SERVER_PROTOCOL']) < 15
+ && strlen($_SERVER['SERVER_PROTOCOL']) > 1) {
+ $key = $_SERVER['SERVER_PROTOCOL'];
}
} else {
@@ -1203,7 +1250,7 @@ class Common
*/
public static function getCurrentLocationProviderId()
{
- $cache = Cache::getCacheGeneral();
+ $cache = TrackerCache::getCacheGeneral();
return empty($cache['currentLocationProviderId'])
? DefaultProvider::ID
: $cache['currentLocationProviderId'];
@@ -1225,25 +1272,32 @@ class Common
$var = null;
}
- public static function printDebug($info = '')
+ /**
+ * @todo This method is weird, it's debugging statements but seem to only work for the tracker, maybe it
+ * should be moved elsewhere
+ */
+ public static function printDebug($info = '')
{
if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) {
+ if(!headers_sent()) {
+ // prevent XSS in tracker debug output
+ header('Content-type: text/plain');
+ }
+
if (is_object($info)) {
$info = var_export($info, true);
}
- Log::getInstance()->setLogLevel(Log::DEBUG);
-
if (is_array($info) || is_object($info)) {
$info = Common::sanitizeInputValues($info);
$out = var_export($info, true);
foreach (explode("\n", $out) as $line) {
- Log::debug($line);
+ echo $line . "\n";
}
} else {
foreach (explode("\n", $info) as $line) {
- Log::debug(htmlspecialchars($line, ENT_QUOTES));
+ echo $line . "\n";
}
}
}
@@ -1255,8 +1309,11 @@ class Common
*/
protected static function checkValidLanguagesIsSet($validLanguages)
{
+ /** @var LanguageDataProvider $dataProvider */
+ $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\LanguageDataProvider');
+
if (empty($validLanguages)) {
- $validLanguages = array_keys(Common::getLanguagesList());
+ $validLanguages = array_keys($dataProvider->getLanguageList());
return $validLanguages;
}
return $validLanguages;
diff --git a/core/Config.php b/core/Config.php
index db2e1e5d6a..014424c119 100644
--- a/core/Config.php
+++ b/core/Config.php
@@ -10,6 +10,9 @@
namespace Piwik;
use Exception;
+use Piwik\Ini\IniReader;
+use Piwik\Ini\IniReadingException;
+use Piwik\Ini\IniWriter;
/**
* Singleton that provides read & write access to Piwik's INI configuration.
@@ -45,9 +48,7 @@ class Config extends Singleton
const DEFAULT_GLOBAL_CONFIG_PATH = '/config/global.ini.php';
/**
- * Contains configuration files values
- *
- * @var array
+ * @var boolean
*/
protected $initialized = false;
protected $configGlobal = array();
@@ -61,16 +62,25 @@ class Config extends Singleton
/**
* @var boolean
*/
- protected $isTest = false;
+ protected $doNotWriteConfigInTests = false;
+
+ /**
+ * @var IniReader
+ */
+ private $iniReader;
/**
- * Constructor
+ * @var IniWriter
*/
+ private $iniWriter;
+
public function __construct($pathGlobal = null, $pathLocal = null, $pathCommon = null)
{
$this->pathGlobal = $pathGlobal ?: self::getGlobalConfigPath();
$this->pathCommon = $pathCommon ?: self::getCommonConfigPath();
$this->pathLocal = $pathLocal ?: self::getLocalConfigPath();
+ $this->iniReader = new IniReader();
+ $this->iniWriter = new IniWriter();
}
/**
@@ -113,19 +123,15 @@ class Config extends Singleton
public function setTestEnvironment($pathLocal = null, $pathGlobal = null, $pathCommon = null, $allowSaving = false)
{
if (!$allowSaving) {
- $this->isTest = true;
+ $this->doNotWriteConfigInTests = true;
}
- $this->clear();
-
$this->pathLocal = $pathLocal ?: Config::getLocalConfigPath();
$this->pathGlobal = $pathGlobal ?: Config::getGlobalConfigPath();
$this->pathCommon = $pathCommon ?: Config::getCommonConfigPath();
$this->init();
- // this proxy will not record any data in the production database.
- // this provides security for Piwik installs and tests were setup.
if (isset($this->configGlobal['database_tests'])
|| isset($this->configLocal['database_tests'])
) {
@@ -269,10 +275,11 @@ class Config extends Singleton
*/
public function forceUsageOfLocalHostnameConfig($hostname)
{
- $hostConfig = static::getLocalConfigInfoForHostname($hostname);
+ $hostConfig = self::getLocalConfigInfoForHostname($hostname);
- if (!Filesystem::isValidFilename($hostConfig['file'])) {
- throw new Exception('Hostname is not valid');
+ $filename = $hostConfig['file'];
+ if (!Filesystem::isValidFilename($filename)) {
+ throw new Exception('Piwik domain is not a valid looking hostname (' . $filename . ').');
}
$this->pathLocal = $hostConfig['path'];
@@ -298,6 +305,7 @@ class Config extends Singleton
{
$this->configGlobal = array();
$this->configLocal = array();
+ $this->configCommon = array();
$this->configCache = array();
$this->initialized = false;
}
@@ -309,6 +317,7 @@ class Config extends Singleton
*/
public function init()
{
+ $this->clear();
$this->initialized = true;
$reportError = SettingsServer::isTrackerApiRequest();
@@ -317,20 +326,33 @@ class Config extends Singleton
throw new Exception(Piwik::translate('General_ExceptionConfigurationFileNotFound', array($this->pathGlobal)));
}
- $this->configGlobal = _parse_ini_file($this->pathGlobal, true);
-
- if (empty($this->configGlobal) && $reportError) {
- throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()")));
+ try {
+ $this->configGlobal = $this->iniReader->readFile($this->pathGlobal);
+ } catch (IniReadingException $e) {
+ if ($reportError) {
+ throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathGlobal, "parse_ini_file()")));
+ }
}
- $this->configCommon = _parse_ini_file($this->pathCommon, true);
+ try {
+ if (file_exists($this->pathCommon)) {
+ $this->configCommon = $this->iniReader->readFile($this->pathCommon);
+ } else {
+ $this->configCommon = false;
+ }
+ } catch (IniReadingException $e) {
+ $this->configCommon = false;
+ }
// Check config.ini.php last
$this->checkLocalConfigFound();
- $this->configLocal = _parse_ini_file($this->pathLocal, true);
- if (empty($this->configLocal) && $reportError) {
- throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()")));
+ try {
+ $this->configLocal = $this->iniReader->readFile($this->pathLocal);
+ } catch (IniReadingException $e) {
+ if ($reportError) {
+ throw new Exception(Piwik::translate('General_ExceptionUnreadableFileDisabledMethod', array($this->pathLocal, "parse_ini_file()")));
+ }
}
}
@@ -365,8 +387,10 @@ class Config extends Singleton
$value = $this->decodeValues($value);
}
return $values;
+ } elseif (is_string($values)) {
+ return html_entity_decode($values, ENT_COMPAT, 'UTF-8');
}
- return html_entity_decode($values, ENT_COMPAT, 'UTF-8');
+ return $values;
}
/**
@@ -381,11 +405,9 @@ class Config extends Singleton
foreach ($values as &$value) {
$value = $this->encodeValues($value);
}
- } else {
- if (is_float($values)) {
- $values = Common::forceDotAsSeparatorForDecimalPoint($values);
- }
-
+ } elseif (is_float($values)) {
+ $values = Common::forceDotAsSeparatorForDecimalPoint($values);
+ } elseif (is_string($values)) {
$values = htmlentities($values, ENT_COMPAT, 'UTF-8');
$values = str_replace('$', '&#36;', $values);
}
@@ -556,13 +578,12 @@ class Config extends Singleton
{
$dirty = false;
- $output = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n";
- $output .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n";
-
if (!$configCache) {
return false;
}
+ $configToWrite = array();
+
// If there is a common.config.ini.php, this will ensure config.ini.php does not duplicate its values
if (!empty($configCommon)) {
$configGlobal = $this->array_merge_recursive_distinct($configGlobal, $configCommon);
@@ -605,38 +626,13 @@ class Config extends Singleton
$dirty = true;
}
- // no point in writing empty sections, so skip if the cached section is empty
- if (empty($config)) {
- continue;
- }
-
- $output .= "[$section]\n";
-
- foreach ($config as $name => $value) {
- $value = $this->encodeValues($value);
-
- if (is_numeric($name)) {
- $name = $section;
- $value = array($value);
- }
-
- if (is_array($value)) {
- foreach ($value as $currentValue) {
- $output .= $name . "[] = \"$currentValue\"\n";
- }
- } else {
- if (!is_numeric($value)) {
- $value = "\"$value\"";
- }
- $output .= $name . ' = ' . $value . "\n";
- }
- }
-
- $output .= "\n";
+ $configToWrite[$section] = array_map(array($this, 'encodeValues'), $config);
}
if ($dirty) {
- return $output;
+ $header = "; <?php exit; ?> DO NOT REMOVE THIS LINE\n";
+ $header .= "; file automatically generated or modified by Piwik; you can manually override the default values in global.ini.php by redefining them in this file.\n";
+ return $this->iniWriter->writeToString($configToWrite, $header);
}
return false;
}
@@ -655,7 +651,7 @@ class Config extends Singleton
*/
protected function writeConfig($configLocal, $configGlobal, $configCommon, $configCache, $pathLocal, $clear = true)
{
- if ($this->isTest) {
+ if ($this->doNotWriteConfigInTests) {
return;
}
diff --git a/core/Console.php b/core/Console.php
index d0268569c9..de51756f39 100644
--- a/core/Console.php
+++ b/core/Console.php
@@ -8,7 +8,9 @@
*/
namespace Piwik;
+use Piwik\Container\StaticContainer;
use Piwik\Plugin\Manager as PluginManager;
+use Symfony\Bridge\Monolog\Handler\ConsoleHandler;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -29,12 +31,15 @@ class Console extends Application
);
$this->getDefinition()->addOption($option);
+
+ StaticContainer::setEnvironment('cli');
}
public function doRun(InputInterface $input, OutputInterface $output)
{
$this->initPiwikHost($input);
$this->initConfig($output);
+ $this->initLoggerOutput($output);
try {
self::initPlugins();
@@ -42,8 +47,6 @@ class Console extends Application
// Piwik not installed yet, no config file?
}
- Translate::reloadLanguage('en');
-
$commands = $this->getAvailableCommands();
foreach ($commands as $command) {
@@ -142,6 +145,21 @@ class Console extends Application
}
}
+ /**
+ * Register the console output into the logger.
+ *
+ * Ideally, this should be done automatically with events:
+ * @see http://symfony.com/fr/doc/current/components/console/events.html
+ * @see Symfony\Bridge\Monolog\Handler\ConsoleHandler::onCommand()
+ * But it would require to install Symfony's Event Dispatcher.
+ */
+ private function initLoggerOutput(OutputInterface $output)
+ {
+ /** @var ConsoleHandler $consoleLogHandler */
+ $consoleLogHandler = StaticContainer::get('Symfony\Bridge\Monolog\Handler\ConsoleHandler');
+ $consoleLogHandler->setOutput($output);
+ }
+
public static function initPlugins()
{
Plugin\Manager::getInstance()->loadActivatedPlugins();
diff --git a/core/Container/ContainerFactory.php b/core/Container/ContainerFactory.php
new file mode 100644
index 0000000000..cbbc36a00a
--- /dev/null
+++ b/core/Container/ContainerFactory.php
@@ -0,0 +1,100 @@
+<?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\Container;
+
+use DI\Container;
+use DI\ContainerBuilder;
+use Doctrine\Common\Cache\ArrayCache;
+use Piwik\Config;
+use Piwik\Development;
+use Piwik\Plugin\Manager;
+
+/**
+ * Creates a configured DI container.
+ */
+class ContainerFactory
+{
+ /**
+ * Optional environment config to load.
+ *
+ * @var string|null
+ */
+ private $environment;
+
+ /**
+ * @param string|null $environment Optional environment config to load.
+ */
+ public function __construct($environment = null)
+ {
+ $this->environment = $environment;
+ }
+
+ /**
+ * @link http://php-di.org/doc/container-configuration.html
+ * @throws \Exception
+ * @return Container
+ */
+ public function create()
+ {
+ $builder = new ContainerBuilder();
+
+ $builder->useAnnotations(false);
+ $builder->setDefinitionCache(new ArrayCache());
+
+ // INI config
+ $builder->addDefinitions(new IniConfigDefinitionSource(Config::getInstance()));
+
+ // Global config
+ $builder->addDefinitions(PIWIK_USER_PATH . '/config/global.php');
+
+ // Plugin configs
+ $this->addPluginConfigs($builder);
+
+ // Development config
+ if (Development::isEnabled()) {
+ $builder->addDefinitions(PIWIK_USER_PATH . '/config/environment/dev.php');
+ }
+
+ // User config
+ if (file_exists(PIWIK_USER_PATH . '/config/config.php')) {
+ $builder->addDefinitions(PIWIK_USER_PATH . '/config/config.php');
+ }
+
+ // Environment config
+ $this->addEnvironmentConfig($builder);
+
+ return $builder->build();
+ }
+
+ private function addEnvironmentConfig(ContainerBuilder $builder)
+ {
+ if (!$this->environment) {
+ return;
+ }
+
+ $file = sprintf('%s/config/environment/%s.php', PIWIK_USER_PATH, $this->environment);
+
+ $builder->addDefinitions($file);
+ }
+
+ private function addPluginConfigs(ContainerBuilder $builder)
+ {
+ $plugins = Manager::getInstance()->getActivatedPluginsFromConfig();
+
+ foreach ($plugins as $plugin) {
+ $file = Manager::getPluginsDirectory() . $plugin . '/config/config.php';
+
+ if (! file_exists($file)) {
+ continue;
+ }
+
+ $builder->addDefinitions($file);
+ }
+ }
+}
diff --git a/core/Container/IniConfigDefinitionSource.php b/core/Container/IniConfigDefinitionSource.php
index b84d33d94f..9f5b32c226 100644
--- a/core/Container/IniConfigDefinitionSource.php
+++ b/core/Container/IniConfigDefinitionSource.php
@@ -9,16 +9,14 @@
namespace Piwik\Container;
use DI\Definition\Exception\DefinitionException;
-use DI\Definition\MergeableDefinition;
use DI\Definition\Source\ChainableDefinitionSource;
-use DI\Definition\Source\DefinitionSource;
use DI\Definition\ValueDefinition;
use Piwik\Config;
/**
* Import the old INI config into PHP-DI.
*/
-class IniConfigDefinitionSource implements DefinitionSource, ChainableDefinitionSource
+class IniConfigDefinitionSource extends ChainableDefinitionSource
{
/**
* @var Config
@@ -31,29 +29,19 @@ class IniConfigDefinitionSource implements DefinitionSource, ChainableDefinition
private $prefix;
/**
- * @var DefinitionSource
- */
- private $chainedSource;
-
- /**
* @param Config $config
* @param string $prefix Prefix for the container entries.
*/
- public function __construct(Config $config, $prefix = 'old_config.')
+ public function __construct(Config $config, $prefix = 'ini.')
{
$this->config = $config;
$this->prefix = $prefix;
}
- public function getDefinition($name, MergeableDefinition $parentDefinition = null)
+ protected function findDefinition($name)
{
- // INI only contains values, so no definition merging here
- if ($parentDefinition) {
- return $this->notFound($name, $parentDefinition);
- }
-
if (strpos($name, $this->prefix) !== 0) {
- return $this->notFound($name, $parentDefinition);
+ return null;
}
list($sectionName, $configKey) = $this->parseEntryName($name);
@@ -65,17 +53,12 @@ class IniConfigDefinitionSource implements DefinitionSource, ChainableDefinition
}
if (! array_key_exists($configKey, $section)) {
- return $this->notFound($name, $parentDefinition);
+ return null;
}
return new ValueDefinition($name, $section[$configKey]);
}
- public function chain(DefinitionSource $source)
- {
- $this->chainedSource = $source;
- }
-
private function parseEntryName($name)
{
$parts = explode('.', $name, 3);
@@ -102,13 +85,4 @@ class IniConfigDefinitionSource implements DefinitionSource, ChainableDefinition
return $section;
}
-
- private function notFound($name, $parentDefinition)
- {
- if ($this->chainedSource) {
- return $this->chainedSource->getDefinition($name, $parentDefinition);
- }
-
- return null;
- }
}
diff --git a/core/Container/StaticContainer.php b/core/Container/StaticContainer.php
index b46f01e1ca..874851acde 100644
--- a/core/Container/StaticContainer.php
+++ b/core/Container/StaticContainer.php
@@ -9,9 +9,6 @@
namespace Piwik\Container;
use DI\Container;
-use DI\ContainerBuilder;
-use Doctrine\Common\Cache\ArrayCache;
-use Piwik\Config;
/**
* This class provides a static access to the container.
@@ -28,6 +25,13 @@ class StaticContainer
private static $container;
/**
+ * Optional environment config to load.
+ *
+ * @var bool
+ */
+ private static $environment;
+
+ /**
* @return Container
*/
public static function getContainer()
@@ -39,33 +43,49 @@ class StaticContainer
return self::$container;
}
+ public static function clearContainer()
+ {
+ self::$container = null;
+ }
+
+ /**
+ * Only use this in tests.
+ *
+ * @param Container $container
+ */
+ public static function set(Container $container)
+ {
+ self::$container = $container;
+ }
+
/**
* @link http://php-di.org/doc/container-configuration.html
*/
private static function createContainer()
{
- if (!class_exists('DI\ContainerBuilder')) {
- throw new \Exception('DI\ContainerBuilder could not be found, maybe you are using Piwik from git and need to update Composer: php composer.phar update');
- }
-
- $builder = new ContainerBuilder();
-
- $builder->useAnnotations(false);
-
- // TODO set a better cache
- $builder->setDefinitionCache(new ArrayCache());
-
- // Old global INI config
- $builder->addDefinitions(new IniConfigDefinitionSource(Config::getInstance()));
-
- // Global config
- $builder->addDefinitions(PIWIK_USER_PATH . '/config/global.php');
+ $containerFactory = new ContainerFactory(self::$environment);
+ return $containerFactory->create();
+ }
- // User config
- if (file_exists(PIWIK_USER_PATH . '/config/config.php')) {
- $builder->addDefinitions(PIWIK_USER_PATH . '/config/config.php');
- }
+ /**
+ * Set the application environment (cli, test, …) or null for the default one.
+ *
+ * @param string|null $environment
+ */
+ public static function setEnvironment($environment)
+ {
+ self::$environment = $environment;
+ }
- return $builder->build();
+ /**
+ * Proxy to Container::get()
+ *
+ * @param string $name Container entry name.
+ * @return mixed
+ * @throws \DI\NotFoundException
+ */
+ public static function get($name)
+ {
+ return self::getContainer()->get($name);
}
}
diff --git a/core/Cookie.php b/core/Cookie.php
index 993d97f1e2..39c0de95e4 100644
--- a/core/Cookie.php
+++ b/core/Cookie.php
@@ -374,7 +374,8 @@ class Cookie
*/
public function __toString()
{
- $str = 'COOKIE ' . $this->name . ', rows count: ' . count($this->value) . ', cookie size = ' . strlen($this->generateContentString()) . " bytes\n";
+ $str = 'COOKIE ' . $this->name . ', rows count: ' . count($this->value) . ', cookie size = ' . strlen($this->generateContentString()) . " bytes, ";
+ $str .= 'path: ' . $this->path. ', expire: ' . $this->expire . "\n";
$str .= var_export($this->value, $return = true);
return $str;
diff --git a/core/CronArchive.php b/core/CronArchive.php
index eb4e45e383..29c897c8bf 100644
--- a/core/CronArchive.php
+++ b/core/CronArchive.php
@@ -12,10 +12,15 @@ use Exception;
use Piwik\ArchiveProcessor\Rules;
use Piwik\CronArchive\FixedSiteIds;
use Piwik\CronArchive\SharedSiteIds;
+use Piwik\DataAccess\ArchiveInvalidator;
+use Piwik\Exception\UnexpectedWebsiteFoundException;
use Piwik\Metrics\Formatter;
use Piwik\Period\Factory as PeriodFactory;
use Piwik\DataAccess\InvalidatedReports;
+use Piwik\Plugins\CoreAdminHome\API as CoreAdminHomeAPI;
use Piwik\Plugins\SitesManager\API as APISitesManager;
+use Piwik\Plugins\UsersManager\API as APIUsersManager;
+use Piwik\Plugins\UsersManager\UserPreferences;
/**
* ./console core:archive runs as a cron and is a useful tool for general maintenance,
@@ -73,6 +78,7 @@ class CronArchive
private $segments = array();
private $piwikUrl = false;
private $token_auth = false;
+ private $validTokenAuths = array();
private $visitsToday = 0;
private $requests = 0;
private $output = '';
@@ -81,6 +87,8 @@ class CronArchive
private $lastSuccessRunTimestamp = false;
private $errors = array();
+ private $apiToInvalidateArchivedReport;
+
const NO_ERROR = "no error";
public $testmode = false;
@@ -214,7 +222,6 @@ class CronArchive
{
$this->formatter = new Formatter();
- $this->initLog();
$this->initPiwikHost($piwikUrl);
$this->initCore();
$this->initTokenAuth();
@@ -250,9 +257,11 @@ class CronArchive
$this->allWebsites = APISitesManager::getInstance()->getAllSitesId();
if (!empty($this->shouldArchiveOnlySpecificPeriods)) {
- $this->log("- Will process the following periods: " . implode(", ", $this->shouldArchiveOnlySpecificPeriods) . " (--force-periods)");
+ $this->log("- Will only process the following periods: " . implode(", ", $this->shouldArchiveOnlySpecificPeriods) . " (--force-periods)");
}
+ $this->invalidateArchivedReportsForSitesThatNeedToBeArchivedAgain();
+
$websitesIds = $this->initWebsiteIds();
$this->filterWebsiteIds($websitesIds);
@@ -393,7 +402,8 @@ class CronArchive
public function logFatalError($m)
{
$this->logError($m);
- exit(1);
+
+ throw new Exception($m);
}
public function runScheduledTasks()
@@ -426,10 +436,10 @@ class CronArchive
if ($this->archiveAndRespectTTL) {
Option::clearCachedOption($this->lastRunKey($idSite, "periods"));
- $lastTimestampWebsiteProcessedPeriods = Option::get($this->lastRunKey($idSite, "periods"));
+ $lastTimestampWebsiteProcessedPeriods = $this->getPeriodLastProcessedTimestamp($idSite);
Option::clearCachedOption($this->lastRunKey($idSite, "day"));
- $lastTimestampWebsiteProcessedDay = Option::get($this->lastRunKey($idSite, "day"));
+ $lastTimestampWebsiteProcessedDay = $this->getDayLastProcessedTimestamp($idSite);
}
$this->updateIdSitesInvalidatedOldReports();
@@ -501,7 +511,14 @@ class CronArchive
return false;
}
- $shouldProceed = $this->processArchiveDays($idSite, $lastTimestampWebsiteProcessedDay, $shouldArchivePeriods, $timerWebsite);
+ try {
+ $shouldProceed = $this->processArchiveDays($idSite, $lastTimestampWebsiteProcessedDay, $shouldArchivePeriods, $timerWebsite);
+ } catch(UnexpectedWebsiteFoundException $e) {
+ // this website was deleted in the meantime
+ $shouldProceed = false;
+ $this->log("Skipped website id $idSite, got: UnexpectedWebsiteFoundException, " . $timerWebsite->__toString());
+ }
+
if (!$shouldProceed) {
return false;
}
@@ -515,18 +532,8 @@ class CronArchive
return false;
}
- $success = true;
- foreach (array('week', 'month', 'year') as $period) {
-
- if (!$this->shouldProcessPeriod($period)) {
- // if any period was skipped, we do not mark the Periods archiving as successful
- $success = false;
- continue;
- }
+ $success = $this->processArchiveForPeriods($idSite, $lastTimestampWebsiteProcessedPeriods);
- $success = $this->archiveVisitsAndSegments($idSite, $period, $lastTimestampWebsiteProcessedPeriods)
- && $success;
- }
// Record succesful run of this website's periods archiving
if ($success) {
Option::set($this->lastRunKey($idSite, "periods"), time());
@@ -554,6 +561,37 @@ class CronArchive
}
/**
+ * @param $idSite
+ * @param $lastTimestampWebsiteProcessedPeriods
+ * @return bool
+ */
+ private function processArchiveForPeriods($idSite, $lastTimestampWebsiteProcessedPeriods)
+ {
+ $success = true;
+
+ foreach (array('week', 'month', 'year') as $period) {
+
+ if (!$this->shouldProcessPeriod($period)) {
+ // if any period was skipped, we do not mark the Periods archiving as successful
+ $success = false;
+ continue;
+ }
+
+ $date = $this->getApiDateParameter($idSite, $period, $lastTimestampWebsiteProcessedPeriods);
+ $periodArchiveWasSuccessful = $this->archiveVisitsAndSegments($idSite, $period, $date);
+ $success = $periodArchiveWasSuccessful && $success;
+ }
+
+ // period=range
+ $customDateRangesToPreProcessForSite = $this->getCustomDateRangeToPreProcess($idSite);
+ foreach ($customDateRangesToPreProcessForSite as $dateRange) {
+ $periodArchiveWasSuccessful = $this->archiveVisitsAndSegments($idSite, 'range', $dateRange);
+ $success = $periodArchiveWasSuccessful && $success;
+ }
+ return $success;
+ }
+
+ /**
* Checks the config file is found.
*
* @param $piwikUrl
@@ -682,7 +720,8 @@ class CronArchive
$this->visitsToday += $visitsToday;
$this->websitesWithVisitsSinceLastRun++;
- $this->archiveVisitsAndSegments($idSite, "day", $processDaysSince);
+
+ $this->archiveVisitsAndSegments($idSite, "day", $this->getApiDateParameter($idSite, "day", $processDaysSince));
$this->logArchivedWebsite($idSite, "day", $date, $visitsLastDays, $visitsToday, $timerWebsite);
return true;
@@ -705,17 +744,16 @@ class CronArchive
* Requests are triggered using cURL multi handle
*
* @param $idSite int
- * @param $period
- * @param $lastTimestampWebsiteProcessed
+ * @param $period string
+ * @param $date string
* @return bool True on success, false if some request failed
*/
- private function archiveVisitsAndSegments($idSite, $period, $lastTimestampWebsiteProcessed)
+ private function archiveVisitsAndSegments($idSite, $period, $date)
{
$timer = new Timer();
$url = $this->piwikUrl;
- $date = $this->getApiDateParameter($idSite, $period, $lastTimestampWebsiteProcessed);
$url .= $this->getVisitsRequestUrl($idSite, $period, $date);
$url .= self::APPEND_TO_API_REQUEST;
@@ -754,6 +792,12 @@ class CronArchive
$this->logError("Error unserializing the following response from $url: " . $content);
}
+ if($period == 'range') {
+ // range returns one dataset (the sum of data between the two dates),
+ // whereas other periods return lastN which is N datasets in an array. Here we make our period=range dataset look like others:
+ $stats = array($stats);
+ }
+
$visitsInLastPeriods = $this->getVisitsFromApiResponse($stats);
$visitsLastPeriod = $this->getVisitsLastPeriodFromApiResponse($stats);
}
@@ -781,11 +825,7 @@ class CronArchive
public function log($m)
{
$this->output .= $m . "\n";
- try {
- Log::info($m);
- } catch(Exception $e) {
- print($m . "\n");
- }
+ Log::info($m);
}
public function logError($m)
@@ -806,6 +846,7 @@ class CronArchive
} else {
$message .= "Response was '$response'";
}
+
$this->logError($message);
return false;
}
@@ -853,28 +894,6 @@ class CronArchive
}
/**
- * Configures Piwik\Log so messages are written in output
- */
- private function initLog()
- {
- $config = Config::getInstance();
-
- $log = $config->log;
- $log['log_only_when_debug_parameter'] = 0;
- $log[Log::LOG_WRITERS_CONFIG_OPTION][] = "screen";
-
- $config->log = $log;
-
- Log::unsetInstance();
-
- // Make sure we log at least INFO (if logger is set to DEBUG then keep it)
- $logLevel = Log::getInstance()->getLogLevel();
- if ($logLevel < Log::INFO) {
- Log::getInstance()->setLogLevel(Log::INFO);
- }
- }
-
- /**
* Init Piwik, connect DB, create log & config objects, etc.
*/
private function initCore()
@@ -894,7 +913,7 @@ class CronArchive
{
$this->todayArchiveTimeToLive = Rules::getTodayArchiveTimeToLive();
$this->processPeriodsMaximumEverySeconds = $this->getDelayBetweenPeriodsArchives();
- $this->lastSuccessRunTimestamp = Option::get(self::OPTION_ARCHIVING_FINISHED_TS);
+ $this->lastSuccessRunTimestamp = $this->getLastSuccessRunTimestamp();
$this->shouldArchiveOnlySitesWithTrafficSince = $this->isShouldArchiveAllSitesWithTrafficSince();
$this->shouldArchiveOnlySpecificPeriods = $this->getPeriodsToProcess();
@@ -936,6 +955,40 @@ class CronArchive
}
/**
+ * @internal
+ */
+ public function setApiToInvalidateArchivedReport($api)
+ {
+ $this->apiToInvalidateArchivedReport = $api;
+ }
+
+ private function getApiToInvalidateArchivedReport()
+ {
+ if ($this->apiToInvalidateArchivedReport) {
+ return $this->apiToInvalidateArchivedReport;
+ }
+
+ return CoreAdminHomeAPI::getInstance();
+ }
+
+ public function invalidateArchivedReportsForSitesThatNeedToBeArchivedAgain()
+ {
+ $invalidator = new ArchiveInvalidator();
+ $sitesPerDays = $invalidator->getRememberedArchivedReportsThatShouldBeInvalidated();
+
+ foreach ($sitesPerDays as $date => $siteIds) {
+ $listSiteIds = implode(',', $siteIds);
+
+ try {
+ $this->log('Will invalidate archived reports for ' . $date . ' for following siteIds: ' . $listSiteIds);
+ $this->getApiToInvalidateArchivedReport()->invalidateArchivedReports($siteIds, $date);
+ } catch (Exception $e) {
+ $this->log('Failed to invalidate archived reports: ' . $e->getMessage());
+ }
+ }
+ }
+
+ /**
* Returns the list of sites to loop over and archive.
* @return array
*/
@@ -961,22 +1014,31 @@ class CronArchive
private function initTokenAuth()
{
- $token = '';
+ $tokens = array();
/**
* @ignore
*/
- Piwik::postEvent('CronArchive.getTokenAuth', array(&$token));
-
- $this->token_auth = $token;
+ Piwik::postEvent('CronArchive.getTokenAuth', array(&$tokens));
+
+ $this->validTokenAuths = $tokens;
+ $this->token_auth = array_shift($tokens);
}
- public function getTokenAuth()
+ public function isTokenAuthSuperUserToken($token_auth)
{
- return $this->token_auth;
+ if(empty($token_auth)
+ || strlen($token_auth) != 32) {
+ return false;
+ }
+
+ return in_array($token_auth, $this->validTokenAuths);
}
- private function initPiwikHost($piwikUrl = false)
+ /**
+ * @param string|bool $piwikUrl
+ */
+ protected function initPiwikHost($piwikUrl = false)
{
// If core:archive command run as a web cron, we use the current hostname+path
if (empty($piwikUrl)) {
@@ -998,7 +1060,7 @@ class CronArchive
}
if (!\Piwik\UrlHelper::isLookLikeUrl($piwikUrl)) {
- $this->logFatalErrorUrlExpected();
+ $this->logFatalErrorUrlExpected($piwikUrl);
}
// ensure there is a trailing slash
@@ -1128,7 +1190,7 @@ class CronArchive
/**
* Test that the specified piwik URL is a valid Piwik endpoint.
*/
- private function checkPiwikUrlIsValid()
+ protected function checkPiwikUrlIsValid()
{
$response = $this->request("?module=API&method=API.getDefaultMetricTranslations&format=original&serialize=1");
$responseUnserialized = @unserialize($response);
@@ -1207,10 +1269,11 @@ class CronArchive
return true;
}
- private function logFatalErrorUrlExpected()
+ private function logFatalErrorUrlExpected($piwikUrl = false)
{
- $this->logFatalError("./console core:archive expects the argument 'url' to be set to your Piwik URL, for example: --url=http://example.org/piwik/ "
- . "\n--help for more information");
+ $this->logFatalError("./console core:archive expects the argument 'url' to be set to your Piwik URL, for example: --url=http://example.org/piwik/"
+ . ($piwikUrl ? "\n '$piwikUrl' supplied" : "")
+ . "\nuse --help for more information");
}
private function getVisitsLastPeriodFromApiResponse($stats)
@@ -1272,7 +1335,7 @@ class CronArchive
*/
private function logArchivedWebsite($idSite, $period, $date, $visitsInLastPeriods, $visitsToday, Timer $timer)
{
- if (substr($date, 0, 4) === 'last') {
+ if (strpos($date, 'last') === 0 || strpos($date, 'previous') === 0) {
$visitsInLastPeriods = (int)$visitsInLastPeriods . " visits in last " . $date . " " . $period . "s, ";
$thisPeriod = $period == "day" ? "today" : "this " . $period;
$visitsInLastPeriod = (int)$visitsToday . " visits " . $thisPeriod . ", ";
@@ -1352,7 +1415,8 @@ class CronArchive
$dateLastMax = self::DEFAULT_DATE_LAST_WEEKS;
}
if (empty($lastTimestampWebsiteProcessed)) {
- $lastTimestampWebsiteProcessed = strtotime(\Piwik\Site::getCreationDateFor($idSite));
+ $creationDateFor = \Piwik\Site::getCreationDateFor($idSite);
+ $lastTimestampWebsiteProcessed = strtotime($creationDateFor);
}
// Enforcing last2 at minimum to work around timing issues and ensure we make most archives available
@@ -1379,4 +1443,93 @@ class CronArchive
return self::MAX_CONCURRENT_API_REQUESTS;
}
+
+ /**
+ * @param $idSite
+ * @return false|string
+ */
+ private function getPeriodLastProcessedTimestamp($idSite)
+ {
+ $timestamp = Option::get($this->lastRunKey($idSite, "periods"));
+ return $this->sanitiseTimestamp($timestamp);
+ }
+
+ /**
+ * @param $idSite
+ * @return false|string
+ */
+ private function getDayLastProcessedTimestamp($idSite)
+ {
+ $timestamp = Option::get($this->lastRunKey($idSite, "day"));
+ return $this->sanitiseTimestamp($timestamp);
+ }
+
+ /**
+ * @return false|string
+ */
+ private function getLastSuccessRunTimestamp()
+ {
+ $timestamp = Option::get(self::OPTION_ARCHIVING_FINISHED_TS);
+ return $this->sanitiseTimestamp($timestamp);
+ }
+
+ private function sanitiseTimestamp($timestamp)
+ {
+ $now = time();
+ return ($timestamp < $now) ? $timestamp : $now;
+ }
+
+ /**
+ * @param $idSite
+ * @return array of date strings
+ */
+ private function getCustomDateRangeToPreProcess($idSite)
+ {
+ static $cache = null;
+ if(is_null($cache)) {
+ $cache = $this->loadCustomDateRangeToPreProcess();
+ }
+ if(empty($cache[$idSite])) {
+ return array();
+ }
+ $dates = array_unique($cache[$idSite]);
+ return $dates;
+ }
+
+ /**
+ * @return array
+ */
+ private function loadCustomDateRangeToPreProcess()
+ {
+ $customDateRangesToProcessForSites = array();
+ // For all users who have selected this website to load by default,
+ // we load the default period/date that will be loaded for this user
+ // and make sure it's pre-archived
+ $userPreferences = APIUsersManager::getInstance()->getAllUsersPreferences(array(APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE, APIUsersManager::PREFERENCE_DEFAULT_REPORT));
+ foreach ($userPreferences as $userLogin => $userPreferences) {
+
+ $defaultDate = $userPreferences[APIUsersManager::PREFERENCE_DEFAULT_REPORT_DATE];
+ $preference = new UserPreferences();
+ $period = $preference->getDefaultPeriod($defaultDate);
+ if ($period != 'range') {
+ continue;
+ }
+
+ if (isset($userPreferences[APIUsersManager::PREFERENCE_DEFAULT_REPORT])
+ && is_numeric($userPreferences[APIUsersManager::PREFERENCE_DEFAULT_REPORT])) {
+ // If user selected one particular website ID
+ $idSites = array($userPreferences[APIUsersManager::PREFERENCE_DEFAULT_REPORT]);
+ } else {
+ // If user selected "All websites" or some other random value, we pre-process all websites that he has access to
+ $idSites = APISitesManager::getInstance()->getSitesIdWithAtLeastViewAccess($userLogin);
+ }
+
+ foreach ($idSites as $idSite) {
+ $customDateRangesToProcessForSites[$idSite][] = $defaultDate;
+ }
+ }
+
+ return $customDateRangesToProcessForSites;
+ }
+
}
diff --git a/core/DataAccess/Actions.php b/core/DataAccess/Actions.php
new file mode 100644
index 0000000000..9efc4fc2a5
--- /dev/null
+++ b/core/DataAccess/Actions.php
@@ -0,0 +1,34 @@
+<?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\DataAccess;
+
+use Piwik\Db;
+use Piwik\Common;
+
+/**
+ * Data Access Object for operations dealing with the log_action table.
+ */
+class Actions
+{
+ /**
+ * Removes a list of actions from the log_action table by ID.
+ *
+ * @param int[] $idActions
+ */
+ public function delete($idActions)
+ {
+ foreach ($idActions as &$id) {
+ $id = (int)$id;
+ }
+
+ $table = Common::prefixTable('log_action');
+
+ $sql = "DELETE FROM $table WHERE idaction IN (" . implode(",", $idActions) . ")";
+ Db::query($sql);
+ }
+} \ No newline at end of file
diff --git a/core/DataAccess/ArchiveInvalidator.php b/core/DataAccess/ArchiveInvalidator.php
index 6648c637c8..916d94991b 100644
--- a/core/DataAccess/ArchiveInvalidator.php
+++ b/core/DataAccess/ArchiveInvalidator.php
@@ -9,12 +9,14 @@
namespace Piwik\DataAccess;
-
use Piwik\Date;
use Piwik\Db;
+use Piwik\Option;
use Piwik\Plugins\PrivacyManager\PrivacyManager;
use Piwik\Period;
use Piwik\Period\Week;
+use Piwik\Plugins\SitesManager\Model as SitesManagerModel;
+use Piwik\Site;
/**
* Marks archives as Invalidated by setting the done flag to a special value (see Model->updateArchiveAsInvalidated)
@@ -32,6 +34,73 @@ class ArchiveInvalidator {
private $minimumDateWithLogs = false;
private $invalidDates = array();
+ private $rememberArchivedReportIdStart = 'report_to_invalidate_';
+
+ public function rememberToInvalidateArchivedReportsLater($idSite, Date $date)
+ {
+ $key = $this->buildRememberArchivedReportId($idSite, $date->toString());
+ $value = Option::get($key);
+
+ // we do not really have to get the value first. we could simply always try to call set() and it would update or
+ // insert the record if needed but we do not want to lock the table (especially since there are still some
+ // MyISAM installations)
+
+ if (false === $value) {
+ Option::set($key, '1');
+ }
+ }
+
+ public function getRememberedArchivedReportsThatShouldBeInvalidated()
+ {
+ $reports = Option::getLike($this->rememberArchivedReportIdStart . '%_%');
+
+ $sitesPerDay = array();
+
+ foreach ($reports as $report => $value) {
+ $report = str_replace($this->rememberArchivedReportIdStart, '', $report);
+ $report = explode('_', $report);
+ $siteId = (int) $report[0];
+ $date = $report[1];
+
+ if (empty($sitesPerDay[$date])) {
+ $sitesPerDay[$date] = array();
+ }
+
+ $sitesPerDay[$date][] = $siteId;
+ }
+
+ return $sitesPerDay;
+ }
+
+ private function buildRememberArchivedReportId($idSite, $date)
+ {
+ $id = $this->buildRememberArchivedReportIdForSite($idSite);
+ $id .= '_' . trim($date);
+
+ return $id;
+ }
+
+ private function buildRememberArchivedReportIdForSite($idSite)
+ {
+ return $this->rememberArchivedReportIdStart . (int) $idSite;
+ }
+
+ public function forgetRememberedArchivedReportsToInvalidateForSite($idSite)
+ {
+ $id = $this->buildRememberArchivedReportIdForSite($idSite) . '_%';
+ Option::deleteLike($id);
+ }
+
+ /**
+ * @internal
+ */
+ public function forgetRememberedArchivedReportsToInvalidate($idSite, Date $date)
+ {
+ $id = $this->buildRememberArchivedReportId($idSite, $date->toString());
+
+ Option::delete($id);
+ }
+
/**
* @param $idSites array
* @param $dates string
@@ -45,16 +114,31 @@ class ArchiveInvalidator {
$datesToInvalidate = $this->getDatesToInvalidateFromString($dates);
$minDate = $this->getMinimumDateToInvalidate($datesToInvalidate);
- \Piwik\Plugins\SitesManager\API::getInstance()->updateSiteCreatedTime($idSites, $minDate);
+ $this->updateSiteCreatedTime($idSites, $minDate);
$datesByMonth = $this->getDatesByYearMonth($datesToInvalidate);
$this->markArchivesInvalidatedFor($idSites, $period, $datesByMonth);
$this->persistInvalidatedArchives($idSites, $datesByMonth);
+ foreach ($idSites as $idSite) {
+ foreach ($datesToInvalidate as $date) {
+ $this->forgetRememberedArchivedReportsToInvalidate($idSite, $date);
+ }
+ }
+
return $this->makeOutputLogs();
}
+ private function updateSiteCreatedTime($idSites, Date $minDate)
+ {
+ $idSites = Site::getIdSitesFromIdSitesString($idSites);
+ $minDateSql = $minDate->subDay(1)->getDatetime();
+
+ $model = new SitesManagerModel();
+ $model->updateSiteCreatedTime($idSites, $minDateSql);
+ }
+
/**
* @param $toInvalidate
* @return bool|Date
@@ -90,7 +174,12 @@ class ArchiveInvalidator {
// In each table, invalidate day/week/month/year containing this date
$archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
- foreach ($archiveTables as $table) {
+
+ $archiveNumericTables = array_filter($archiveTables, function($name) {
+ return ArchiveTableCreator::getTypeFromTableName($name) == ArchiveTableCreator::NUMERIC_TABLE;
+ });
+
+ foreach ($archiveNumericTables as $table) {
// Extract Y_m from table name
$suffix = ArchiveTableCreator::getDateFromTableName($table);
if (!isset($datesByMonth[$suffix])) {
@@ -105,7 +194,7 @@ class ArchiveInvalidator {
/**
* Ensure the specified dates are valid.
* Store invalid date so we can log them
- * @param $dates string
+ * @param array $dates
* @return Date[]
*/
private function getDatesToInvalidateFromString($dates)
@@ -129,6 +218,7 @@ class ArchiveInvalidator {
$this->invalidDates[] = $theDate;
}
}
+
return $toInvalidate;
}
@@ -136,13 +226,13 @@ class ArchiveInvalidator {
{
// If using the feature "Delete logs older than N days"...
$purgeDataSettings = PrivacyManager::getPurgeDataSettings();
- $logsAreDeletedBeforeThisDate = $purgeDataSettings['delete_logs_schedule_lowest_interval'];
+ $logsDeletedWhenOlderThanDays = $purgeDataSettings['delete_logs_older_than'];
$logsDeleteEnabled = $purgeDataSettings['delete_logs_enable'];
if ($logsDeleteEnabled
- && $logsAreDeletedBeforeThisDate
+ && $logsDeletedWhenOlderThanDays
) {
- $this->minimumDateWithLogs = Date::factory('today')->subDay($logsAreDeletedBeforeThisDate);
+ $this->minimumDateWithLogs = Date::factory('today')->subDay($logsDeletedWhenOlderThanDays);
}
}
diff --git a/core/DataAccess/ArchivePurger.php b/core/DataAccess/ArchivePurger.php
index 7a961f99db..e7f185f475 100644
--- a/core/DataAccess/ArchivePurger.php
+++ b/core/DataAccess/ArchivePurger.php
@@ -10,6 +10,7 @@ namespace Piwik\DataAccess;
use Exception;
use Piwik\ArchiveProcessor\Rules;
+use Piwik\Config;
use Piwik\Date;
use Piwik\Db;
use Piwik\Log;
@@ -104,12 +105,13 @@ class ArchivePurger
{
$numericTable = ArchiveTableCreator::getNumericTable($date);
$blobTable = ArchiveTableCreator::getBlobTable($date);
- $yesterday = Date::factory('yesterday')->getDateTime();
+ $daysRangesValid = Config::getInstance()->General['purge_date_range_archives_after_X_days'];
+ $pastDate = Date::factory('today')->subDay($daysRangesValid)->getDateTime();
- self::getModel()->deleteArchivesWithPeriod($numericTable, $blobTable, Piwik::$idPeriods['range'], $yesterday);
+ self::getModel()->deleteArchivesWithPeriod($numericTable, $blobTable, Piwik::$idPeriods['range'], $pastDate);
Log::debug("Purging Custom Range archives: done [ purged archives older than %s from %s / blob ]",
- $yesterday, $numericTable);
+ $pastDate, $numericTable);
}
/**
diff --git a/core/DataAccess/ArchiveSelector.php b/core/DataAccess/ArchiveSelector.php
index 56dede02b7..de9521d756 100644
--- a/core/DataAccess/ArchiveSelector.php
+++ b/core/DataAccess/ArchiveSelector.php
@@ -60,10 +60,9 @@ class ArchiveSelector
$requestedPlugin = $params->getRequestedPlugin();
$segment = $params->getSegment();
- $isSkipAggregationOfSubTables = $params->isSkipAggregationOfSubTables();
$plugins = array("VisitsSummary", $requestedPlugin);
- $doneFlags = Rules::getDoneFlags($plugins, $segment, $isSkipAggregationOfSubTables);
+ $doneFlags = Rules::getDoneFlags($plugins, $segment);
$doneFlagValues = Rules::getSelectableDoneFlagValues();
$results = self::getModel()->getArchiveIdAndVisits($numericTable, $idSite, $period, $dateStartIso, $dateEndIso, $minDatetimeIsoArchiveProcessedUTC, $doneFlags, $doneFlagValues);
@@ -72,8 +71,9 @@ class ArchiveSelector
return false;
}
- $idArchive = self::getMostRecentIdArchiveFromResults($segment, $requestedPlugin, $isSkipAggregationOfSubTables, $results);
- $idArchiveVisitsSummary = self::getMostRecentIdArchiveFromResults($segment, "VisitsSummary", $isSkipAggregationOfSubTables, $results);
+ $idArchive = self::getMostRecentIdArchiveFromResults($segment, $requestedPlugin, $results);
+
+ $idArchiveVisitsSummary = self::getMostRecentIdArchiveFromResults($segment, "VisitsSummary", $results);
list($visits, $visitsConverted) = self::getVisitsMetricsFromResults($idArchive, $idArchiveVisitsSummary, $results);
@@ -113,10 +113,10 @@ class ArchiveSelector
return array($visits, $visitsConverted);
}
- protected static function getMostRecentIdArchiveFromResults(Segment $segment, $requestedPlugin, $isSkipAggregationOfSubTables, $results)
+ protected static function getMostRecentIdArchiveFromResults(Segment $segment, $requestedPlugin, $results)
{
$idArchive = false;
- $namesRequestedPlugin = Rules::getDoneFlags(array($requestedPlugin), $segment, $isSkipAggregationOfSubTables);
+ $namesRequestedPlugin = Rules::getDoneFlags(array($requestedPlugin), $segment);
foreach ($results as $result) {
if ($idArchive === false
@@ -137,7 +137,6 @@ class ArchiveSelector
* @param array $periods
* @param Segment $segment
* @param array $plugins List of plugin names for which data is being requested.
- * @param bool $isSkipAggregationOfSubTables Whether we are selecting an archive that may be partial (no sub-tables)
* @return array Archive IDs are grouped by archive name and period range, ie,
* array(
* 'VisitsSummary.done' => array(
@@ -146,7 +145,7 @@ class ArchiveSelector
* )
* @throws
*/
- public static function getArchiveIds($siteIds, $periods, $segment, $plugins, $isSkipAggregationOfSubTables = false)
+ public static function getArchiveIds($siteIds, $periods, $segment, $plugins)
{
if (empty($siteIds)) {
throw new \Exception("Website IDs could not be read from the request, ie. idSite=");
@@ -154,9 +153,9 @@ class ArchiveSelector
$getArchiveIdsSql = "SELECT idsite, name, date1, date2, MAX(idarchive) as idarchive
FROM %s
- WHERE %s
- AND " . self::getNameCondition($plugins, $segment, $isSkipAggregationOfSubTables) . "
- AND idsite IN (" . implode(',', $siteIds) . ")
+ WHERE idsite IN (" . Common::getSqlStringFieldsArray($siteIds) . ")
+ AND " . self::getNameCondition($plugins, $segment) . "
+ AND %s
GROUP BY idsite, date1, date2";
$monthToPeriods = array();
@@ -171,7 +170,7 @@ class ArchiveSelector
foreach ($monthToPeriods as $table => $periods) {
$firstPeriod = reset($periods);
- $bind = array();
+ $bind = array_values($siteIds);
if ($firstPeriod instanceof Range) {
$dateCondition = "period = ? AND date1 = ? AND date2 = ?";
@@ -282,14 +281,13 @@ class ArchiveSelector
*
* @param array $plugins
* @param Segment $segment
- * @param bool $isSkipAggregationOfSubTables
* @return string
*/
- private static function getNameCondition(array $plugins, Segment $segment, $isSkipAggregationOfSubTables)
+ private static function getNameCondition(array $plugins, Segment $segment)
{
// the flags used to tell how the archiving process for a specific archive was completed,
// if it was completed
- $doneFlags = Rules::getDoneFlags($plugins, $segment, $isSkipAggregationOfSubTables);
+ $doneFlags = Rules::getDoneFlags($plugins, $segment);
$allDoneFlags = "'" . implode("','", $doneFlags) . "'";
$possibleValues = Rules::getSelectableDoneFlagValues();
diff --git a/core/DataAccess/ArchiveWriter.php b/core/DataAccess/ArchiveWriter.php
index 1fcacf790d..32943ceeb4 100644
--- a/core/DataAccess/ArchiveWriter.php
+++ b/core/DataAccess/ArchiveWriter.php
@@ -67,7 +67,7 @@ class ArchiveWriter
$this->period = $params->getPeriod();
$idSites = array($this->idSite);
- $this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin(), $params->isSkipAggregationOfSubTables());
+ $this->doneFlag = Rules::getDoneStringFlagFor($idSites, $this->segment, $this->period->getLabel(), $params->getRequestedPlugin());
$this->isArchiveTemporary = $isArchiveTemporary;
$this->dateStart = $this->period->getDateStart();
diff --git a/core/DataAccess/LogAggregator.php b/core/DataAccess/LogAggregator.php
index 83946b1f6f..8b8d5175ff 100644
--- a/core/DataAccess/LogAggregator.php
+++ b/core/DataAccess/LogAggregator.php
@@ -158,14 +158,15 @@ class LogAggregator
protected function getVisitsMetricFields()
{
return array(
- Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_VISIT_TABLE . ".idvisitor)",
- Metrics::INDEX_NB_VISITS => "count(*)",
- Metrics::INDEX_NB_ACTIONS => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_actions)",
- Metrics::INDEX_MAX_ACTIONS => "max(" . self::LOG_VISIT_TABLE . ".visit_total_actions)",
- Metrics::INDEX_SUM_VISIT_LENGTH => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_time)",
- Metrics::INDEX_BOUNCE_COUNT => "sum(case " . self::LOG_VISIT_TABLE . ".visit_total_actions when 1 then 1 when 0 then 1 else 0 end)",
- Metrics::INDEX_NB_VISITS_CONVERTED => "sum(case " . self::LOG_VISIT_TABLE . ".visit_goal_converted when 1 then 1 else 0 end)",
- Metrics::INDEX_NB_USERS => "count(distinct " . self::LOG_VISIT_TABLE . ".user_id)",
+ Metrics::INDEX_NB_UNIQ_VISITORS => "count(distinct " . self::LOG_VISIT_TABLE . ".idvisitor)",
+ Metrics::INDEX_NB_UNIQ_FINGERPRINTS => "count(distinct " . self::LOG_VISIT_TABLE . ".config_id)",
+ Metrics::INDEX_NB_VISITS => "count(*)",
+ Metrics::INDEX_NB_ACTIONS => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_actions)",
+ Metrics::INDEX_MAX_ACTIONS => "max(" . self::LOG_VISIT_TABLE . ".visit_total_actions)",
+ Metrics::INDEX_SUM_VISIT_LENGTH => "sum(" . self::LOG_VISIT_TABLE . ".visit_total_time)",
+ Metrics::INDEX_BOUNCE_COUNT => "sum(case " . self::LOG_VISIT_TABLE . ".visit_total_actions when 1 then 1 when 0 then 1 else 0 end)",
+ Metrics::INDEX_NB_VISITS_CONVERTED => "sum(case " . self::LOG_VISIT_TABLE . ".visit_goal_converted when 1 then 1 else 0 end)",
+ Metrics::INDEX_NB_USERS => "count(distinct " . self::LOG_VISIT_TABLE . ".user_id)",
);
}
@@ -445,8 +446,14 @@ class LogAggregator
protected function isMetricRequested($metricId, $metricsRequested)
{
- return $metricsRequested === false
- || in_array($metricId, $metricsRequested);
+ // do not process INDEX_NB_UNIQ_FINGERPRINTS unless specifically asked for
+ if($metricsRequested === false) {
+ if($metricId == Metrics::INDEX_NB_UNIQ_FINGERPRINTS) {
+ return false;
+ }
+ return true;
+ }
+ return in_array($metricId, $metricsRequested);
}
protected function getWhereStatement($tableName, $datetimeField, $extraWhere = false)
diff --git a/core/DataAccess/LogQueryBuilder.php b/core/DataAccess/LogQueryBuilder.php
new file mode 100644
index 0000000000..a5b2fcf986
--- /dev/null
+++ b/core/DataAccess/LogQueryBuilder.php
@@ -0,0 +1,284 @@
+<?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\DataAccess;
+
+
+use Exception;
+use Piwik\Common;
+use Piwik\Segment\SegmentExpression;
+
+class LogQueryBuilder
+{
+ public function __construct(SegmentExpression $segmentExpression)
+ {
+ $this->segmentExpression = $segmentExpression;
+ }
+
+ public function getSelectQueryString($select, $from, $where, $bind, $groupBy, $orderBy, $limit)
+ {
+ if (!is_array($from)) {
+ $from = array($from);
+ }
+
+ if(!$this->segmentExpression->isEmpty()) {
+ $this->segmentExpression->parseSubExpressionsIntoSqlExpressions($from);
+ $segmentSql = $this->segmentExpression->getSql();
+ $where = $this->getWhereMatchBoth($where, $segmentSql['where']);
+ $bind = array_merge($bind, $segmentSql['bind']);
+ }
+
+ $joins = $this->generateJoinsString($from);
+ $joinWithSubSelect = $joins['joinWithSubSelect'];
+ $from = $joins['sql'];
+
+ if ($joinWithSubSelect) {
+ $sql = $this->buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit);
+ } else {
+ $sql = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit);
+ }
+ return array(
+ 'sql' => $sql,
+ 'bind' => $bind
+ );
+ }
+
+
+ /**
+ * Generate the join sql based on the needed tables
+ * @param array $tables tables to join
+ * @throws Exception if tables can't be joined
+ * @return array
+ */
+ private function generateJoinsString($tables)
+ {
+ $knownTables = array("log_visit", "log_link_visit_action", "log_conversion", "log_conversion_item");
+ $visitsAvailable = $actionsAvailable = $conversionsAvailable = $conversionItemAvailable = false;
+ $joinWithSubSelect = false;
+ $sql = '';
+
+ // make sure the tables are joined in the right order
+ // base table first, then action before conversion
+ // this way, conversions can be joined on idlink_va
+ $actionIndex = array_search("log_link_visit_action", $tables);
+ $conversionIndex = array_search("log_conversion", $tables);
+ if ($actionIndex > 0 && $conversionIndex > 0 && $actionIndex > $conversionIndex) {
+ $tables[$actionIndex] = "log_conversion";
+ $tables[$conversionIndex] = "log_link_visit_action";
+ }
+
+ // same as above: action before visit
+ $actionIndex = array_search("log_link_visit_action", $tables);
+ $visitIndex = array_search("log_visit", $tables);
+ if ($actionIndex > 0 && $visitIndex > 0 && $actionIndex > $visitIndex) {
+ $tables[$actionIndex] = "log_visit";
+ $tables[$visitIndex] = "log_link_visit_action";
+ }
+
+ foreach ($tables as $i => $table) {
+ if (is_array($table)) {
+ // join condition provided
+ $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table'];
+ $sql .= "
+ LEFT JOIN " . Common::prefixTable($table['table']) . " AS " . $alias
+ . " ON " . $table['joinOn'];
+ continue;
+ }
+
+ if (!in_array($table, $knownTables)) {
+ throw new Exception("Table '$table' can't be used for segmentation");
+ }
+
+ $tableSql = Common::prefixTable($table) . " AS $table";
+
+ if ($i == 0) {
+ // first table
+ $sql .= $tableSql;
+ } else {
+ if ($actionsAvailable && $table == "log_conversion") {
+ // have actions, need conversions => join on idlink_va
+ $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va "
+ . "AND log_conversion.idsite = log_link_visit_action.idsite";
+ } else if ($actionsAvailable && $table == "log_visit") {
+ // have actions, need visits => join on idvisit
+ $join = "log_visit.idvisit = log_link_visit_action.idvisit";
+ } else if ($visitsAvailable && $table == "log_link_visit_action") {
+ // have visits, need actions => we have to use a more complex join
+ // we don't hande this here, we just return joinWithSubSelect=true in this case
+ $joinWithSubSelect = true;
+ $join = "log_link_visit_action.idvisit = log_visit.idvisit";
+ } else if ($conversionsAvailable && $table == "log_link_visit_action") {
+ // have conversions, need actions => join on idlink_va
+ $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va";
+ } else if (($visitsAvailable && $table == "log_conversion")
+ || ($conversionsAvailable && $table == "log_visit")
+ ) {
+ // have visits, need conversion (or vice versa) => join on idvisit
+ // notice that joining conversions on visits has lower priority than joining it on actions
+ $join = "log_conversion.idvisit = log_visit.idvisit";
+
+ // if conversions are joined on visits, we need a complex join
+ if ($table == "log_conversion") {
+ $joinWithSubSelect = true;
+ }
+ } elseif ($conversionItemAvailable && $table === 'log_visit') {
+ $join = "log_conversion_item.idvisit = log_visit.idvisit";
+ } elseif ($conversionItemAvailable && $table === 'log_link_visit_action') {
+ $join = "log_conversion_item.idvisit = log_link_visit_action.idvisit";
+ } elseif ($conversionItemAvailable && $table === 'log_conversion') {
+ $join = "log_conversion_item.idvisit = log_conversion.idvisit";
+ } else {
+ throw new Exception("Table '$table' can't be joined for segmentation");
+ }
+
+ // the join sql the default way
+ $sql .= "
+ LEFT JOIN $tableSql ON $join";
+ }
+
+ // remember which tables are available
+ $visitsAvailable = ($visitsAvailable || $table == "log_visit");
+ $actionsAvailable = ($actionsAvailable || $table == "log_link_visit_action");
+ $conversionsAvailable = ($conversionsAvailable || $table == "log_conversion");
+ $conversionItemAvailable = ($conversionItemAvailable || $table == "log_conversion_item");
+ }
+
+ $return = array(
+ 'sql' => $sql,
+ 'joinWithSubSelect' => $joinWithSubSelect
+ );
+ return $return;
+
+ }
+
+
+ /**
+ * Build a select query where actions have to be joined on visits (or conversions)
+ * In this case, the query gets wrapped in another query so that grouping by visit is possible
+ * @param string $select
+ * @param string $from
+ * @param string $where
+ * @param string $groupBy
+ * @param string $orderBy
+ * @param string $limit
+ * @throws Exception
+ * @return string
+ */
+ private function buildWrappedSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit)
+ {
+ $matchTables = "(log_visit|log_conversion_item|log_conversion|log_action)";
+ preg_match_all("/". $matchTables ."\.[a-z0-9_\*]+/", $select, $matches);
+ $neededFields = array_unique($matches[0]);
+
+ if (count($neededFields) == 0) {
+ throw new Exception("No needed fields found in select expression. "
+ . "Please use a table prefix.");
+ }
+
+ $innerSelect = implode(", \n", $neededFields);
+ $innerFrom = $from;
+ $innerWhere = $where;
+
+ $innerLimit = $limit;
+ $innerGroupBy = "log_visit.idvisit";
+ $innerOrderBy = "NULL";
+ if($innerLimit && $orderBy) {
+ // only When LIMITing we can apply to the inner query the same ORDER BY as the parent query
+ $innerOrderBy = $orderBy;
+ }
+ if($innerLimit) {
+ // When LIMITing, no need to GROUP BY (GROUPing by is done before the LIMIT which is super slow when large amount of rows is matched)
+ $innerGroupBy = false;
+ }
+
+ $innerQuery = $this->buildSelectQuery($innerSelect, $innerFrom, $innerWhere, $innerGroupBy, $innerOrderBy, $innerLimit);
+
+ $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select);
+ $from = "
+ (
+ $innerQuery
+ ) AS log_inner";
+ $where = false;
+ $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy);
+ $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy);
+ $query = $this->buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit);
+ return $query;
+ }
+
+
+ /**
+ * Build select query the normal way
+ *
+ * @param string $select fieldlist to be selected
+ * @param string $from tablelist to select from
+ * @param string $where where clause
+ * @param string $groupBy group by clause
+ * @param string $orderBy order by clause
+ * @param string $limit limit by clause
+ * @return string
+ */
+ private function buildSelectQuery($select, $from, $where, $groupBy, $orderBy, $limit)
+ {
+ $sql = "
+ SELECT
+ $select
+ FROM
+ $from";
+
+ if ($where) {
+ $sql .= "
+ WHERE
+ $where";
+ }
+
+ if ($groupBy) {
+ $sql .= "
+ GROUP BY
+ $groupBy";
+ }
+
+ if ($orderBy) {
+ $sql .= "
+ ORDER BY
+ $orderBy";
+ }
+
+ $limit = (int)$limit;
+ if ($limit >= 1) {
+ $sql .= "
+ LIMIT
+ $limit";
+ }
+
+ return $sql;
+ }
+
+ /**
+ * @param $where
+ * @param $segmentWhere
+ * @return string
+ * @throws
+ */
+ protected function getWhereMatchBoth($where, $segmentWhere)
+ {
+ if (empty($segmentWhere) && empty($where)) {
+ throw new \Exception("Segment where clause should be non empty.");
+ }
+ if (empty($segmentWhere)) {
+ return $where;
+ }
+ if (empty($where)) {
+ return $segmentWhere;
+ }
+ return "( $where )
+ AND
+ ($segmentWhere)";
+ }
+
+} \ No newline at end of file
diff --git a/core/DataAccess/Model.php b/core/DataAccess/Model.php
index dd020af083..239bce6a23 100644
--- a/core/DataAccess/Model.php
+++ b/core/DataAccess/Model.php
@@ -37,6 +37,9 @@ class Model
// prevent error 'The SELECT would examine more than MAX_JOIN_SIZE rows'
Db::get()->query('SET SQL_BIG_SELECTS=1');
+ $idSites = array_values($idSites);
+ $idSitesString = Common::getSqlStringFieldsArray($idSites);
+
$query = 'SELECT t1.idarchive FROM `' . $archiveTable . '` t1
INNER JOIN `' . $archiveTable . '` t2
ON t1.name = t2.name
@@ -45,13 +48,13 @@ class Model
AND t1.date2 = t2.date2
AND t1.period = t2.period
WHERE t1.value = ' . ArchiveWriter::DONE_INVALIDATED . '
- AND t1.idsite IN (' . implode(",", $idSites) . ')
+ AND t1.idsite IN (' . $idSitesString . ')
AND t2.value IN(' . ArchiveWriter::DONE_OK . ', ' . ArchiveWriter::DONE_OK_TEMPORARY . ')
AND t1.ts_archived < t2.ts_archived
AND t1.name LIKE \'done%\'
';
- $result = Db::fetchAll($query);
+ $result = Db::fetchAll($query, $idSites);
$archiveIds = array_map(
function ($elm) {
@@ -80,6 +83,10 @@ class Model
}
$sql = implode(" OR ", $sql);
+ $idSites = array_values($idSites);
+ $sqlSites = " AND idsite IN (" . Common::getSqlStringFieldsArray($idSites) . ")";
+ $bind = array_merge($bind, $idSites);
+
$sqlPeriod = "";
if ($periodId) {
$sqlPeriod = " AND period = ? ";
@@ -89,7 +96,7 @@ class Model
$query = "UPDATE $archiveTable " .
" SET value = " . ArchiveWriter::DONE_INVALIDATED .
" WHERE ( $sql ) " .
- " AND idsite IN (" . implode(",", $idSites) . ")" .
+ $sqlSites .
$sqlPeriod;
Db::query($query, $bind);
}
@@ -122,12 +129,13 @@ class Model
public function deleteArchiveIds($numericTable, $blobTable, $idsToDelete)
{
- $query = "DELETE FROM %s WHERE idarchive IN (" . implode(',', $idsToDelete) . ")";
+ $idsToDelete = array_values($idsToDelete);
+ $query = "DELETE FROM %s WHERE idarchive IN (" . Common::getSqlStringFieldsArray($idsToDelete) . ")";
- Db::query(sprintf($query, $numericTable));
+ Db::query(sprintf($query, $numericTable), $idsToDelete);
try {
- Db::query(sprintf($query, $blobTable));
+ Db::query(sprintf($query, $blobTable), $idsToDelete);
} catch (Exception $e) {
// Individual blob tables could be missing
}
@@ -195,7 +203,15 @@ class Model
public function allocateNewArchiveId($numericTable)
{
$sequence = new Sequence($numericTable);
- $idarchive = $sequence->getNextId();
+
+ try {
+ $idarchive = $sequence->getNextId();
+ } catch(Exception $e) {
+ // edge case: sequence was not found, create it now
+ $sequence->create();
+
+ $idarchive = $sequence->getNextId();
+ }
return $idarchive;
}
diff --git a/core/DataAccess/TableMetadata.php b/core/DataAccess/TableMetadata.php
new file mode 100644
index 0000000000..92e1e8b34e
--- /dev/null
+++ b/core/DataAccess/TableMetadata.php
@@ -0,0 +1,56 @@
+<?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\DataAccess;
+
+use Piwik\Db;
+
+/**
+ * Data Access Object that can be used to get metadata information about
+ * the MySQL tables Piwik uses.
+ */
+class TableMetadata
+{
+ /**
+ * Returns the list of column names for a table.
+ *
+ * @param string $table Prefixed table name.
+ * @return string[] List of column names..
+ */
+ public function getColumns($table)
+ {
+ $table = str_replace("`", "", $table);
+
+ $columns = Db::fetchAll("SHOW COLUMNS FROM `" . $table . "`");
+
+ $columnNames = array();
+ foreach ($columns as $column) {
+ $columnNames[] = $column['Field'];
+ }
+
+ return $columnNames;
+ }
+
+ /**
+ * Returns the list of idaction columns in a table. A column is
+ * assumed to be an idaction reference if it has `"idaction"` in its
+ * name (eg, `"idaction_url"` or `"idaction_content_name"`.
+ *
+ * @param string $table Prefixed table name.
+ * @return string[]
+ */
+ public function getIdActionColumnNames($table)
+ {
+ $columns = $this->getColumns($table);
+
+ $columns = array_filter($columns, function ($columnName) {
+ return strpos($columnName, 'idaction') !== false;
+ });
+
+ return array_values($columns);
+ }
+} \ No newline at end of file
diff --git a/core/DataFiles/Countries.php b/core/DataFiles/Countries.php
deleted file mode 100644
index 4d752d75ae..0000000000
--- a/core/DataFiles/Countries.php
+++ /dev/null
@@ -1,326 +0,0 @@
-<?php
-/**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
-
-/**
- * Country code and continent database.
- *
- * The mapping of countries to continents is from MaxMind with the exception
- * of Central America. MaxMind groups Central American countries with
- * North America. Piwik previously grouped Central American countries with
- * South America. Given this conflict and the fact that most of Central
- * America lies on its own continental plate (i.e., the Caribbean Plate), we
- * currently use a separate continent code (amc).
- */
-if (!isset($GLOBALS['Piwik_CountryList'])) {
- // Primary reference: ISO 3166-1 alpha-2
- $GLOBALS['Piwik_CountryList'] = array(
- 'ad' => 'eur',
- 'ae' => 'asi',
- 'af' => 'asi',
- 'ag' => 'amc',
- 'ai' => 'amc',
- 'al' => 'eur',
- 'am' => 'asi',
- 'ao' => 'afr',
- 'aq' => 'ant',
- 'ar' => 'ams',
- 'as' => 'oce',
- 'at' => 'eur',
- 'au' => 'oce',
- 'aw' => 'amc',
- 'ax' => 'eur',
- 'az' => 'asi',
- 'ba' => 'eur',
- 'bb' => 'amc',
- 'bd' => 'asi',
- 'be' => 'eur',
- 'bf' => 'afr',
- 'bg' => 'eur',
- 'bh' => 'asi',
- 'bi' => 'afr',
- 'bj' => 'afr',
- 'bl' => 'amc',
- 'bm' => 'amc',
- 'bn' => 'asi',
- 'bo' => 'ams',
- 'bq' => 'amc',
- 'br' => 'ams',
- 'bs' => 'amc',
- 'bt' => 'asi',
- 'bv' => 'ant',
- 'bw' => 'afr',
- 'by' => 'eur',
- 'bz' => 'amc',
- 'ca' => 'amn',
- 'cc' => 'asi',
- 'cd' => 'afr',
- 'cf' => 'afr',
- 'cg' => 'afr',
- 'ch' => 'eur',
- 'ci' => 'afr',
- 'ck' => 'oce',
- 'cl' => 'ams',
- 'cm' => 'afr',
- 'cn' => 'asi',
- 'co' => 'ams',
- 'cr' => 'amc',
- 'cu' => 'amc',
- 'cv' => 'afr',
- 'cw' => 'amc',
- 'cx' => 'asi',
- 'cy' => 'eur',
- 'cz' => 'eur',
- 'de' => 'eur',
- 'dj' => 'afr',
- 'dk' => 'eur',
- 'dm' => 'amc',
- 'do' => 'amc',
- 'dz' => 'afr',
- 'ec' => 'ams',
- 'ee' => 'eur',
- 'eg' => 'afr',
- 'eh' => 'afr',
- 'er' => 'afr',
- 'es' => 'eur',
- 'et' => 'afr',
- 'fi' => 'eur',
- 'fj' => 'oce',
- 'fk' => 'ams',
- 'fm' => 'oce',
- 'fo' => 'eur',
- 'fr' => 'eur',
- 'ga' => 'afr',
- 'gb' => 'eur',
- 'gd' => 'amc',
- 'ge' => 'asi',
- 'gf' => 'ams',
- 'gg' => 'eur',
- 'gh' => 'afr',
- 'gi' => 'eur',
- 'gl' => 'amn',
- 'gm' => 'afr',
- 'gn' => 'afr',
- 'gp' => 'amc',
- 'gq' => 'afr',
- 'gr' => 'eur',
- 'gs' => 'ant',
- 'gt' => 'amc',
- 'gu' => 'oce',
- 'gw' => 'afr',
- 'gy' => 'ams',
- 'hk' => 'asi',
- 'hm' => 'ant',
- 'hn' => 'amc',
- 'hr' => 'eur',
- 'ht' => 'amc',
- 'hu' => 'eur',
- 'id' => 'asi',
- 'ie' => 'eur',
- 'il' => 'asi',
- 'im' => 'eur',
- 'in' => 'asi',
- 'io' => 'asi',
- 'iq' => 'asi',
- 'ir' => 'asi',
- 'is' => 'eur',
- 'it' => 'eur',
- 'je' => 'eur',
- 'jm' => 'amc',
- 'jo' => 'asi',
- 'jp' => 'asi',
- 'ke' => 'afr',
- 'kg' => 'asi',
- 'kh' => 'asi',
- 'ki' => 'oce',
- 'km' => 'afr',
- 'kn' => 'amc',
- 'kp' => 'asi',
- 'kr' => 'asi',
- 'kw' => 'asi',
- 'ky' => 'amc',
- 'kz' => 'asi',
- 'la' => 'asi',
- 'lb' => 'asi',
- 'lc' => 'amc',
- 'li' => 'eur',
- 'lk' => 'asi',
- 'lr' => 'afr',
- 'ls' => 'afr',
- 'lt' => 'eur',
- 'lu' => 'eur',
- 'lv' => 'eur',
- 'ly' => 'afr',
- 'ma' => 'afr',
- 'mc' => 'eur',
- 'md' => 'eur',
- 'me' => 'eur',
- 'mf' => 'amc',
- 'mg' => 'afr',
- 'mh' => 'oce',
- 'mk' => 'eur',
- 'ml' => 'afr',
- 'mm' => 'asi',
- 'mn' => 'asi',
- 'mo' => 'asi',
- 'mp' => 'oce',
- 'mq' => 'amc',
- 'mr' => 'afr',
- 'ms' => 'amc',
- 'mt' => 'eur',
- 'mu' => 'afr',
- 'mv' => 'asi',
- 'mw' => 'afr',
- 'mx' => 'amn',
- 'my' => 'asi',
- 'mz' => 'afr',
- 'na' => 'afr',
- 'nc' => 'oce',
- 'ne' => 'afr',
- 'nf' => 'oce',
- 'ng' => 'afr',
- 'ni' => 'amc',
- 'nl' => 'eur',
- 'no' => 'eur',
- 'np' => 'asi',
- 'nr' => 'oce',
- 'nu' => 'oce',
- 'nz' => 'oce',
- 'om' => 'asi',
- 'pa' => 'amc',
- 'pe' => 'ams',
- 'pf' => 'oce',
- 'pg' => 'oce',
- 'ph' => 'asi',
- 'pk' => 'asi',
- 'pl' => 'eur',
- 'pm' => 'amn',
- 'pn' => 'oce',
- 'pr' => 'amc',
- 'ps' => 'asi',
- 'pt' => 'eur',
- 'pw' => 'oce',
- 'py' => 'ams',
- 'qa' => 'asi',
- 're' => 'afr',
- 'ro' => 'eur',
- 'rs' => 'eur',
- 'ru' => 'eur',
- 'rw' => 'afr',
- 'sa' => 'asi',
- 'sb' => 'oce',
- 'sc' => 'afr',
- 'sd' => 'afr',
- 'se' => 'eur',
- 'sg' => 'asi',
- 'sh' => 'afr',
- 'si' => 'eur',
- 'sj' => 'eur',
- 'sk' => 'eur',
- 'sl' => 'afr',
- 'sm' => 'eur',
- 'sn' => 'afr',
- 'so' => 'afr',
- 'sr' => 'ams',
- 'ss' => 'afr',
- 'st' => 'afr',
- 'sv' => 'amc',
- 'sx' => 'amc',
- 'sy' => 'asi',
- 'sz' => 'afr',
- 'tc' => 'amc',
- 'td' => 'afr',
- 'tf' => 'ant',
- 'tg' => 'afr',
- 'th' => 'asi',
- 'ti' => 'asi',
- 'tj' => 'asi',
- 'tk' => 'oce',
- 'tl' => 'asi',
- 'tm' => 'asi',
- 'tn' => 'afr',
- 'to' => 'oce',
- 'tr' => 'eur',
- 'tt' => 'amc',
- 'tv' => 'oce',
- 'tw' => 'asi',
- 'tz' => 'afr',
- 'ua' => 'eur',
- 'ug' => 'afr',
- 'um' => 'oce',
- 'us' => 'amn',
- 'uy' => 'ams',
- 'uz' => 'asi',
- 'va' => 'eur',
- 'vc' => 'amc',
- 've' => 'ams',
- 'vg' => 'amc',
- 'vi' => 'amc',
- 'vn' => 'asi',
- 'vu' => 'oce',
- 'wf' => 'oce',
- 'ws' => 'oce',
- 'ye' => 'asi',
- 'yt' => 'afr',
- 'za' => 'afr',
- 'zm' => 'afr',
- 'zw' => 'afr',
- );
-
- // codes for internal use
- $GLOBALS['Piwik_CountryList_Extras'] = array(
- // unknown
- 'xx' => 'unk',
-
- // exceptionally reserved
- 'ac' => 'afr', // .ac TLD
- 'cp' => 'amc',
- 'dg' => 'asi',
- 'ea' => 'afr',
- 'eu' => 'eur', // .eu TLD
- 'fx' => 'eur',
- 'ic' => 'afr',
- 'su' => 'eur', // .su TLD
- 'ta' => 'afr',
- 'uk' => 'eur', // .uk TLD
-
- // transitionally reserved
- 'an' => 'amc', // former Netherlands Antilles
- 'bu' => 'asi',
- 'cs' => 'eur', // former Serbia and Montenegro
- 'nt' => 'asi',
- 'sf' => 'eur',
- 'tp' => 'oce', // .tp TLD
- 'yu' => 'eur', // .yu TLD
- 'zr' => 'afr',
-
- // MaxMind GeoIP specific
- 'a1' => 'unk',
- 'a2' => 'unk',
- 'ap' => 'asi',
- 'o1' => 'unk',
-
- // Catalonia (Spain)
- 'cat' => 'eur',
- );
-}
-
-if (!isset($GLOBALS['Piwik_ContinentList'])) {
- // Primary reference: ISO 3166-1 alpha-2
- $GLOBALS['Piwik_ContinentList'] = array(
- 'unk', // unknown
- 'amn', // North America
- 'amc', // Central America
- 'ams', // South America
- 'eur', // Europe
- 'afr', // Africa
- 'asi', // Asia
- 'oce', // Oceania
- 'ant', // Antarctica
- );
-}
diff --git a/core/DataFiles/Currencies.php b/core/DataFiles/Currencies.php
deleted file mode 100644
index 4ebdf810e9..0000000000
--- a/core/DataFiles/Currencies.php
+++ /dev/null
@@ -1,186 +0,0 @@
-<?php
-/**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
-
-/**
- * International currencies in circulation.
- *
- * @see http://en.wikipedia.org/wiki/List_of_circulating_currencies
- */
-if (!isset($GLOBALS['Piwik_CurrencyList'])) {
- $GLOBALS['Piwik_CurrencyList'] = array(
- // 'ISO-4217 CODE' => array('currency symbol', 'description'),
-
- // Top 5 by global trading volume
- 'USD' => array('$', 'US dollar'),
- 'EUR' => array('€', 'Euro'),
- 'JPY' => array('¥', 'Japanese yen'),
- 'GBP' => array('£', 'British pound'),
- 'CHF' => array('Fr', 'Swiss franc'),
-
- 'AFN' => array('؋', 'Afghan afghani'),
- 'ALL' => array('L', 'Albanian lek'),
- 'DZD' => array('د.ج', 'Algerian dinar'),
- 'AOA' => array('Kz', 'Angolan kwanza'),
- 'ARS' => array('$', 'Argentine peso'),
- 'AMD' => array('դր.', 'Armenian dram'),
- 'AWG' => array('ƒ', 'Aruban florin'),
- 'AUD' => array('$', 'Australian dollar'),
- 'AZN' => array('m', 'Azerbaijani manat'),
- 'BSD' => array('$', 'Bahamian dollar'),
- 'BHD' => array('.د.ب', 'Bahraini dinar'),
- 'BDT' => array('৳', 'Bangladeshi taka'),
- 'BBD' => array('$', 'Barbadian dollar'),
- 'BYR' => array('Br', 'Belarusian ruble'),
- 'BZD' => array('$', 'Belize dollar'),
- 'BMD' => array('$', 'Bermudian dollar'),
- 'BTC' => array('BTC', 'Bitcoin'),
- 'BTN' => array('Nu.', 'Bhutanese ngultrum'),
- 'BOB' => array('Bs.', 'Bolivian boliviano'),
- 'BAM' => array('KM', 'Bosnia Herzegovina mark'),
- 'BWP' => array('P', 'Botswana pula'),
- 'BRL' => array('R$', 'Brazilian real'),
-// 'GBP' => array('£', 'British pound'),
- 'BND' => array('$', 'Brunei dollar'),
- 'BGN' => array('лв', 'Bulgarian lev'),
- 'BIF' => array('Fr', 'Burundian franc'),
- 'KHR' => array('៛', 'Cambodian riel'),
- 'CAD' => array('$', 'Canadian dollar'),
- 'CVE' => array('$', 'Cape Verdean escudo'),
- 'KYD' => array('$', 'Cayman Islands dollar'),
- 'XAF' => array('Fr', 'Central African CFA franc'),
- 'CLP' => array('$', 'Chilean peso'),
- 'CNY' => array('元', 'Chinese yuan'),
- 'COP' => array('$', 'Colombian peso'),
- 'KMF' => array('Fr', 'Comorian franc'),
- 'CDF' => array('Fr', 'Congolese franc'),
- 'CRC' => array('₡', 'Costa Rican colón'),
- 'HRK' => array('kn', 'Croatian kuna'),
- 'XPF' => array('F', 'CFP franc'),
- 'CUC' => array('$', 'Cuban convertible peso'),
- 'CUP' => array('$', 'Cuban peso'),
- 'CMG' => array('ƒ', 'Curaçao and Sint Maarten guilder'),
- 'CZK' => array('Kč', 'Czech koruna'),
- 'DKK' => array('kr', 'Danish krone'),
- 'DJF' => array('Fr', 'Djiboutian franc'),
- 'DOP' => array('$', 'Dominican peso'),
- 'XCD' => array('$', 'East Caribbean dollar'),
- 'EGP' => array('ج.م', 'Egyptian pound'),
- 'ERN' => array('Nfk', 'Eritrean nakfa'),
- 'ETB' => array('Br', 'Ethiopian birr'),
-// 'EUR' => array('€', 'Euro'),
- 'FKP' => array('£', 'Falkland Islands pound'),
- 'FJD' => array('$', 'Fijian dollar'),
- 'GMD' => array('D', 'Gambian dalasi'),
- 'GEL' => array('ლ', 'Georgian lari'),
- 'GHS' => array('₵', 'Ghanaian cedi'),
- 'GIP' => array('£', 'Gibraltar pound'),
- 'GTQ' => array('Q', 'Guatemalan quetzal'),
- 'GNF' => array('Fr', 'Guinean franc'),
- 'GYD' => array('$', 'Guyanese dollar'),
- 'HTG' => array('G', 'Haitian gourde'),
- 'HNL' => array('L', 'Honduran lempira'),
- 'HKD' => array('$', 'Hong Kong dollar'),
- 'HUF' => array('Ft', 'Hungarian forint'),
- 'ISK' => array('kr', 'Icelandic króna'),
- 'INR' => array('‎₹', 'Indian rupee'),
- 'IDR' => array('Rp', 'Indonesian rupiah'),
- 'IRR' => array('﷼', 'Iranian rial'),
- 'IQD' => array('ع.د', 'Iraqi dinar'),
- 'ILS' => array('₪', 'Israeli new shekel'),
- 'JMD' => array('$', 'Jamaican dollar'),
-// 'JPY' => array('¥', 'Japanese yen'),
- 'JOD' => array('د.ا', 'Jordanian dinar'),
- 'KZT' => array('₸', 'Kazakhstani tenge'),
- 'KES' => array('Sh', 'Kenyan shilling'),
- 'KWD' => array('د.ك', 'Kuwaiti dinar'),
- 'KGS' => array('лв', 'Kyrgyzstani som'),
- 'LAK' => array('₭', 'Lao kip'),
- 'LBP' => array('ل.ل', 'Lebanese pound'),
- 'LSL' => array('L', 'Lesotho loti'),
- 'LRD' => array('$', 'Liberian dollar'),
- 'LYD' => array('ل.د', 'Libyan dinar'),
- 'LTL' => array('Lt', 'Lithuanian litas'),
- 'MOP' => array('P', 'Macanese pataca'),
- 'MKD' => array('ден', 'Macedonian denar'),
- 'MGA' => array('Ar', 'Malagasy ariary'),
- 'MWK' => array('MK', 'Malawian kwacha'),
- 'MYR' => array('RM', 'Malaysian ringgit'),
- 'MVR' => array('ރ.', 'Maldivian rufiyaa'),
- 'MRO' => array('UM', 'Mauritanian ouguiya'),
- 'MUR' => array('₨', 'Mauritian rupee'),
- 'MXN' => array('$', 'Mexican peso'),
- 'MDL' => array('L', 'Moldovan leu'),
- 'MNT' => array('₮', 'Mongolian tögrög'),
- 'MAD' => array('د.م.', 'Moroccan dirham'),
- 'MZN' => array('MTn', 'Mozambican metical'),
- 'MMK' => array('K', 'Myanma kyat'),
- 'NAD' => array('$', 'Namibian dollar'),
- 'NPR' => array('₨', 'Nepalese rupee'),
- 'ANG' => array('ƒ', 'Netherlands Antillean guilder'),
- 'TWD' => array('$', 'New Taiwan dollar'),
- 'NZD' => array('$', 'New Zealand dollar'),
- 'NIO' => array('C$', 'Nicaraguan córdoba'),
- 'NGN' => array('₦', 'Nigerian naira'),
- 'KPW' => array('₩', 'North Korean won'),
- 'NOK' => array('kr', 'Norwegian krone'),
- 'OMR' => array('ر.ع.', 'Omani rial'),
- 'PKR' => array('₨', 'Pakistani rupee'),
- 'PAB' => array('B/.', 'Panamanian balboa'),
- 'PGK' => array('K', 'Papua New Guinean kina'),
- 'PYG' => array('₲', 'Paraguayan guaraní'),
- 'PEN' => array('S/.', 'Peruvian nuevo sol'),
- 'PHP' => array('₱', 'Philippine peso'),
- 'PLN' => array('zł', 'Polish złoty'),
- 'QAR' => array('ر.ق', 'Qatari riyal'),
- 'RON' => array('L', 'Romanian leu'),
- 'RUB' => array('руб.', 'Russian ruble'),
- 'RWF' => array('Fr', 'Rwandan franc'),
- 'SHP' => array('£', 'Saint Helena pound'),
- 'SVC' => array('₡', 'Salvadoran colón'),
- 'WST' => array('T', 'Samoan tala'),
- 'STD' => array('Db', 'São Tomé and Príncipe dobra'),
- 'SAR' => array('ر.س', 'Saudi riyal'),
- 'RSD' => array('дин. or din.', 'Serbian dinar'),
- 'SCR' => array('₨', 'Seychellois rupee'),
- 'SLL' => array('Le', 'Sierra Leonean leone'),
- 'SGD' => array('$', 'Singapore dollar'),
- 'SBD' => array('$', 'Solomon Islands dollar'),
- 'SOS' => array('Sh', 'Somali shilling'),
- 'ZAR' => array('R', 'South African rand'),
- 'KRW' => array('₩', 'South Korean won'),
- 'LKR' => array('Rs', 'Sri Lankan rupee'),
- 'SDG' => array('جنيه سوداني', 'Sudanese pound'),
- 'SRD' => array('$', 'Surinamese dollar'),
- 'SZL' => array('L', 'Swazi lilangeni'),
- 'SEK' => array('kr', 'Swedish krona'),
-// 'CHF' => array('Fr', 'Swiss franc'),
- 'SYP' => array('ل.س', 'Syrian pound'),
- 'TJS' => array('ЅМ', 'Tajikistani somoni'),
- 'TZS' => array('Sh', 'Tanzanian shilling'),
- 'THB' => array('฿', 'Thai baht'),
- 'TOP' => array('T$', 'Tongan paʻanga'),
- 'TTD' => array('$', 'Trinidad and Tobago dollar'),
- 'TND' => array('د.ت', 'Tunisian dinar'),
- 'TRY' => array('TL', 'Turkish lira'),
- 'TMM' => array('m', 'Turkmenistani manat'),
- 'UGX' => array('Sh', 'Ugandan shilling'),
- 'UAH' => array('₴', 'Ukrainian hryvnia'),
- 'AED' => array('د.إ', 'United Arab Emirates dirham'),
-// 'USD' => array('$', 'United States dollar'),
- 'UYU' => array('$', 'Uruguayan peso'),
- 'UZS' => array('лв', 'Uzbekistani som'),
- 'VUV' => array('Vt', 'Vanuatu vatu'),
- 'VEF' => array('Bs F', 'Venezuelan bolívar'),
- 'VND' => array('₫', 'Vietnamese đồng'),
- 'XOF' => array('Fr', 'West African CFA franc'),
- 'YER' => array('﷼', 'Yemeni rial'),
- 'ZMW' => array('ZK', 'Zambian kwacha'),
- 'ZWL' => array('$', 'Zimbabwean dollar'),
- );
-}
diff --git a/core/DataFiles/LanguageToCountry.php b/core/DataFiles/LanguageToCountry.php
deleted file mode 100644
index fdfbb97e72..0000000000
--- a/core/DataFiles/LanguageToCountry.php
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-/**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
-
-/**
- * Language to Country mapping
- *
- * This list is used to guess the visitor's country when the region is
- * not specified in the first language tag. Inclusion/exclusion is
- * based on Piwik.org visitor statistics and probability of disambiguation.
- * (Notably, "en" and "zh" are excluded.)
- *
- * If you want to add a new entry, please email us at hello at piwik.org
- */
-if (!isset($GLOBALS['Piwik_LanguageToCountry'])) {
- $GLOBALS['Piwik_LanguageToCountry'] = array(
- 'bg' => 'bg', // Bulgarian => Bulgaria
- 'ca' => 'es', // Catalan => Spain
- 'cs' => 'cz', // Czech => Czech Republic
- 'da' => 'dk', // Danish => Denmark
- 'de' => 'de', // German => Germany
- 'el' => 'gr', // Greek => Greece
- 'es' => 'es', // Spanish => Spain
- 'et' => 'ee', // Estonian => Estonia
- 'fa' => 'ir', // Farsi => Iran
- 'fi' => 'fi', // Finnish => Finland
- 'fr' => 'fr', // French => France
- 'he' => 'il', // Hebrew => Israel
- 'hr' => 'hr', // Croatian => Croatia
- 'hu' => 'hu', // Hungarian => Hungary
- 'id' => 'id', // Indonesian => Indonesia
- 'is' => 'is', // Icelandic => Iceland
- 'it' => 'it', // Italian => Italy
- 'ja' => 'jp', // Japanese => Japan
- 'ko' => 'kr', // Korean => South Korea
- 'lt' => 'lt', // Lithuanian => Lithuania
- 'lv' => 'lv', // Latvian => Latvia
- 'mk' => 'mk', // Macedonian => Macedonia
- 'ms' => 'my', // Malay => Malaysia
- 'nb' => 'no', // Bokmål => Norway
- 'nl' => 'nl', // Dutch => Netherlands
- 'nn' => 'no', // Nynorsk => Norway
- 'no' => 'no', // Norwegian => Norway
- 'pl' => 'pl', // Polish => Poland
- 'pt' => 'pt', // Portugese => Portugal
- 'ro' => 'ro', // Romanian => Romania
- 'ru' => 'ru', // Russian => Russia
- 'sk' => 'sk', // Slovak => Slovakia
- 'sl' => 'si', // Slovene => Slovenia
- 'sq' => 'al', // Albanian => Albania
- 'sr' => 'rs', // Serbian => Serbia
- 'sv' => 'se', // Swedish => Sweden
- 'th' => 'th', // Thai => Thailand
- 'bo' => 'ti', // Tibetan => Tibet
- 'tr' => 'tr', // Turkish => Turkey
- 'uk' => 'ua', // Ukrainian => Ukraine
- );
-}
diff --git a/core/DataFiles/Languages.php b/core/DataFiles/Languages.php
deleted file mode 100644
index d41b443ff4..0000000000
--- a/core/DataFiles/Languages.php
+++ /dev/null
@@ -1,203 +0,0 @@
-<?php
-/**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
-
-/*
- * Language database
- */
-if (!isset($GLOBALS['Piwik_LanguageList'])) {
- // Reference: ISO 639-1 alpha-2
- $GLOBALS['Piwik_LanguageList'] = array(
- 'aa' => array('Afar'),
- 'ab' => array('Abkhazian'),
- 'ae' => array('Avestan'),
- 'af' => array('Afrikaans'),
- 'ak' => array('Akan'),
- 'am' => array('Amharic'),
- 'an' => array('Aragonese'),
- 'ar' => array('Arabic'),
- 'as' => array('Assamese'),
- 'av' => array('Avaric'),
- 'ay' => array('Aymara'),
- 'az' => array('Azerbaijani'),
- 'ba' => array('Bashkir'),
- 'be' => array('Belarusian'),
- 'bg' => array('Bulgarian'),
- 'bh' => array('Bihari'), // 'Bihari languages'
- 'bi' => array('Bislama'),
- 'bm' => array('Bambara'),
- 'bn' => array('Bengali'),
- 'bo' => array('Tibetan'),
- 'br' => array('Breton'),
- 'bs' => array('Bosnian'),
- 'ca' => array('Catalan', 'Valencian'),
- 'ce' => array('Chechen'),
- 'ch' => array('Chamorro'),
- 'co' => array('Corsican'),
- 'cr' => array('Cree'),
- 'cs' => array('Czech'),
- 'cu' => array('Church Slavic', 'Old Slavonic', 'Church Slavonic', 'Old Bulgarian', 'Old Church Slavonic'),
- 'cv' => array('Chuvash'),
- 'cy' => array('Welsh'),
- 'da' => array('Danish'),
- 'de' => array('German'),
- 'dv' => array('Divehi', 'Dhivehi', 'Maldivian'),
- 'dz' => array('Dzongkha'),
- 'ee' => array('Ewe'),
- 'el' => array('Greek', 'Modern Greek', 'Hellenic'), // Greek, Modern (1453-)
- 'en' => array('English'),
- 'eo' => array('Esperanto'),
- 'es' => array('Spanish', 'Castilian'),
- 'et' => array('Estonian'),
- 'eu' => array('Basque'),
- 'fa' => array('Persian'),
- 'ff' => array('Fulah'),
- 'fi' => array('Finnish'),
- 'fj' => array('Fijian'),
- 'fo' => array('Faroese'),
- 'fr' => array('French'),
- 'fy' => array('Western Frisian'),
- 'ga' => array('Irish'),
- 'gd' => array('Gaelic', 'Scottish Gaelic'),
- 'gl' => array('Galician'),
- 'gn' => array('Guarani'),
- 'gu' => array('Gujarati'),
- 'gv' => array('Manx'),
- 'ha' => array('Hausa'),
- 'he' => array('Hebrew'),
- 'hi' => array('Hindi'),
- 'ho' => array('Hiri Motu'),
- 'hr' => array('Croatian'),
- 'ht' => array('Haitian', 'Haitian Creole'),
- 'hu' => array('Hungarian'),
- 'hy' => array('Armenian'),
- 'hz' => array('Herero'),
- 'ia' => array('Interlingua'), // 'Interlingua (International Auxiliary Language Association)'
- 'id' => array('Indonesian'),
- 'ie' => array('Interlingue', 'Occidental'),
- 'ig' => array('Igbo'),
- 'ii' => array('Sichuan Yi', 'Nuosu'),
- 'ik' => array('Inupiaq'),
- 'io' => array('Ido'),
- 'is' => array('Icelandic'),
- 'it' => array('Italian'),
- 'iu' => array('Inuktitut'),
- 'ja' => array('Japanese'),
- 'jv' => array('Javanese'),
- 'ka' => array('Georgian'),
- 'kg' => array('Kongo'),
- 'ki' => array('Kikuyu', 'Gikuyu'),
- 'kj' => array('Kuanyama', 'Kwanyama'),
- 'kk' => array('Kazakh'),
- 'kl' => array('Kalaallisut', 'Greenlandic'),
- 'km' => array('Central Khmer'),
- 'kn' => array('Kannada'),
- 'ko' => array('Korean'),
- 'kr' => array('Kanuri'),
- 'ks' => array('Kashmiri'),
- 'ku' => array('Kurdish'),
- 'kv' => array('Komi'),
- 'kw' => array('Cornish'),
- 'ky' => array('Kirghiz', 'Kyrgyz'),
- 'la' => array('Latin'),
- 'lb' => array('Luxembourgish', 'Letzeburgesch'),
- 'lg' => array('Ganda'),
- 'li' => array('Limburgan', 'Limburger', 'Limburgish'),
- 'ln' => array('Lingala'),
- 'lo' => array('Lao'),
- 'lt' => array('Lithuanian'),
- 'lu' => array('Luba-Katanga'),
- 'lv' => array('Latvian'),
- 'mg' => array('Malagasy'),
- 'mh' => array('Marshallese'),
- 'mi' => array('Maori'),
- 'mk' => array('Macedonian'),
- 'ml' => array('Malayalam'),
- 'mn' => array('Mongolian'),
-// 'mo' => array('Moldavian'), // deprecated
- 'mr' => array('Marathi'),
- 'ms' => array('Malay'),
- 'mt' => array('Maltese'),
- 'my' => array('Burmese'),
- 'na' => array('Nauru'),
- 'nb' => array('Norwegian Bokmål'),
- 'nd' => array('North Ndebele'),
- 'ne' => array('Nepali'),
- 'ng' => array('Ndonga'),
- 'nl' => array('Dutch', 'Flemish'),
- 'nn' => array('Norwegian Nynorsk'),
- 'no' => array('Norwegian'),
- 'nr' => array('South Ndebele'),
- 'nv' => array('Navajo', 'Navaho'),
- 'ny' => array('Chichewa', 'Chewa', 'Nyanja'),
- 'oc' => array('Occitan', 'Provençal'), // Occitan (post 1500)
- 'oj' => array('Ojibwa'),
- 'om' => array('Oromo'),
- 'or' => array('Oriya'),
- 'os' => array('Ossetian', 'Ossetic'),
- 'pa' => array('Panjabi', 'Punjabi'),
- 'pi' => array('Pali'),
- 'pl' => array('Polish'),
- 'ps' => array('Pushto', 'Pashto'),
- 'pt' => array('Portuguese'),
- 'qu' => array('Quechua'),
- 'rm' => array('Romansh'),
- 'rn' => array('Rundi'),
- 'ro' => array('Romanian', 'Moldavian', 'Moldovan'),
- 'ru' => array('Russian'),
- 'rw' => array('Kinyarwanda'),
- 'sa' => array('Sanskrit'),
- 'sc' => array('Sardinian'),
- 'sd' => array('Sindhi'),
- 'se' => array('Northern Sami'),
- 'sg' => array('Sango'),
-// 'sh' => array('Serbo-Croatian'), // deprecated
- 'si' => array('Sinhala', 'Sinhalese'),
- 'sk' => array('Slovak'),
- 'sl' => array('Slovenian'),
- 'sm' => array('Samoan'),
- 'sn' => array('Shona'),
- 'so' => array('Somali'),
- 'sq' => array('Albanian'),
- 'sr' => array('Serbian'),
- 'ss' => array('Swati'),
- 'st' => array('Southern Soth'),
- 'su' => array('Sundanese'),
- 'sv' => array('Swedish'),
- 'sw' => array('Swahili'),
- 'ta' => array('Tamil'),
- 'te' => array('Telugu'),
- 'tg' => array('Tajik'),
- 'th' => array('Thai'),
- 'ti' => array('Tigrinya'),
- 'tk' => array('Turkmen'),
- 'tl' => array('Tagalog'),
- 'tn' => array('Tswana'),
- 'to' => array('Tonga'), // Tonga (Tonga Islands)
- 'tr' => array('Turkish'),
- 'ts' => array('Tsonga'),
- 'tt' => array('Tatar'),
- 'tw' => array('Twi'),
- 'ty' => array('Tahitian'),
- 'ug' => array('Uighur', 'Uyghur'),
- 'uk' => array('Ukrainian'),
- 'ur' => array('Urdu'),
- 'uz' => array('Uzbek'),
- 've' => array('Venda'),
- 'vi' => array('Vietnamese'),
- 'vo' => array('Volapük'),
- 'wa' => array('Walloon'),
- 'wo' => array('Wolof'),
- 'xh' => array('Xhosa'),
- 'yi' => array('Yiddish'),
- 'yo' => array('Yoruba'),
- 'za' => array('Zhuang', 'Chuang'),
- 'zh' => array('Chinese'),
- 'zu' => array('Zulu'),
- );
-}
diff --git a/core/DataFiles/SearchEngines.php b/core/DataFiles/SearchEngines.php
index c44836eb2c..084c00e5e1 100644
--- a/core/DataFiles/SearchEngines.php
+++ b/core/DataFiles/SearchEngines.php
@@ -189,6 +189,8 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
'searchqu.com' => array('Ask'),
'search.tb.ask.com' => array('Ask'),
'nortonsafe.search.ask.com' => array('Ask'),
+ 'safesearch.avira.com' => array('Ask'),
+ 'avira.search.ask.com' => array('Ask'),
// Atlas
'searchatlas.centrum.cz' => array('Atlas', 'q', '?q={k}'),
@@ -208,6 +210,8 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
// Baidu
'www.baidu.com' => array('Baidu', array('wd', 'word', 'kw'), 's?wd={k}', array('UTF-8', 'gb2312')),
'www1.baidu.com' => array('Baidu'),
+ 'm.baidu.com' => array('Baidu'),
+ 'www.baidu.co.th' => array('Baidu'),
'zhidao.baidu.com' => array('Baidu'),
'tieba.baidu.com' => array('Baidu'),
'news.baidu.com' => array('Baidu'),
@@ -281,6 +285,7 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
// DasOertliche
'www.dasoertliche.de' => array('DasOertliche', 'kw'),
+ 'www2.dasoertliche.de' => array('DasOertliche', array('ph', 'kw')),
// DasTelefonbuch
'www1.dastelefonbuch.de' => array('DasTelefonbuch', 'kw'),
@@ -451,6 +456,7 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
'suche.gmx.net' => array('Google', 'q', 'web?q={k}'),
'search.incredibar.com' => array('Google', 'q', 'search.php?q={k}'),
'www.delta-search.com' => array('Google', 'q'),
+ 'www1.delta-search.com' => array('Google', 'q'),
'search.1und1.de' => array('Google', 'q', 'web?q={k}'),
'search.zonealarm.com' => array('Google'),
'start.lenovo.com' => array('Google', 'q', 'search/index.php?q={k}'),
@@ -462,11 +468,8 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
'search.smt.docomo.ne.jp' => array('Google', 'MT'),
'image.search.smt.docomo.ne.jp' => array('Google', 'MT'),
'gfsoso.com' => array('Google', 'q'),
-
- // Google Earth
- // - 2010-09-13: are these redirects now?
- 'www.googleearth.de' => array('Google'),
- 'www.googleearth.fr' => array('Google'),
+ 'searches.safehomepage.com' => array('Google', 'q'),
+ 'searches.f-secure.com' => array('Google', 'query', 'search?query={k}'),
// Google Cache
'webcache.googleusercontent.com' => array('Google', '/\/search\?q=cache:[A-Za-z0-9]+:[^+]+([^&]+)/', 'search?q={k}'),
@@ -523,6 +526,9 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
// Gule Sider
'www.gulesider.no' => array('Gule Sider', 'q'),
+ // Haosou
+ 'www.haosou.com' => array('Haosou', 'q', 's?q={k}'),
+
// HighBeam
'www.highbeam.com' => array('HighBeam', 'q', 'Search.aspx?q={k}'),
@@ -614,6 +620,8 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
'eu.ixquick.com' => array('Ixquick'),
's8-eu.ixquick.com' => array('Ixquick'),
's1-eu.ixquick.de' => array('Ixquick'),
+ 's2-eu4.ixquick.com' => array('Ixquick'),
+ 's5-eu4.ixquick.com' => array('Ixquick'),
// Jyxo
'jyxo.1188.cz' => array('Jyxo', 'q', 's?q={k}'),
@@ -675,6 +683,7 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
// Metager
'meta.rrzn.uni-hannover.de' => array('Metager', 'eingabe', 'meta/cgi-bin/meta.ger1?eingabe={k}'),
'www.metager.de' => array('Metager'),
+ 'metager.de' => array('Metager'),
// Metager2
'metager2.de' => array('Metager2', 'q', 'search/index.php?q={k}'),
@@ -751,6 +760,9 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
// PeoplePC
'search.peoplepc.com' => array('PeoplePC', 'q', 'search?q={k}'),
+ // PeopleCheck
+ 'extern.peoplecheck.de' => array('PeopleCheck', 'q', 'link.php?q={k}'),
+
// Picsearch
'www.picsearch.com' => array('Picsearch', 'q', 'index.cgi?q={k}'),
@@ -772,6 +784,9 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
'www.qualigo.de' => array('Qualigo'),
'www.qualigo.nl' => array('Qualigo'),
+ // Qwant
+ 'www.qwant.com' => array('Qwant', 'q'),
+
// Rakuten
'websearch.rakuten.co.jp' => array('Rakuten', 'qt', 'WebIS?qt={k}'),
@@ -828,6 +843,13 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
// Skynet
'www.skynet.be' => array('Skynet', 'q', 'services/recherche/google?q={k}'),
+ // sm.cn
+ 'm.sm.cn' => array('sm.cn', 'q', 's?q={k}'),
+ 'm.sp.sm.cn' => array('sm.cn'),
+
+ // sm.de
+ 'www.sm.de' => array('sm.de', 'q', '?q={k}'),
+
// SmartAdressbar
'search.smartaddressbar.com' => array('SmartAddressbar', 's', '?s={k}'),
@@ -843,6 +865,7 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
// Sogou
'www.sogou.com' => array('Sogou', 'query', 'web?query={k}', 'gb2312'),
+ 'm.sogou.com' => array('Sogou', 'keyword'),
// Softonic
'search.softonic.com' => array('Softonic', 'q', 'default/default?q={k}'),
@@ -903,9 +926,15 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
// Toolbarhome
'www.toolbarhome.com' => array('Toolbarhome', 'q', 'search.aspx?q={k}'),
-
'vshare.toolbarhome.com' => array('Toolbarhome'),
+ // Toppreise.ch
+ 'www.toppreise.ch' => array('Toppreise.ch', 'search', 'index.php?search={k}', 'ISO-8859-1'),
+ 'toppreise.ch' => array('Toppreise.ch', null, null, 'ISO-8859-1'),
+ 'fr.toppreise.ch' => array('Toppreise.ch', null, null, 'ISO-8859-1'),
+ 'de.toppreise.ch' => array('Toppreise.ch', null, null, 'ISO-8859-1'),
+ 'en.toppreise.ch' => array('Toppreise.ch', null, null, 'ISO-8859-1'),
+
// Trouvez.com
'www.trouvez.com' => array('Trouvez.com', 'query'),
@@ -1017,7 +1046,7 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
// '{}.dir.yahoo.com' => array('Yahoo! Directory'),
// Yahoo! Images
- 'images.search.yahoo.com' => array('Yahoo! Images', 'p', 'search/images?p={k}'),
+ 'images.search.yahoo.com' => array('Yahoo! Images', array('p', 'va'), 'search/images?p={k}'),
// '*.images.search.yahoo.com'=> array('Yahoo! Images'), // see built-in helper in Common.php
'{}.images.yahoo.com' => array('Yahoo! Images'),
'cade.images.yahoo.com' => array('Yahoo! Images'),
@@ -1082,5 +1111,8 @@ if (!isset($GLOBALS['Piwik_SearchEngines'])) {
// Zoznam
'www.zoznam.sk' => array('Zoznam', 's', 'hladaj.fcgi?s={k}&co=svet'),
+
+ // Zxuso
+ 'www.zxuso.com' => array('Zxuso', 'wd', 'ri/?wd={k}'),
);
}
diff --git a/core/DataTable.php b/core/DataTable.php
index 61e8abb1eb..b060a87245 100644
--- a/core/DataTable.php
+++ b/core/DataTable.php
@@ -356,10 +356,11 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
if ($this->enableRecursiveSort === true) {
foreach ($this->getRows() as $row) {
- if (($idSubtable = $row->getIdSubDataTable()) !== null) {
- $table = Manager::getInstance()->getTable($idSubtable);
- $table->enableRecursiveSort();
- $table->sort($functionCallback, $columnSortedBy);
+
+ $subTable = $row->getSubtable();
+ if ($subTable) {
+ $subTable->enableRecursiveSort();
+ $subTable->sort($functionCallback, $columnSortedBy);
}
}
}
@@ -476,10 +477,9 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
* metadata can be used to specify a different type of operation.
*
* @param \Piwik\DataTable $tableToSum
- * @param bool $doAggregateSubTables
* @throws Exception
*/
- public function addDataTable(DataTable $tableToSum, $doAggregateSubTables = true)
+ public function addDataTable(DataTable $tableToSum)
{
if ($tableToSum instanceof Simple) {
if ($tableToSum->getRowsCount() > 1) {
@@ -489,7 +489,7 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
$this->aggregateRowFromSimpleTable($row);
} else {
foreach ($tableToSum->getRows() as $row) {
- $this->aggregateRowWithLabel($row, $doAggregateSubTables);
+ $this->aggregateRowWithLabel($row);
}
}
}
@@ -868,8 +868,8 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
{
$totalCount = 0;
foreach ($this->rows as $row) {
- if (($idSubTable = $row->getIdSubDataTable()) !== null) {
- $subTable = Manager::getInstance()->getTable($idSubTable);
+ $subTable = $row->getSubtable();
+ if ($subTable) {
$count = $subTable->getRowsCountRecursive();
$totalCount += $count;
}
@@ -901,15 +901,14 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
* @param string $oldName Old column name.
* @param string $newName New column name.
*/
- public function renameColumn($oldName, $newName, $doRenameColumnsOfSubTables = true)
+ public function renameColumn($oldName, $newName)
{
foreach ($this->getRows() as $row) {
$row->renameColumn($oldName, $newName);
- if ($doRenameColumnsOfSubTables) {
- if (($idSubDataTable = $row->getIdSubDataTable()) !== null) {
- Manager::getInstance()->getTable($idSubDataTable)->renameColumn($oldName, $newName);
- }
+ $subTable = $row->getSubtable();
+ if ($subTable) {
+ $subTable->renameColumn($oldName, $newName);
}
}
if (!is_null($this->summaryRow)) {
@@ -929,8 +928,9 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
foreach ($names as $name) {
$row->deleteColumn($name);
}
- if (($idSubDataTable = $row->getIdSubDataTable()) !== null) {
- Manager::getInstance()->getTable($idSubDataTable)->deleteColumns($names, $deleteRecursiveInSubtables);
+ $subTable = $row->getSubtable();
+ if ($subTable) {
+ $subTable->deleteColumns($names, $deleteRecursiveInSubtables);
}
}
if (!is_null($this->summaryRow)) {
@@ -1110,20 +1110,13 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
// but returns all serialized tables and subtable in an array of 1 dimension
$aSerializedDataTable = array();
foreach ($this->rows as $row) {
- if (($idSubTable = $row->getIdSubDataTable()) !== null) {
- $subTable = null;
- try {
- $subTable = Manager::getInstance()->getTable($idSubTable);
- } catch(TableNotFoundException $e) {
- // This occurs is an unknown & random data issue. Catch Exception and remove subtable from the row.
- $row->removeSubtable();
- // Go to next row
- continue;
- }
-
+ $subTable = $row->getSubtable();
+ if ($subTable) {
$depth++;
$aSerializedDataTable = $aSerializedDataTable + $subTable->getSerialized($maximumRowsInSubDataTable, $maximumRowsInSubDataTable, $columnToSortByBeforeTruncation);
$depth--;
+ } else {
+ $row->removeSubtable();
}
}
// we load the current Id of the DataTable
@@ -1595,7 +1588,7 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
* @param $row
* @throws \Exception
*/
- protected function aggregateRowWithLabel(Row $row, $doAggregateSubTables = true)
+ protected function aggregateRowWithLabel(Row $row)
{
$labelToLookFor = $row->getColumn('label');
if ($labelToLookFor === false) {
@@ -1611,17 +1604,15 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess
} else {
$rowFound->sumRow($row, $copyMeta = true, $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME));
- if ($doAggregateSubTables) {
- // if the row to add has a subtable whereas the current row doesn't
- // we simply add it (cloning the subtable)
- // if the row has the subtable already
- // then we have to recursively sum the subtables
- if (($idSubTable = $row->getIdSubDataTable()) !== null) {
- $subTable = Manager::getInstance()->getTable($idSubTable);
- $subTable->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME]
- = $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME);
- $rowFound->sumSubtable($subTable);
- }
+ // if the row to add has a subtable whereas the current row doesn't
+ // we simply add it (cloning the subtable)
+ // if the row has the subtable already
+ // then we have to recursively sum the subtables
+ $subTable = $row->getSubtable();
+ if ($subTable) {
+ $subTable->metadata[self::COLUMN_AGGREGATION_OPS_METADATA_NAME]
+ = $this->getMetadata(self::COLUMN_AGGREGATION_OPS_METADATA_NAME);
+ $rowFound->sumSubtable($subTable);
}
}
}
diff --git a/core/DataTable/BaseFilter.php b/core/DataTable/BaseFilter.php
index fb2dc009f9..dc4756d82e 100644
--- a/core/DataTable/BaseFilter.php
+++ b/core/DataTable/BaseFilter.php
@@ -73,8 +73,8 @@ abstract class BaseFilter
if (!$this->enableRecursive) {
return;
}
- if ($row->isSubtableLoaded()) {
- $subTable = Manager::getInstance()->getTable($row->getIdSubDataTable());
+ $subTable = $row->getSubtable();
+ if ($subTable) {
$this->filter($subTable);
}
}
diff --git a/core/DataTable/Filter/AddSegmentByLabel.php b/core/DataTable/Filter/AddSegmentByLabel.php
new file mode 100644
index 0000000000..2e9fd693d1
--- /dev/null
+++ b/core/DataTable/Filter/AddSegmentByLabel.php
@@ -0,0 +1,107 @@
+<?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\DataTable\Filter;
+
+use Piwik\DataTable;
+use Piwik\DataTable\BaseFilter;
+use Piwik\Development;
+
+/**
+ * Executes a filter for each row of a {@link DataTable} and generates a segment filter for each row.
+ *
+ * **Basic usage example**
+ *
+ * $dataTable->filter('AddSegmentByLabel', array('segmentName'));
+ * $dataTable->filter('AddSegmentByLabel', array(array('segmentName1', 'segment2'), ';');
+ *
+ * @api
+ */
+class AddSegmentByLabel extends BaseFilter
+{
+ private $segments;
+ private $delimiter;
+
+ /**
+ * Generates a segment filter based on the label column and the given segment names
+ *
+ * @param DataTable $table
+ * @param string|array $segmentOrSegments Either one segment or an array of segments.
+ * If more than one segment is given a delimter has to be defined.
+ * @param string $delimiter The delimiter by which the label should be splitted.
+ */
+ public function __construct($table, $segmentOrSegments, $delimiter = '')
+ {
+ parent::__construct($table);
+
+ if (!is_array($segmentOrSegments)) {
+ $segmentOrSegments = array($segmentOrSegments);
+ }
+
+ $this->segments = $segmentOrSegments;
+ $this->delimiter = $delimiter;
+ }
+
+ /**
+ * See {@link AddSegmentByLabel}.
+ *
+ * @param DataTable $table
+ */
+ public function filter($table)
+ {
+ if (empty($this->segments)) {
+ $msg = 'AddSegmentByLabel is called without having any segments defined';
+ Development::error($msg);
+ return;
+ }
+
+ if (count($this->segments) === 1) {
+ $segment = reset($this->segments);
+
+ foreach ($table->getRows() as $key => $row) {
+ if ($key == DataTable::ID_SUMMARY_ROW) {
+ continue;
+ }
+
+ $label = $row->getColumn('label');
+
+ if (!empty($label)) {
+ $row->setMetadata('segment', $segment . '==' . urlencode($label));
+ }
+ }
+ } else if (!empty($this->delimiter)) {
+ $numSegments = count($this->segments);
+ $conditionAnd = ';';
+
+ foreach ($table->getRows() as $key => $row) {
+ if ($key == DataTable::ID_SUMMARY_ROW) {
+ continue;
+ }
+
+ $label = $row->getColumn('label');
+ if (!empty($label)) {
+ $parts = explode($this->delimiter, $label);
+
+ if (count($parts) === $numSegments) {
+ $filter = array();
+ foreach ($this->segments as $index => $segment) {
+ if (!empty($segment)) {
+ $filter[] = $segment . '==' . urlencode($parts[$index]);
+ }
+ }
+ $row->setMetadata('segment', implode($conditionAnd, $filter));
+ }
+ }
+ }
+ } else {
+ $names = implode(', ', $this->segments);
+ $msg = 'Multiple segments are given but no delimiter defined. Segments: ' . $names;
+ Development::error($msg);
+ }
+ }
+}
diff --git a/core/DataTable/Filter/AddSegmentByLabelMapping.php b/core/DataTable/Filter/AddSegmentByLabelMapping.php
new file mode 100644
index 0000000000..29fbdceda8
--- /dev/null
+++ b/core/DataTable/Filter/AddSegmentByLabelMapping.php
@@ -0,0 +1,63 @@
+<?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\DataTable\Filter;
+
+use Piwik\DataTable;
+use Piwik\DataTable\BaseFilter;
+
+/**
+ * Executes a filter for each row of a {@link DataTable} and generates a segment filter for each row.
+ * It will map the label column to a segmentValue by searching for the label in the index of the given
+ * mapping array.
+ *
+ * **Basic usage example**
+ *
+ * $dataTable->filter('AddSegmentByLabelMapping', array('segmentName', array('1' => 'smartphone, '2' => 'desktop')));
+ *
+ * @api
+ */
+class AddSegmentByLabelMapping extends BaseFilter
+{
+ private $segment;
+ private $mapping;
+
+ /**
+ * @param DataTable $table
+ * @param string $segment
+ * @param array $mapping
+ */
+ public function __construct($table, $segment, $mapping)
+ {
+ parent::__construct($table);
+
+ $this->segment = $segment;
+ $this->mapping = $mapping;
+ }
+
+ /**
+ * See {@link AddSegmentByLabelMapping}.
+ *
+ * @param DataTable $table
+ */
+ public function filter($table)
+ {
+ if (empty($this->segment) || empty($this->mapping)) {
+ return;
+ }
+
+ foreach ($table->getRows() as $row) {
+ $label = $row->getColumn('label');
+
+ if (!empty($this->mapping[$label])) {
+ $label = $this->mapping[$label];
+ $row->setMetadata('segment', $this->segment . '==' . urlencode($label));
+ }
+ }
+ }
+}
diff --git a/core/DataTable/Filter/AddSegmentBySegmentValue.php b/core/DataTable/Filter/AddSegmentBySegmentValue.php
new file mode 100644
index 0000000000..7417a48c67
--- /dev/null
+++ b/core/DataTable/Filter/AddSegmentBySegmentValue.php
@@ -0,0 +1,78 @@
+<?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\DataTable\Filter;
+
+use Piwik\DataTable\BaseFilter;
+use Piwik\DataTable;
+
+/**
+ * Converts for each row of a {@link DataTable} a segmentValue to a segment (expression). The name of the segment
+ * is automatically detected based on the given report.
+ *
+ * **Basic usage example**
+ *
+ * $dataTable->filter('AddSegmentBySegmentValue', array($reportInstance));
+ *
+ * @api
+ */
+class AddSegmentBySegmentValue extends BaseFilter
+{
+ /**
+ * @var \Piwik\Plugin\Report
+ */
+ private $report;
+
+ /**
+ * @param DataTable $table
+ * @param $report
+ */
+ public function __construct($table, $report)
+ {
+ parent::__construct($table);
+ $this->report = $report;
+ }
+
+ /**
+ * See {@link AddSegmentBySegmentValue}.
+ *
+ * @param DataTable $table
+ * @return int The number of deleted rows.
+ */
+ public function filter($table)
+ {
+ if (empty($this->report) || !$table->getRowsCount()) {
+ return;
+ }
+
+ $dimension = $this->report->getDimension();
+
+ if (empty($dimension)) {
+ return;
+ }
+
+ $segments = $dimension->getSegments();
+
+ if (empty($segments)) {
+ return;
+ }
+
+ /** @var \Piwik\Plugin\Segment $segment */
+ $segment = reset($segments);
+ $segmentName = $segment->getSegment();
+
+ foreach ($table->getRows() as $row) {
+ $value = $row->getMetadata('segmentValue');
+ $filter = $row->getMetadata('segment');
+
+ if ($value !== false && $filter === false) {
+ $row->setMetadata('segment', sprintf('%s==%s', $segmentName, urlencode($value)));
+ }
+ }
+ }
+}
diff --git a/core/DataTable/Filter/AddSegmentValue.php b/core/DataTable/Filter/AddSegmentValue.php
new file mode 100644
index 0000000000..857bf02ce3
--- /dev/null
+++ b/core/DataTable/Filter/AddSegmentValue.php
@@ -0,0 +1,33 @@
+<?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\DataTable\Filter;
+
+use Piwik\DataTable;
+
+/**
+ * Executes a filter for each row of a {@link DataTable} and generates a segment filter for each row.
+ *
+ * **Basic usage example**
+ *
+ * $dataTable->filter('AddSegmentValue', array());
+ * $dataTable->filter('AddSegmentValue', array(function ($label) {
+ * $transformedValue = urldecode($transformedValue);
+ * return $transformedValue;
+ * });
+ *
+ * @api
+ */
+class AddSegmentValue extends ColumnCallbackAddMetadata
+{
+ public function __construct($table, $callback = null)
+ {
+ parent::__construct($table, 'label', 'segmentValue', $callback, null, false);
+ }
+
+}
diff --git a/core/DataTable/Filter/ColumnCallbackDeleteMetadata.php b/core/DataTable/Filter/ColumnCallbackDeleteMetadata.php
new file mode 100644
index 0000000000..bb8618a6cb
--- /dev/null
+++ b/core/DataTable/Filter/ColumnCallbackDeleteMetadata.php
@@ -0,0 +1,55 @@
+<?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\DataTable\Filter;
+
+use Piwik\DataTable;
+use Piwik\DataTable\BaseFilter;
+
+/**
+ * Executes a callback for each row of a {@link DataTable} and removes the defined metadata column from each row.
+ *
+ * **Basic usage example**
+ *
+ * $dataTable->filter('ColumnCallbackDeleteMetadata', array('segmentValue'));
+ *
+ * @api
+ */
+class ColumnCallbackDeleteMetadata extends BaseFilter
+{
+ private $metadataToRemove;
+
+ /**
+ * Constructor.
+ *
+ * @param DataTable $table The DataTable instance that will be filtered.
+ * @param string $metadataToRemove The name of the metadata field that will be removed from each row.
+ */
+ public function __construct($table, $metadataToRemove)
+ {
+ parent::__construct($table);
+
+ $this->metadataToRemove = $metadataToRemove;
+ }
+
+ /**
+ * See {@link ColumnCallbackDeleteMetadata}.
+ *
+ * @param DataTable $table
+ */
+ public function filter($table)
+ {
+ $this->enableRecursive(true);
+
+ foreach ($table->getRows() as $row) {
+ $row->deleteMetadata($this->metadataToRemove);
+
+ $this->filterSubTable($row);
+ }
+ }
+}
diff --git a/core/DataTable/Filter/ColumnCallbackReplace.php b/core/DataTable/Filter/ColumnCallbackReplace.php
index 3e167b5593..4d78831e88 100644
--- a/core/DataTable/Filter/ColumnCallbackReplace.php
+++ b/core/DataTable/Filter/ColumnCallbackReplace.php
@@ -81,6 +81,7 @@ class ColumnCallbackReplace extends BaseFilter
}
foreach ($this->columnsToFilter as $column) {
+
// when a value is not defined, we set it to zero by default (rather than displaying '-')
$value = $this->getElementToReplace($row, $column);
if ($value === false) {
diff --git a/core/DataTable/Filter/PatternRecursive.php b/core/DataTable/Filter/PatternRecursive.php
index 697403c2e3..f383a13260 100644
--- a/core/DataTable/Filter/PatternRecursive.php
+++ b/core/DataTable/Filter/PatternRecursive.php
@@ -62,18 +62,15 @@ class PatternRecursive extends BaseFilter
// AND 2 - the label is not found in the children
$patternNotFoundInChildren = false;
- try {
- $idSubTable = $row->getIdSubDataTable();
- $subTable = Manager::getInstance()->getTable($idSubTable);
-
+ $subTable = $row->getSubtable();
+ if(!$subTable) {
+ $patternNotFoundInChildren = true;
+ } else {
// we delete the row if we couldn't find the pattern in any row in the
// children hierarchy
if ($this->filter($subTable) == 0) {
$patternNotFoundInChildren = true;
}
- } catch (Exception $e) {
- // there is no subtable loaded for example
- $patternNotFoundInChildren = true;
}
if ($patternNotFoundInChildren
diff --git a/core/DataTable/Filter/PrependSegment.php b/core/DataTable/Filter/PrependSegment.php
new file mode 100644
index 0000000000..34d14e6f0a
--- /dev/null
+++ b/core/DataTable/Filter/PrependSegment.php
@@ -0,0 +1,34 @@
+<?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\DataTable\Filter;
+
+use Piwik\DataTable;
+
+/**
+ * Executes a callback for each row of a {@link DataTable} and prepends each existing segment with the
+ * given segment.
+ *
+ * **Basic usage example**
+ *
+ * $dataTable->filter('PrependSegment', array('segmentName==segmentValue;'));
+ *
+ * @api
+ */
+class PrependSegment extends PrependValueToMetadata
+{
+ /**
+ * @param DataTable $table
+ * @param string $prependSegment The segment to prepend if a segment is already defined. Make sure to include
+ * A condition, eg the segment should end with ';' or ','
+ */
+ public function __construct($table, $prependSegment = '')
+ {
+ parent::__construct($table, 'segment', $prependSegment);
+ }
+}
diff --git a/core/DataTable/Filter/PrependValueToMetadata.php b/core/DataTable/Filter/PrependValueToMetadata.php
new file mode 100644
index 0000000000..3e2e0ceb82
--- /dev/null
+++ b/core/DataTable/Filter/PrependValueToMetadata.php
@@ -0,0 +1,65 @@
+<?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\DataTable\Filter;
+
+use Piwik\DataTable;
+use Piwik\DataTable\BaseFilter;
+
+/**
+ * Executes a callback for each row of a {@link DataTable} and prepends the given value to each metadata entry
+ * but only if the given metadata entry exists.
+ *
+ * **Basic usage example**
+ *
+ * $dataTable->filter('PrependValueToMetadata', array('segment', 'segmentName==segmentValue'));
+ *
+ * @api
+ */
+class PrependValueToMetadata extends BaseFilter
+{
+ private $metadataColumn;
+ private $valueToPrepend;
+
+ /**
+ * @param DataTable $table
+ * @param string $metadataName The name of the metadata that should be prepended
+ * @param string $valueToPrepend The value to prepend if the metadata entry exists
+ */
+ public function __construct($table, $metadataName, $valueToPrepend)
+ {
+ parent::__construct($table);
+
+ $this->metadataColumn = $metadataName;
+ $this->valueToPrepend = $valueToPrepend;
+ }
+
+ /**
+ * See {@link PrependValueToMetadata}.
+ *
+ * @param DataTable $table
+ */
+ public function filter($table)
+ {
+ if (empty($this->metadataColumn) || empty($this->valueToPrepend)) {
+ return;
+ }
+
+ $metadataColumn = $this->metadataColumn;
+ $valueToPrepend = $this->valueToPrepend;
+
+ $table->filter(function (DataTable $dataTable) use ($metadataColumn, $valueToPrepend) {
+ foreach ($dataTable->getRows() as $row) {
+ $filter = $row->getMetadata($metadataColumn);
+ if ($filter !== false) {
+ $row->setMetadata($metadataColumn, $valueToPrepend . $filter);
+ }
+ }
+ });
+ }
+}
diff --git a/core/DataTable/Filter/ReplaceSummaryRowLabel.php b/core/DataTable/Filter/ReplaceSummaryRowLabel.php
index 3c1e31e2d0..1e550f6e3f 100644
--- a/core/DataTable/Filter/ReplaceSummaryRowLabel.php
+++ b/core/DataTable/Filter/ReplaceSummaryRowLabel.php
@@ -65,8 +65,8 @@ class ReplaceSummaryRowLabel extends BaseFilter
// recurse
foreach ($rows as $row) {
- if ($row->isSubtableLoaded()) {
- $subTable = Manager::getInstance()->getTable($row->getIdSubDataTable());
+ $subTable = $row->getSubtable();
+ if ($subTable) {
$this->filter($subTable);
}
}
diff --git a/core/DataTable/Filter/Sort.php b/core/DataTable/Filter/Sort.php
index 6ffe33bced..42441c8e30 100644
--- a/core/DataTable/Filter/Sort.php
+++ b/core/DataTable/Filter/Sort.php
@@ -67,22 +67,14 @@ class Sort extends BaseFilter
/**
* Sorting method used for sorting numbers
*
- * @param number $a
- * @param number $b
+ * @param Row $a
+ * @param Row $b
* @return int
*/
public function numberSort($a, $b)
{
- $valA = $a->getColumn($this->columnToSort);
- $valB = $b->getColumn($this->columnToSort);
-
- if ($valA === false) {
- $valA = null;
- }
-
- if ($valB === false) {
- $valB = null;
- }
+ $valA = $this->getColumnValue($a);
+ $valB = $this->getColumnValue($b);
return !isset($valA)
&& !isset($valB)
@@ -118,16 +110,8 @@ class Sort extends BaseFilter
*/
function naturalSort($a, $b)
{
- $valA = $a->getColumn($this->columnToSort);
- $valB = $b->getColumn($this->columnToSort);
-
- if ($valA === false) {
- $valA = null;
- }
-
- if ($valB === false) {
- $valB = null;
- }
+ $valA = $this->getColumnValue($a);
+ $valB = $this->getColumnValue($b);
return !isset($valA)
&& !isset($valB)
@@ -153,16 +137,8 @@ class Sort extends BaseFilter
*/
function sortString($a, $b)
{
- $valA = $a->getColumn($this->columnToSort);
- $valB = $b->getColumn($this->columnToSort);
-
- if ($valA === false) {
- $valA = null;
- }
-
- if ($valB === false) {
- $valB = null;
- }
+ $valA = $this->getColumnValue($a);
+ $valB = $this->getColumnValue($b);
return !isset($valA)
&& !isset($valB)
@@ -179,6 +155,18 @@ class Sort extends BaseFilter
);
}
+ protected function getColumnValue(Row $table )
+ {
+ $value = $table->getColumn($this->columnToSort);
+
+ if ($value === false
+ || is_array($value)
+ ) {
+ return null;
+ }
+ return $value;
+ }
+
/**
* Sets the column to be used for sorting
*
diff --git a/core/DataTable/Filter/Truncate.php b/core/DataTable/Filter/Truncate.php
index df0cf7509e..632eb2755e 100644
--- a/core/DataTable/Filter/Truncate.php
+++ b/core/DataTable/Filter/Truncate.php
@@ -85,6 +85,9 @@ class Truncate extends BaseFilter
}
}
+ /**
+ * @param DataTable $table
+ */
private function addSummaryRow($table)
{
$table->filter('Sort', array($this->columnToSortByBeforeTruncating, 'desc'));
diff --git a/core/DataTable/Manager.php b/core/DataTable/Manager.php
index d225b8fb87..07e7bc01b2 100644
--- a/core/DataTable/Manager.php
+++ b/core/DataTable/Manager.php
@@ -61,7 +61,7 @@ class Manager extends Singleton
public function getTable($idTable)
{
if (!isset($this->tables[$idTable])) {
- throw new TableNotFoundException(sprintf("This report has been reprocessed since your last click. To see this error less often, please increase the timeout value in seconds in Settings > General Settings. (error: id %s not found).", $idTable));
+ throw new TableNotFoundException(sprintf("Error: table id %s not found in memory. (If this error is causing you problems in production, please report it in Piwik issue tracker.)", $idTable));
}
return $this->tables[$idTable];
diff --git a/core/DataTable/Renderer/Console.php b/core/DataTable/Renderer/Console.php
index 0e1c127fb1..a4fcbff0c5 100644
--- a/core/DataTable/Renderer/Console.php
+++ b/core/DataTable/Renderer/Console.php
@@ -120,14 +120,10 @@ class Console extends Renderer
. $row->getIdSubDataTable() . "]<br />\n";
if (!is_null($row->getIdSubDataTable())) {
- if ($row->isSubtableLoaded()) {
+ $subTable = $row->getSubtable();
+ if ($subTable) {
$depth++;
- $output .= $this->renderTable(
- Manager::getInstance()->getTable(
- $row->getIdSubDataTable()
- ),
- $prefix . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'
- );
+ $output .= $this->renderTable($subTable, $prefix . '&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;');
$depth--;
} else {
$output .= "-- Sub DataTable not loaded<br />\n";
diff --git a/core/DataTable/Renderer/Json.php b/core/DataTable/Renderer/Json.php
index f53ea60e67..b5d2ebf979 100644
--- a/core/DataTable/Renderer/Json.php
+++ b/core/DataTable/Renderer/Json.php
@@ -71,7 +71,8 @@ class Json extends Renderer
};
array_walk_recursive($array, $callback);
- $str = json_encode($array);
+ // silence "Warning: json_encode(): Invalid UTF-8 sequence in argument"
+ $str = @json_encode($array);
return $str;
}
diff --git a/core/DataTable/Renderer/Php.php b/core/DataTable/Renderer/Php.php
index 56360b939c..eb6d32a3e3 100644
--- a/core/DataTable/Renderer/Php.php
+++ b/core/DataTable/Renderer/Php.php
@@ -206,10 +206,11 @@ class Php extends Renderer
$newRow['issummaryrow'] = true;
}
+ $subTable = $row->getSubtable();
if ($this->isRenderSubtables()
- && $row->isSubtableLoaded()
+ && $subTable
) {
- $subTable = $this->renderTable(Manager::getInstance()->getTable($row->getIdSubDataTable()));
+ $subTable = $this->renderTable($subTable);
$newRow['subtable'] = $subTable;
if ($this->hideIdSubDatatable === false
&& isset($newRow['metadata']['idsubdatatable_in_db'])
diff --git a/core/DataTable/Row.php b/core/DataTable/Row.php
index c6c0d21e9f..5f21f3fdec 100644
--- a/core/DataTable/Row.php
+++ b/core/DataTable/Row.php
@@ -224,11 +224,15 @@ class Row implements \ArrayAccess, \IteratorAggregate
private function isColumnValueCallable($name)
{
+ if (! is_callable($name)) {
+ return false;
+ }
+
if (is_object($name) && ($name instanceof \Closure)) {
return true;
}
- return is_array($name) && array_key_exists(0, $name) && is_object($name[0]) && is_callable($name);
+ return is_array($name) && isset($name[0]) && is_object($name[0]);
}
private function resolveCallableColumn($columnName)
@@ -317,7 +321,11 @@ class Row implements \ArrayAccess, \IteratorAggregate
public function getSubtable()
{
if ($this->isSubtableLoaded()) {
- return Manager::getInstance()->getTable($this->getIdSubDataTable());
+ try {
+ return Manager::getInstance()->getTable($this->getIdSubDataTable());
+ } catch(TableNotFoundException $e) {
+ // edge case
+ }
}
return false;
}
diff --git a/core/DataTable/Row/DataTableSummaryRow.php b/core/DataTable/Row/DataTableSummaryRow.php
index 7d477a304c..2c9eda5e8e 100644
--- a/core/DataTable/Row/DataTableSummaryRow.php
+++ b/core/DataTable/Row/DataTableSummaryRow.php
@@ -47,9 +47,8 @@ class DataTableSummaryRow extends Row
*/
public function recalculate()
{
- $id = $this->getIdSubDataTable();
- if ($id !== null) {
- $subTable = Manager::getInstance()->getTable($id);
+ $subTable = $this->getSubtable();
+ if ($subTable) {
$this->sumTable($subTable);
}
}
diff --git a/core/Date.php b/core/Date.php
index 5b8e0fab6e..a046f4e27a 100644
--- a/core/Date.php
+++ b/core/Date.php
@@ -10,6 +10,7 @@
namespace Piwik;
use Exception;
+use Piwik\Container\StaticContainer;
/**
* Utility class that wraps date/time related PHP functions. Using this class can
@@ -104,7 +105,6 @@ class Date
*/
public static function factory($dateString, $timezone = null)
{
- $invalidDateException = new Exception(Piwik::translate('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "http://php.net/strtotime")) . ": $dateString");
if ($dateString instanceof self) {
$dateString = $dateString->toString();
}
@@ -125,7 +125,7 @@ class Date
($dateString = strtotime($dateString)) === false
)
) {
- throw $invalidDateException;
+ throw self::getInvalidDateFormatException($dateString);
} else {
$date = new Date($dateString);
}
@@ -133,7 +133,7 @@ class Date
// can't be doing web analytics before the 1st website
// Tue, 06 Aug 1991 00:00:00 GMT
if ($timestamp < 681436800) {
- throw $invalidDateException;
+ throw self::getInvalidDateFormatException($dateString);
}
if (empty($timezone)) {
return $date;
@@ -154,6 +154,19 @@ class Date
}
/**
+ * Returns the current hour in UTC timezone.
+ * @return string
+ * @throws Exception
+ */
+ public function getHourUTC()
+ {
+ $dateTime = $this->getDatetime();
+ $hourInTz = Date::factory($dateTime, 'UTC')->toString('G');
+
+ return $hourInTz;
+ }
+
+ /**
* 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'`.
@@ -243,6 +256,17 @@ class Date
}
/**
+ * Returns the date in the "Y-m-d H:i:s" PHP format
+ *
+ * @param int $timestamp
+ * @return string
+ */
+ public static function getDatetimeFromTimestamp($timestamp)
+ {
+ return date("Y-m-d H:i:s", $timestamp);
+ }
+
+ /**
* Returns the Unix timestamp of the date in UTC.
*
* @return int
@@ -598,15 +622,16 @@ class Date
*/
public function getLocalized($template)
{
+ $translator = StaticContainer::get('Piwik\Translation\Translator');
$day = $this->toString('j');
$dayOfWeek = $this->toString('N');
$monthOfYear = $this->toString('n');
$patternToValue = array(
"%day%" => $day,
- "%shortMonth%" => Piwik::translate('General_ShortMonth_' . $monthOfYear),
- "%longMonth%" => Piwik::translate('General_LongMonth_' . $monthOfYear),
- "%shortDay%" => Piwik::translate('General_ShortDay_' . $dayOfWeek),
- "%longDay%" => Piwik::translate('General_LongDay_' . $dayOfWeek),
+ "%shortMonth%" => $translator->translate('General_ShortMonth_' . $monthOfYear),
+ "%longMonth%" => $translator->translate('General_LongMonth_' . $monthOfYear),
+ "%shortDay%" => $translator->translate('General_ShortDay_' . $dayOfWeek),
+ "%longDay%" => $translator->translate('General_LongDay_' . $dayOfWeek),
"%longYear%" => $this->toString('Y'),
"%shortYear%" => $this->toString('y'),
"%time%" => $this->toString('H:i:s')
@@ -751,4 +776,10 @@ class Date
{
return $secs / self::NUM_SECONDS_IN_DAY;
}
+
+ private static function getInvalidDateFormatException($dateString)
+ {
+ $message = Piwik::translate('General_ExceptionInvalidDateFormat', array("YYYY-MM-DD, or 'today' or 'yesterday'", "strtotime", "http://php.net/strtotime"));
+ return new Exception($message . ": $dateString");
+ }
}
diff --git a/core/Db.php b/core/Db.php
index fa26f34f02..a96786e1e3 100644
--- a/core/Db.php
+++ b/core/Db.php
@@ -9,6 +9,7 @@
namespace Piwik;
use Exception;
+use Piwik\DataAccess\TableMetadata;
use Piwik\Db\Adapter;
use Piwik\Tracker;
@@ -35,6 +36,8 @@ class Db
{
private static $connection = null;
+ private static $logQueries = true;
+
/**
* Returns the database connection and creates it if it hasn't been already.
*
@@ -385,17 +388,12 @@ class Db
*
* @param string|array $table The name of the table you want to get the columns definition for.
* @return \Zend_Db_Statement
+ * @deprecated since 2.11.0
*/
public static function getColumnNamesFromTable($table)
{
- $columns = self::fetchAll("SHOW COLUMNS FROM `" . $table . "`");
-
- $columnNames = array();
- foreach ($columns as $column) {
- $columnNames[] = $column['Field'];
- }
-
- return $columnNames;
+ $tableMetadataAccess = new TableMetadata();
+ return $tableMetadataAccess->getColumns($table);
}
/**
@@ -710,7 +708,27 @@ class Db
private static function logSql($functionName, $sql, $parameters = array())
{
- // NOTE: at the moment we dont log bind in order to avoid sensitive information leaks
- Log::verbose("Db::%s() executing SQL:\n%s", $functionName, $sql);
+ if (self::$logQueries === false) {
+ return;
+ }
+
+ // NOTE: at the moment we don't log parameters in order to avoid sensitive information leaks
+ Log::debug("Db::%s() executing SQL: %s", $functionName, $sql);
+ }
+
+ /**
+ * @param bool $enable
+ */
+ public static function enableQueryLog($enable)
+ {
+ self::$logQueries = $enable;
+ }
+
+ /**
+ * @return boolean
+ */
+ public static function isQueryLogEnabled()
+ {
+ return self::$logQueries;
}
}
diff --git a/core/Db/Adapter.php b/core/Db/Adapter.php
index 5ddd529caa..b75690bc08 100644
--- a/core/Db/Adapter.php
+++ b/core/Db/Adapter.php
@@ -38,6 +38,7 @@ class Adapter
}
$className = self::getAdapterClassName($adapterName);
+
$adapter = new $className($dbInfos);
if ($connect) {
@@ -56,10 +57,15 @@ class Adapter
*
* @param string $adapterName
* @return string
+ * @throws \Exception
*/
private static function getAdapterClassName($adapterName)
{
- return 'Piwik\Db\Adapter\\' . str_replace(' ', '\\', ucwords(str_replace(array('_', '\\'), ' ', strtolower($adapterName))));
+ $className = 'Piwik\Db\Adapter\\' . str_replace(' ', '\\', ucwords(str_replace(array('_', '\\'), ' ', strtolower($adapterName))));
+ if(!class_exists($className)) {
+ throw new \Exception("Adapter $adapterName is not valid.");
+ }
+ return $className;
}
/**
diff --git a/core/Db/BatchInsert.php b/core/Db/BatchInsert.php
index bb620d692e..2fbd6674ad 100644
--- a/core/Db/BatchInsert.php
+++ b/core/Db/BatchInsert.php
@@ -57,7 +57,7 @@ class BatchInsert
*/
public static function tableInsertBatch($tableName, $fields, $values, $throwException = false)
{
- $filePath = StaticContainer::getContainer()->get('path.tmp') . '/assets/' . $tableName . '-' . Common::generateUniqId() . '.csv';
+ $filePath = StaticContainer::get('path.tmp') . '/assets/' . $tableName . '-' . Common::generateUniqId() . '.csv';
$loadDataInfileEnabled = Config::getInstance()->General['enable_load_data_infile'];
@@ -92,7 +92,6 @@ class BatchInsert
return true;
}
} catch (Exception $e) {
- Log::info("LOAD DATA INFILE failed or not supported, falling back to normal INSERTs... Error was: %s", $e->getMessage());
if ($throwException) {
throw $e;
@@ -189,18 +188,16 @@ class BatchInsert
return true;
} catch (Exception $e) {
-// echo $sql . ' ---- ' . $e->getMessage();
$code = $e->getCode();
$message = $e->getMessage() . ($code ? "[$code]" : '');
- if (!Db::get()->isErrNo($e, '1148')) {
- Log::info("LOAD DATA INFILE failed... Error was: %s", $message);
- }
$exceptions[] = "\n Try #" . (count($exceptions) + 1) . ': ' . $queryStart . ": " . $message;
}
}
if (count($exceptions)) {
- throw new Exception(implode(",", $exceptions));
+ $message = "LOAD DATA INFILE failed... Error was: " . implode(",", $exceptions);
+ Log::info($message);
+ throw new Exception($message);
}
return false;
diff --git a/core/DeviceDetectorCache.php b/core/DeviceDetectorCache.php
index ba65b82c9f..7cf2a15226 100644
--- a/core/DeviceDetectorCache.php
+++ b/core/DeviceDetectorCache.php
@@ -8,6 +8,7 @@
*/
namespace Piwik;
+use Piwik\Cache as PiwikCache;
use Exception;
/**
@@ -17,29 +18,40 @@ use Exception;
*
* Static caching speeds up multiple detections in one request, which is the case when sending bulk requests
*/
-class DeviceDetectorCache extends CacheFile implements \DeviceDetector\Cache\CacheInterface
+class DeviceDetectorCache implements \DeviceDetector\Cache\Cache
{
protected static $staticCache = array();
+ private $cache;
+ private $ttl;
+
+ public function __construct($ttl = 300)
+ {
+ $this->ttl = (int) $ttl;
+ $this->cache = PiwikCache::getEagerCache();
+ }
+
/**
* Function to fetch a cache entry
*
* @param string $id The cache entry ID
* @return array|bool False on error, or array the cache content
*/
- public function get($id)
+ public function fetch($id)
{
if (empty($id)) {
return false;
}
- $id = $this->cleanupId($id);
-
if (array_key_exists($id, self::$staticCache)) {
return self::$staticCache[$id];
}
- return parent::get($id);
+ if (!$this->cache->contains($id)) {
+ return false;
+ }
+
+ return $this->cache->fetch($id);
}
/**
@@ -50,16 +62,36 @@ class DeviceDetectorCache extends CacheFile implements \DeviceDetector\Cache\Cac
* @throws \Exception
* @return bool True if the entry was succesfully stored
*/
- public function set($id, $content)
+ public function save($id, $content, $ttl=0)
{
if (empty($id)) {
return false;
}
- $id = $this->cleanupId($id);
-
self::$staticCache[$id] = $content;
- return parent::set($id, $content);
+ return $this->cache->save($id, $content, $this->ttl);
+ }
+
+ public function contains($id)
+ {
+ return !empty(self::$staticCache[$id]) && $this->cache->contains($id);
+ }
+
+ public function delete($id)
+ {
+ if (empty($id)) {
+ return false;
+ }
+
+ unset(self::$staticCache[$id]);
+
+ return $this->cache->delete($id);
+ }
+
+ public function flushAll()
+ {
+ return $this->cache->flushAll();
}
+
}
diff --git a/core/DeviceDetectorFactory.php b/core/DeviceDetectorFactory.php
index c61da87a14..c7962a96f0 100644
--- a/core/DeviceDetectorFactory.php
+++ b/core/DeviceDetectorFactory.php
@@ -28,7 +28,7 @@ class DeviceDetectorFactory
$deviceDetector = new DeviceDetector($userAgent);
$deviceDetector->discardBotInformation();
- $deviceDetector->setCache(new DeviceDetectorCache('tracker', 86400));
+ $deviceDetector->setCache(new DeviceDetectorCache(86400));
$deviceDetector->parse();
self::$deviceDetectorInstances[$userAgent] = $deviceDetector;
diff --git a/core/Error.php b/core/Error.php
deleted file mode 100644
index c56e3301a7..0000000000
--- a/core/Error.php
+++ /dev/null
@@ -1,226 +0,0 @@
-<?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;
-
-require_once PIWIK_INCLUDE_PATH . '/core/Log.php';
-
-/**
- * Holds PHP error information (non-exception errors). Also contains log formatting logic
- * for PHP errors and Piwik's error handler function.
- */
-class Error
-{
- /**
- * The backtrace string to use when testing.
- *
- * @var string
- */
- public static $debugBacktraceForTests = null;
-
- /**
- * The error number. See http://php.net/manual/en/errorfunc.constants.php#errorfunc.constants.errorlevels
- *
- * @var int
- */
- public $errno;
-
- /**
- * The error message.
- *
- * @var string
- */
- public $errstr;
-
- /**
- * The file in which the error occurred.
- *
- * @var string
- */
- public $errfile;
-
- /**
- * The line number on which the error occurred.
- *
- * @var int
- */
- public $errline;
-
- /**
- * The error backtrace.
- *
- * @var string
- */
- public $backtrace;
-
- /**
- * Constructor.
- *
- * @param int $errno
- * @param string $errstr
- * @param string $errfile
- * @param int $errline
- * @param string $backtrace
- */
- public function __construct($errno, $errstr, $errfile, $errline, $backtrace)
- {
- $this->errno = $errno;
- $this->errstr = $errstr;
- $this->errfile = $errfile;
- $this->errline = $errline;
- $this->backtrace = $backtrace;
- }
-
- /**
- * Returns a string description of a PHP error number.
- *
- * @param int $errno `E_ERROR`, `E_WARNING`, `E_PARSE`, etc.
- * @return string
- */
- public static function getErrNoString($errno)
- {
- switch ($errno) {
- case E_ERROR:
- return "Error";
- case E_WARNING:
- return "Warning";
- case E_PARSE:
- return "Parse Error";
- case E_NOTICE:
- return "Notice";
- case E_CORE_ERROR:
- return "Core Error";
- case E_CORE_WARNING:
- return "Core Warning";
- case E_COMPILE_ERROR:
- return "Compile Error";
- case E_COMPILE_WARNING:
- return "Compile Warning";
- case E_USER_ERROR:
- return "User Error";
- case E_USER_WARNING:
- return "User Warning";
- case E_USER_NOTICE:
- return "User Notice";
- case E_STRICT:
- return "Strict Notice";
- case E_RECOVERABLE_ERROR:
- return "Recoverable Error";
- case E_DEPRECATED:
- return "Deprecated";
- case E_USER_DEPRECATED:
- return "User Deprecated";
- default:
- return "Unknown error ($errno)";
- }
- }
-
- public static function formatFileAndDBLogMessage(&$message, $level, $tag, $datetime, $log)
- {
- if ($message instanceof Error) {
- $message = $message->errfile . '(' . $message->errline . '): ' . Error::getErrNoString($message->errno)
- . ' - ' . $message->errstr . "\n" . $message->backtrace;
-
- $message = $log->formatMessage($level, $tag, $datetime, $message);
- }
- }
-
- public static function formatScreenMessage(&$message, $level, $tag, $datetime, $log)
- {
- if ($message instanceof Error) {
- $errno = $message->errno & error_reporting();
-
- // problem when using error_reporting with the @ silent fail operator
- // it gives an errno 0, and in this case the objective is to NOT display anything on the screen!
- // is there any other case where the errno is zero at this point?
- if ($errno == 0) {
- $message = false;
- return;
- }
-
- Common::sendHeader('Content-Type: text/html; charset=utf-8');
-
- $htmlString = '';
- $htmlString .= "\n<div style='word-wrap: break-word; border: 3px solid red; padding:4px; width:70%; background-color:#FFFF96;'>
- <strong>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!).<br /><br/>
- ";
- $htmlString .= Error::getErrNoString($message->errno);
- $htmlString .= ":</strong> <em>{$message->errstr}</em> in <strong>{$message->errfile}</strong>";
- $htmlString .= " on line <strong>{$message->errline}</strong>\n";
- $htmlString .= "<br /><br />Backtrace --&gt;<div style=\"font-family:Courier;font-size:10pt\"><br />\n";
- $htmlString .= str_replace("\n", "<br />\n", $message->backtrace);
- $htmlString .= "</div><br />";
- $htmlString .= "\n </pre></div><br />";
-
- $message = $htmlString;
- }
- }
-
- public static function setErrorHandler()
- {
- Piwik::addAction('Log.formatFileMessage', array('\\Piwik\\Error', 'formatFileAndDBLogMessage'));
- Piwik::addAction('Log.formatDatabaseMessage', array('\\Piwik\\Error', 'formatFileAndDBLogMessage'));
- Piwik::addAction('Log.formatScreenMessage', array('\\Piwik\\Error', 'formatScreenMessage'));
-
- set_error_handler(array('\\Piwik\\Error', 'errorHandler'));
- }
-
- public static function errorHandler($errno, $errstr, $errfile, $errline)
- {
- // if the error has been suppressed by the @ we don't handle the error
- if (error_reporting() == 0) {
- return;
- }
-
- $backtrace = '';
- if (empty(self::$debugBacktraceForTests)) {
- $bt = @debug_backtrace();
- if ($bt !== null && isset($bt[0])) {
- foreach ($bt as $i => $debug) {
- $backtrace .= "#$i "
- . (isset($debug['class']) ? $debug['class'] : '')
- . (isset($debug['type']) ? $debug['type'] : '')
- . (isset($debug['function']) ? $debug['function'] : '')
- . '(...) called at ['
- . (isset($debug['file']) ? $debug['file'] : '') . ':'
- . (isset($debug['line']) ? $debug['line'] : '') . ']' . "\n";
- }
- }
- } else {
- $backtrace = self::$debugBacktraceForTests;
- }
-
- $error = new Error($errno, $errstr, $errfile, $errline, $backtrace);
- Log::error($error);
-
- switch ($errno) {
- case E_ERROR:
- case E_PARSE:
- case E_CORE_ERROR:
- case E_CORE_WARNING:
- case E_COMPILE_ERROR:
- case E_COMPILE_WARNING:
- case E_USER_ERROR:
- exit;
- break;
-
- case E_WARNING:
- case E_NOTICE:
- case E_USER_WARNING:
- case E_USER_NOTICE:
- case E_STRICT:
- case E_RECOVERABLE_ERROR:
- case E_DEPRECATED:
- case E_USER_DEPRECATED:
- default:
- // do not exit
- break;
- }
- }
-}
diff --git a/core/ErrorHandler.php b/core/ErrorHandler.php
new file mode 100644
index 0000000000..fe36a355fd
--- /dev/null
+++ b/core/ErrorHandler.php
@@ -0,0 +1,131 @@
+<?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;
+
+use Piwik\Exception\ErrorException;
+
+/**
+ * Piwik's error handler function.
+ */
+class ErrorHandler
+{
+ /**
+ * Returns a string description of a PHP error number.
+ *
+ * @param int $errno `E_ERROR`, `E_WARNING`, `E_PARSE`, etc.
+ * @return string
+ */
+ public static function getErrNoString($errno)
+ {
+ switch ($errno) {
+ case E_ERROR:
+ return "Error";
+ case E_WARNING:
+ return "Warning";
+ case E_PARSE:
+ return "Parse Error";
+ case E_NOTICE:
+ return "Notice";
+ case E_CORE_ERROR:
+ return "Core Error";
+ case E_CORE_WARNING:
+ return "Core Warning";
+ case E_COMPILE_ERROR:
+ return "Compile Error";
+ case E_COMPILE_WARNING:
+ return "Compile Warning";
+ case E_USER_ERROR:
+ return "User Error";
+ case E_USER_WARNING:
+ return "User Warning";
+ case E_USER_NOTICE:
+ return "User Notice";
+ case E_STRICT:
+ return "Strict Notice";
+ case E_RECOVERABLE_ERROR:
+ return "Recoverable Error";
+ case E_DEPRECATED:
+ return "Deprecated";
+ case E_USER_DEPRECATED:
+ return "User Deprecated";
+ default:
+ return "Unknown error ($errno)";
+ }
+ }
+
+ public static function registerErrorHandler()
+ {
+ set_error_handler(array('Piwik\ErrorHandler', 'errorHandler'));
+ }
+
+ public static function errorHandler($errno, $errstr, $errfile, $errline)
+ {
+ // if the error has been suppressed by the @ we don't handle the error
+ if (error_reporting() == 0) {
+ return;
+ }
+
+ switch ($errno) {
+ case E_ERROR:
+ case E_PARSE:
+ case E_CORE_ERROR:
+ case E_CORE_WARNING:
+ case E_COMPILE_ERROR:
+ case E_COMPILE_WARNING:
+ case E_USER_ERROR:
+ Common::sendResponseCode(500);
+ // Convert the error to an exception with an HTML message
+ $e = new \Exception();
+ $message = self::getHtmlMessage($errno, $errstr, $errfile, $errline, $e->getTraceAsString());
+ throw new ErrorException($message, 0, $errno, $errfile, $errline);
+ break;
+
+ case E_WARNING:
+ case E_NOTICE:
+ case E_USER_WARNING:
+ case E_USER_NOTICE:
+ case E_STRICT:
+ case E_RECOVERABLE_ERROR:
+ case E_DEPRECATED:
+ case E_USER_DEPRECATED:
+ default:
+ Log::warning(self::createLogMessage($errno, $errstr, $errfile, $errline));
+ break;
+ }
+ }
+
+ 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)",
+ $errfile,
+ $errline,
+ ErrorHandler::getErrNoString($errno),
+ $errstr
+ );
+ }
+
+ private static function getHtmlMessage($errno, $errstr, $errfile, $errline, $trace)
+ {
+ $trace = Log::$debugBacktraceForTests ?: $trace;
+
+ $message = ErrorHandler::getErrNoString($errno) . ' - ' . $errstr;
+
+ $html = "<strong>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!).</strong><br /><br/>
+ ";
+ $html .= "<strong>{$message}</strong> in <em>{$errfile}</em>";
+ $html .= " on line {$errline}<br />";
+ $html .= "<br />Backtrace:<div style=\"font-family:Courier;font-size:10pt\"><br />\n";
+ $html .= str_replace("\n", "<br />\n", $trace);
+ $html .= "</div>";
+
+ return $html;
+ }
+}
diff --git a/core/Exception/DatabaseSchemaIsNewerThanCodebaseException.php b/core/Exception/DatabaseSchemaIsNewerThanCodebaseException.php
new file mode 100644
index 0000000000..accc4ce5d5
--- /dev/null
+++ b/core/Exception/DatabaseSchemaIsNewerThanCodebaseException.php
@@ -0,0 +1,14 @@
+<?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\Exception;
+
+class DatabaseSchemaIsNewerThanCodebaseException extends Exception
+{
+
+} \ No newline at end of file
diff --git a/core/Exception/ErrorException.php b/core/Exception/ErrorException.php
new file mode 100644
index 0000000000..1673d8def8
--- /dev/null
+++ b/core/Exception/ErrorException.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Piwik\Exception;
+
+/**
+ * ErrorException
+ *
+ * @author Matthieu Napoli <matthieu@mnapoli.fr>
+ */
+class ErrorException extends \ErrorException
+{
+ public function isHtmlMessage()
+ {
+ return true;
+ }
+}
diff --git a/core/ExceptionHandler.php b/core/ExceptionHandler.php
index 8566385070..e68a058e4a 100644
--- a/core/ExceptionHandler.php
+++ b/core/ExceptionHandler.php
@@ -8,54 +8,92 @@
*/
namespace Piwik;
-use Piwik\API\ResponseBuilder;
-use Piwik\Plugin;
+use Exception;
+use Piwik\Plugins\CoreAdminHome\CustomLogo;
/**
- * Contains Piwik's uncaught exception handler and log file formatting for exception
- * instances.
+ * Contains Piwik's uncaught exception handler.
*/
class ExceptionHandler
{
- /**
- * The backtrace string to use when testing.
- *
- * @var string
- */
- public static $debugBacktraceForTests = null;
-
public static function setUp()
{
- Piwik::addAction('Log.formatFileMessage', array('\\Piwik\\ExceptionHandler', 'formatFileAndDBLogMessage'));
- Piwik::addAction('Log.formatDatabaseMessage', array('\\Piwik\\ExceptionHandler', 'formatFileAndDBLogMessage'));
- Piwik::addAction('Log.formatScreenMessage', array('\\Piwik\\ExceptionHandler', 'formatScreenMessage'));
+ set_exception_handler(array('Piwik\ExceptionHandler', 'handleException'));
+ }
+
+ public static function handleException(Exception $exception)
+ {
+ if (Common::isPhpCliMode()) {
+ self::dieWithCliError($exception);
+ }
- set_exception_handler(array('\\Piwik\\ExceptionHandler', 'logException'));
+ self::dieWithHtmlErrorPage($exception);
}
- public static function formatFileAndDBLogMessage(&$message, $level, $tag, $datetime, $log)
+ public static function dieWithCliError(Exception $exception)
{
- if ($message instanceof \Exception) {
- $message = sprintf("%s(%d): %s\n%s", $message->getFile(), $message->getLine(), $message->getMessage(),
- self::$debugBacktraceForTests ? : $message->getTraceAsString());
+ $message = $exception->getMessage();
- $message = $log->formatMessage($level, $tag, $datetime, $message);
+ if (!method_exists($exception, 'isHtmlMessage') || !$exception->isHtmlMessage()) {
+ $message = strip_tags(str_replace('<br />', PHP_EOL, $message));
}
+
+ $message = sprintf(
+ "Uncaught exception: %s\nin %s line %d\n%s\n",
+ $message,
+ $exception->getFile(),
+ $exception->getLine(),
+ $exception->getTraceAsString()
+ );
+
+ echo $message;
+
+ exit(1);
}
- public static function formatScreenMessage(&$message, $level, $tag, $datetime, $log)
+ public static function dieWithHtmlErrorPage(Exception $exception)
{
- if ($message instanceof \Exception) {
- Common::sendHeader('Content-Type: text/html; charset=utf-8');
+ Common::sendHeader('Content-Type: text/html; char1set=utf-8');
- $outputFormat = strtolower(Common::getRequestVar('format', 'html', 'string'));
- $response = new ResponseBuilder($outputFormat);
- $message = $response->getResponseException(new \Exception($message->getMessage()));
- }
+ echo self::getErrorResponse($exception);
+
+ exit(1);
}
- public static function logException(\Exception $exception)
+ private static function getErrorResponse(Exception $ex)
{
- Log::error($exception);
+ $debugTrace = $ex->getTraceAsString();
+
+ $message = $ex->getMessage();
+
+ if (!method_exists($ex, 'isHtmlMessage') || !$ex->isHtmlMessage()) {
+ $message = Common::sanitizeInputValue($message);
+ }
+
+ $logo = new CustomLogo();
+
+ $logoHeaderUrl = false;
+ $logoFaviconUrl = false;
+ try {
+ $logoHeaderUrl = $logo->getHeaderLogoUrl();
+ $logoFaviconUrl = $logo->getPathUserFavicon();
+ } catch (Exception $ex) {
+ Log::debug($ex);
+ }
+
+ $result = Piwik_GetErrorMessagePage($message, $debugTrace, true, true, $logoHeaderUrl, $logoFaviconUrl);
+
+ /**
+ * Triggered before a Piwik error page is displayed to the user.
+ *
+ * This event can be used to modify the content of the error page that is displayed when
+ * an exception is caught.
+ *
+ * @param string &$result The HTML of the error page.
+ * @param Exception $ex The Exception displayed in the error page.
+ */
+ Piwik::postEvent('FrontController.modifyErrorPage', array(&$result, $ex));
+
+ return $result;
}
}
diff --git a/core/Filesystem.php b/core/Filesystem.php
index 125c47bfda..850b44da63 100644
--- a/core/Filesystem.php
+++ b/core/Filesystem.php
@@ -10,7 +10,8 @@ namespace Piwik;
use Exception;
use Piwik\Container\StaticContainer;
-use Piwik\Tracker\Cache;
+use Piwik\Tracker\Cache as TrackerCache;
+use Piwik\Cache as PiwikCache;
/**
* Contains helper functions that deal with the filesystem.
@@ -26,7 +27,8 @@ class Filesystem
{
AssetManager::getInstance()->removeMergedAssets($pluginName);
View::clearCompiledTemplates();
- Cache::deleteTrackerCache();
+ TrackerCache::deleteTrackerCache();
+ PiwikCache::flushAll();
self::clearPhpCaches();
}
@@ -409,12 +411,36 @@ class Filesystem
}
/**
+ * Remove a file.
+ *
+ * @param string $file
+ * @param bool $silenceErrors If true, no exception will be thrown in case removing fails.
+ */
+ public static function remove($file, $silenceErrors = false)
+ {
+ if (!file_exists($file)) {
+ return;
+ }
+
+ $result = @unlink($file);
+
+ // Testing if the file still exist avoids race conditions
+ if (!$result && file_exists($file)) {
+ if ($silenceErrors) {
+ Log::warning('Failed to delete file ' . $file);
+ } else {
+ throw new \RuntimeException('Unable to delete file ' . $file);
+ }
+ }
+ }
+
+ /**
* @param $path
* @return int
*/
private static function getChmodForPath($path)
{
- $pathIsTmp = StaticContainer::getContainer()->get('path.tmp');
+ $pathIsTmp = StaticContainer::get('path.tmp');
if (strpos($path, $pathIsTmp) === 0) {
// tmp/* folder
return 0750;
diff --git a/core/FrontController.php b/core/FrontController.php
index 7d1702315f..bd473bba8f 100644
--- a/core/FrontController.php
+++ b/core/FrontController.php
@@ -18,7 +18,6 @@ use Piwik\Http\Router;
use Piwik\Plugin\Controller;
use Piwik\Plugin\Report;
use Piwik\Plugin\Widgets;
-use Piwik\Plugins\CoreAdminHome\CustomLogo;
use Piwik\Session;
/**
@@ -230,7 +229,7 @@ class FrontController extends Singleton
Profiler::printQueryCount();
}
} catch (Exception $e) {
- Log::verbose($e);
+ Log::debug($e);
}
}
@@ -254,6 +253,8 @@ class FrontController extends Singleton
{
$lastError = error_get_last();
if (!empty($lastError) && $lastError['type'] == E_ERROR) {
+ Common::sendResponseCode(500);
+
$controller = FrontController::getInstance();
$controller->init();
$message = $controller->dispatch('CorePluginsAdmin', 'safemode', array($lastError));
@@ -263,7 +264,7 @@ class FrontController extends Singleton
}
/**
- * Loads the config file and assign to the global registry
+ * Loads the config file
* This is overridden in tests to ensure test config file is used
*
* @return Exception
@@ -309,11 +310,9 @@ class FrontController extends Singleton
}
$initialized = true;
- Registry::set('timer', new Timer);
-
$exceptionToThrow = self::createConfigObject();
- $tmpPath = StaticContainer::getContainer()->get('path.tmp');
+ $tmpPath = StaticContainer::get('path.tmp');
$directoriesToCheck = array(
$tmpPath,
@@ -324,15 +323,13 @@ class FrontController extends Singleton
$tmpPath . '/templates_c/',
);
- Translate::loadEnglishTranslation();
-
Filechecks::dieIfDirectoriesNotWritable($directoriesToCheck);
$this->handleMaintenanceMode();
$this->handleProfiler();
$this->handleSSLRedirection();
- Plugin\Manager::getInstance()->loadPluginTranslations('en');
+ Plugin\Manager::getInstance()->loadPluginTranslations();
Plugin\Manager::getInstance()->loadActivatedPlugins();
if ($exceptionToThrow) {
@@ -399,6 +396,8 @@ class FrontController extends Singleton
*/
Piwik::postEvent('Request.dispatchCoreAndPluginUpdatesScreen');
+ Updater::throwIfPiwikVersionIsOlderThanDBSchema();
+
\Piwik\Plugin\Manager::getInstance()->installLoadedPlugins();
// ensure the current Piwik URL is known for later use
@@ -416,14 +415,14 @@ class FrontController extends Singleton
* **Example**
*
* Piwik::addAction('Request.initAuthenticationObject', function() {
- * Piwik\Registry::set('auth', new MyAuthImplementation());
+ * StaticContainer::getContainer()->set('Piwik\Auth', new MyAuthImplementation());
* });
*/
Piwik::postEvent('Request.initAuthenticationObject');
try {
- $authAdapter = Registry::get('auth');
+ $authAdapter = StaticContainer::get('Piwik\Auth');
} catch (Exception $e) {
- $message = "Authentication object cannot be found in the Registry. Maybe the Login plugin is not activated?
+ $message = "Authentication object cannot be found in the container. Maybe the Login plugin is not activated?
<br />You can activate the plugin by adding:<br />
<code>Plugins[] = Login</code><br />
under the <code>[Plugins]</code> section in your config/config.ini.php";
@@ -442,7 +441,6 @@ class FrontController extends Singleton
}
SettingsServer::raiseMemoryLimitIfNecessary();
- Translate::reloadLanguage();
\Piwik\Plugin\Manager::getInstance()->postLoadPlugins();
/**
@@ -468,6 +466,8 @@ class FrontController extends Singleton
&& ($module !== 'API' || ($action && $action !== 'index'))
) {
Session::start();
+
+ $this->closeSessionEarlyForFasterUI();
}
if (is_null($parameters)) {
@@ -478,7 +478,7 @@ class FrontController extends Singleton
throw new Exception("Invalid module name '$module'");
}
- $module = Request::renameModule($module);
+ list($module, $action) = Request::getRenamedModuleAndAction($module, $action);
if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated($module)) {
throw new PluginDeactivatedException($module);
@@ -537,6 +537,25 @@ class FrontController extends Singleton
Url::redirectToHttps();
}
+ private function closeSessionEarlyForFasterUI()
+ {
+ $isDashboardReferrer = !empty($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'module=CoreHome&action=index') !== false;
+ $isAllWebsitesReferrer = !empty($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], 'module=MultiSites&action=index') !== false;
+
+ if ($isDashboardReferrer
+ && !empty($_POST['token_auth'])
+ && Common::getRequestVar('widget', 0, 'int') === 1
+ ) {
+ Session::close();
+ }
+
+ if (($isDashboardReferrer || $isAllWebsitesReferrer)
+ && Common::getRequestVar('viewDataTable', '', 'string') === 'sparkline'
+ ) {
+ Session::close();
+ }
+ }
+
private function handleProfiler()
{
if (!empty($_GET['xhprof'])) {
@@ -611,47 +630,4 @@ class FrontController extends Singleton
return $result;
}
- /**
- * Returns HTML that displays an exception's error message (and possibly stack trace).
- * The result of this method is echo'd by dispatch.php.
- *
- * @param Exception $ex The exception to use when generating the error page's HTML.
- * @return string The HTML to echo.
- */
- public function getErrorResponse(Exception $ex)
- {
- $debugTrace = $ex->getTraceAsString();
-
- $message = $ex->getMessage();
-
- if (!method_exists($ex, 'isHtmlMessage') || !$ex->isHtmlMessage()) {
- $message = Common::sanitizeInputValue($message);
- }
-
- $logo = new CustomLogo();
-
- $logoHeaderUrl = false;
- $logoFaviconUrl = false;
- try {
- $logoHeaderUrl = $logo->getHeaderLogoUrl();
- $logoFaviconUrl = $logo->getPathUserFavicon();
- } catch (Exception $ex) {
- Log::debug($ex);
- }
-
- $result = Piwik_GetErrorMessagePage($message, $debugTrace, true, true, $logoHeaderUrl, $logoFaviconUrl);
-
- /**
- * Triggered before a Piwik error page is displayed to the user.
- *
- * This event can be used to modify the content of the error page that is displayed when
- * an exception is caught.
- *
- * @param string &$result The HTML of the error page.
- * @param Exception $ex The Exception displayed in the error page.
- */
- Piwik::postEvent('FrontController.modifyErrorPage', array(&$result, $ex));
-
- return $result;
- }
}
diff --git a/core/Http.php b/core/Http.php
index 3643fc8553..25d6d59566 100644
--- a/core/Http.php
+++ b/core/Http.php
@@ -47,7 +47,7 @@ class Http
protected static function isCurlEnabled()
{
- return function_exists('curl_init');
+ return function_exists('curl_init') && function_exists('curl_exec');
}
/**
@@ -99,7 +99,7 @@ class Http
*
* @param string $method
* @param string $aUrl
- * @param int $timeout
+ * @param int $timeout in seconds
* @param string $userAgent
* @param string $destinationPath
* @param resource $file
@@ -444,6 +444,7 @@ class Http
// only get header info if not saving directly to file
CURLOPT_HEADER => is_resource($file) ? false : true,
CURLOPT_CONNECTTIMEOUT => $timeout,
+ CURLOPT_TIMEOUT => $timeout,
);
// Case core:archive command is triggering archiving on https:// and the certificate is not valid
if ($acceptInvalidSslCertificate) {
diff --git a/core/Intl/Data/Provider/CurrencyDataProvider.php b/core/Intl/Data/Provider/CurrencyDataProvider.php
new file mode 100644
index 0000000000..ae73a5bce0
--- /dev/null
+++ b/core/Intl/Data/Provider/CurrencyDataProvider.php
@@ -0,0 +1,33 @@
+<?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\Intl\Data\Provider;
+
+/**
+ * Provides currency data.
+ */
+class CurrencyDataProvider
+{
+ private $currencyList;
+
+ /**
+ * Returns the list of all known currency symbols.
+ *
+ * @return array An array mapping currency codes to their respective currency symbols
+ * and a description, eg, `array('USD' => array('$', 'US dollar'))`.
+ * @api
+ */
+ public function getCurrencyList()
+ {
+ if ($this->currencyList === null) {
+ $this->currencyList = require __DIR__ . '/../Resources/currencies.php';
+ }
+
+ return $this->currencyList;
+ }
+}
diff --git a/core/Intl/Data/Provider/LanguageDataProvider.php b/core/Intl/Data/Provider/LanguageDataProvider.php
new file mode 100644
index 0000000000..52f0b72f4c
--- /dev/null
+++ b/core/Intl/Data/Provider/LanguageDataProvider.php
@@ -0,0 +1,50 @@
+<?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\Intl\Data\Provider;
+
+/**
+ * Provides language data.
+ */
+class LanguageDataProvider
+{
+ private $languageList;
+ private $languageToCountryList;
+
+ /**
+ * Returns the list of valid language codes.
+ *
+ * @return string[] Array of 2 letter ISO code => language name (in english).
+ * E.g. `array('en' => 'English', 'ja' => 'Japanese')`.
+ * @api
+ */
+ public function getLanguageList()
+ {
+ if ($this->languageList === null) {
+ $this->languageList = require __DIR__ . '/../Resources/languages.php';
+ }
+
+ return $this->languageList;
+ }
+
+ /**
+ * Returns the list of language to country mappings.
+ *
+ * @return string[] Array of 2 letter ISO language code => 2 letter ISO country code.
+ * E.g. `array('fr' => 'fr') // French => France`.
+ * @api
+ */
+ public function getLanguageToCountryList()
+ {
+ if ($this->languageToCountryList === null) {
+ $this->languageToCountryList = require __DIR__ . '/../Resources/languages-to-countries.php';
+ }
+
+ return $this->languageToCountryList;
+ }
+}
diff --git a/core/Intl/Data/Provider/RegionDataProvider.php b/core/Intl/Data/Provider/RegionDataProvider.php
new file mode 100644
index 0000000000..e66614871b
--- /dev/null
+++ b/core/Intl/Data/Provider/RegionDataProvider.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\Intl\Data\Provider;
+
+/**
+ * Provides region related data (continents, countries, etc.).
+ */
+class RegionDataProvider
+{
+ private $continentList;
+ private $countryList;
+ private $countryExtraList;
+
+ /**
+ * Returns the list of continent codes.
+ *
+ * @return string[] Array of 3 letter continent codes
+ * @api
+ */
+ public function getContinentList()
+ {
+ if ($this->continentList === null) {
+ $this->continentList = require __DIR__ . '/../Resources/continents.php';
+ }
+
+ return $this->continentList;
+ }
+
+ /**
+ * Returns the list of valid country codes.
+ *
+ * @param bool $includeInternalCodes
+ * @return string[] Array of 2 letter country ISO codes => 3 letter continent code
+ * @api
+ */
+ public function getCountryList($includeInternalCodes = false)
+ {
+ if ($this->countryList === null) {
+ $this->countryList = require __DIR__ . '/../Resources/countries.php';
+ }
+ if ($this->countryExtraList === null) {
+ $this->countryExtraList = require __DIR__ . '/../Resources/countries-extra.php';
+ }
+
+ if ($includeInternalCodes) {
+ return array_merge($this->countryList, $this->countryExtraList);
+ }
+
+ return $this->countryList;
+ }
+}
diff --git a/core/Intl/Data/Resources/continents.php b/core/Intl/Data/Resources/continents.php
new file mode 100644
index 0000000000..4e346b2cc6
--- /dev/null
+++ b/core/Intl/Data/Resources/continents.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
+ */
+
+/**
+ * Continent database.
+ *
+ * Primary reference: ISO 3166-1 alpha-2
+ */
+return array(
+ 'unk', // unknown
+ 'amn', // North America
+ 'amc', // Central America
+ 'ams', // South America
+ 'eur', // Europe
+ 'afr', // Africa
+ 'asi', // Asia
+ 'oce', // Oceania
+ 'ant', // Antarctica
+);
diff --git a/core/Intl/Data/Resources/countries-extra.php b/core/Intl/Data/Resources/countries-extra.php
new file mode 100644
index 0000000000..e237dd6b6d
--- /dev/null
+++ b/core/Intl/Data/Resources/countries-extra.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Country codes database.
+ *
+ * The mapping of countries to continents is from MaxMind with the exception
+ * of Central America. MaxMind groups Central American countries with
+ * North America. Piwik previously grouped Central American countries with
+ * South America. Given this conflict and the fact that most of Central
+ * America lies on its own continental plate (i.e., the Caribbean Plate), we
+ * currently use a separate continent code (amc).
+ */
+return array(
+ // unknown
+ 'xx' => 'unk',
+
+ // exceptionally reserved
+ 'ac' => 'afr', // .ac TLD
+ 'cp' => 'amc',
+ 'dg' => 'asi',
+ 'ea' => 'afr',
+ 'eu' => 'eur', // .eu TLD
+ 'fx' => 'eur',
+ 'ic' => 'afr',
+ 'su' => 'eur', // .su TLD
+ 'ta' => 'afr',
+ 'uk' => 'eur', // .uk TLD
+
+ // transitionally reserved
+ 'an' => 'amc', // former Netherlands Antilles
+ 'bu' => 'asi',
+ 'cs' => 'eur', // former Serbia and Montenegro
+ 'nt' => 'asi',
+ 'sf' => 'eur',
+ 'tp' => 'oce', // .tp TLD
+ 'yu' => 'eur', // .yu TLD
+ 'zr' => 'afr',
+
+ // MaxMind GeoIP specific
+ 'a1' => 'unk',
+ 'a2' => 'unk',
+ 'ap' => 'asi',
+ 'o1' => 'unk',
+
+ // Catalonia (Spain)
+ 'cat' => 'eur',
+);
diff --git a/core/Intl/Data/Resources/countries.php b/core/Intl/Data/Resources/countries.php
new file mode 100644
index 0000000000..15197f8846
--- /dev/null
+++ b/core/Intl/Data/Resources/countries.php
@@ -0,0 +1,272 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Country code and continent database.
+ *
+ * The mapping of countries to continents is from MaxMind with the exception
+ * of Central America. MaxMind groups Central American countries with
+ * North America. Piwik previously grouped Central American countries with
+ * South America. Given this conflict and the fact that most of Central
+ * America lies on its own continental plate (i.e., the Caribbean Plate), we
+ * currently use a separate continent code (amc).
+ *
+ * Primary reference: ISO 3166-1 alpha-2
+ */
+return array(
+ 'ad' => 'eur',
+ 'ae' => 'asi',
+ 'af' => 'asi',
+ 'ag' => 'amc',
+ 'ai' => 'amc',
+ 'al' => 'eur',
+ 'am' => 'asi',
+ 'ao' => 'afr',
+ 'aq' => 'ant',
+ 'ar' => 'ams',
+ 'as' => 'oce',
+ 'at' => 'eur',
+ 'au' => 'oce',
+ 'aw' => 'amc',
+ 'ax' => 'eur',
+ 'az' => 'asi',
+ 'ba' => 'eur',
+ 'bb' => 'amc',
+ 'bd' => 'asi',
+ 'be' => 'eur',
+ 'bf' => 'afr',
+ 'bg' => 'eur',
+ 'bh' => 'asi',
+ 'bi' => 'afr',
+ 'bj' => 'afr',
+ 'bl' => 'amc',
+ 'bm' => 'amc',
+ 'bn' => 'asi',
+ 'bo' => 'ams',
+ 'bq' => 'amc',
+ 'br' => 'ams',
+ 'bs' => 'amc',
+ 'bt' => 'asi',
+ 'bv' => 'ant',
+ 'bw' => 'afr',
+ 'by' => 'eur',
+ 'bz' => 'amc',
+ 'ca' => 'amn',
+ 'cc' => 'asi',
+ 'cd' => 'afr',
+ 'cf' => 'afr',
+ 'cg' => 'afr',
+ 'ch' => 'eur',
+ 'ci' => 'afr',
+ 'ck' => 'oce',
+ 'cl' => 'ams',
+ 'cm' => 'afr',
+ 'cn' => 'asi',
+ 'co' => 'ams',
+ 'cr' => 'amc',
+ 'cu' => 'amc',
+ 'cv' => 'afr',
+ 'cw' => 'amc',
+ 'cx' => 'asi',
+ 'cy' => 'eur',
+ 'cz' => 'eur',
+ 'de' => 'eur',
+ 'dj' => 'afr',
+ 'dk' => 'eur',
+ 'dm' => 'amc',
+ 'do' => 'amc',
+ 'dz' => 'afr',
+ 'ec' => 'ams',
+ 'ee' => 'eur',
+ 'eg' => 'afr',
+ 'eh' => 'afr',
+ 'er' => 'afr',
+ 'es' => 'eur',
+ 'et' => 'afr',
+ 'fi' => 'eur',
+ 'fj' => 'oce',
+ 'fk' => 'ams',
+ 'fm' => 'oce',
+ 'fo' => 'eur',
+ 'fr' => 'eur',
+ 'ga' => 'afr',
+ 'gb' => 'eur',
+ 'gd' => 'amc',
+ 'ge' => 'asi',
+ 'gf' => 'ams',
+ 'gg' => 'eur',
+ 'gh' => 'afr',
+ 'gi' => 'eur',
+ 'gl' => 'amn',
+ 'gm' => 'afr',
+ 'gn' => 'afr',
+ 'gp' => 'amc',
+ 'gq' => 'afr',
+ 'gr' => 'eur',
+ 'gs' => 'ant',
+ 'gt' => 'amc',
+ 'gu' => 'oce',
+ 'gw' => 'afr',
+ 'gy' => 'ams',
+ 'hk' => 'asi',
+ 'hm' => 'ant',
+ 'hn' => 'amc',
+ 'hr' => 'eur',
+ 'ht' => 'amc',
+ 'hu' => 'eur',
+ 'id' => 'asi',
+ 'ie' => 'eur',
+ 'il' => 'asi',
+ 'im' => 'eur',
+ 'in' => 'asi',
+ 'io' => 'asi',
+ 'iq' => 'asi',
+ 'ir' => 'asi',
+ 'is' => 'eur',
+ 'it' => 'eur',
+ 'je' => 'eur',
+ 'jm' => 'amc',
+ 'jo' => 'asi',
+ 'jp' => 'asi',
+ 'ke' => 'afr',
+ 'kg' => 'asi',
+ 'kh' => 'asi',
+ 'ki' => 'oce',
+ 'km' => 'afr',
+ 'kn' => 'amc',
+ 'kp' => 'asi',
+ 'kr' => 'asi',
+ 'kw' => 'asi',
+ 'ky' => 'amc',
+ 'kz' => 'asi',
+ 'la' => 'asi',
+ 'lb' => 'asi',
+ 'lc' => 'amc',
+ 'li' => 'eur',
+ 'lk' => 'asi',
+ 'lr' => 'afr',
+ 'ls' => 'afr',
+ 'lt' => 'eur',
+ 'lu' => 'eur',
+ 'lv' => 'eur',
+ 'ly' => 'afr',
+ 'ma' => 'afr',
+ 'mc' => 'eur',
+ 'md' => 'eur',
+ 'me' => 'eur',
+ 'mf' => 'amc',
+ 'mg' => 'afr',
+ 'mh' => 'oce',
+ 'mk' => 'eur',
+ 'ml' => 'afr',
+ 'mm' => 'asi',
+ 'mn' => 'asi',
+ 'mo' => 'asi',
+ 'mp' => 'oce',
+ 'mq' => 'amc',
+ 'mr' => 'afr',
+ 'ms' => 'amc',
+ 'mt' => 'eur',
+ 'mu' => 'afr',
+ 'mv' => 'asi',
+ 'mw' => 'afr',
+ 'mx' => 'amn',
+ 'my' => 'asi',
+ 'mz' => 'afr',
+ 'na' => 'afr',
+ 'nc' => 'oce',
+ 'ne' => 'afr',
+ 'nf' => 'oce',
+ 'ng' => 'afr',
+ 'ni' => 'amc',
+ 'nl' => 'eur',
+ 'no' => 'eur',
+ 'np' => 'asi',
+ 'nr' => 'oce',
+ 'nu' => 'oce',
+ 'nz' => 'oce',
+ 'om' => 'asi',
+ 'pa' => 'amc',
+ 'pe' => 'ams',
+ 'pf' => 'oce',
+ 'pg' => 'oce',
+ 'ph' => 'asi',
+ 'pk' => 'asi',
+ 'pl' => 'eur',
+ 'pm' => 'amn',
+ 'pn' => 'oce',
+ 'pr' => 'amc',
+ 'ps' => 'asi',
+ 'pt' => 'eur',
+ 'pw' => 'oce',
+ 'py' => 'ams',
+ 'qa' => 'asi',
+ 're' => 'afr',
+ 'ro' => 'eur',
+ 'rs' => 'eur',
+ 'ru' => 'eur',
+ 'rw' => 'afr',
+ 'sa' => 'asi',
+ 'sb' => 'oce',
+ 'sc' => 'afr',
+ 'sd' => 'afr',
+ 'se' => 'eur',
+ 'sg' => 'asi',
+ 'sh' => 'afr',
+ 'si' => 'eur',
+ 'sj' => 'eur',
+ 'sk' => 'eur',
+ 'sl' => 'afr',
+ 'sm' => 'eur',
+ 'sn' => 'afr',
+ 'so' => 'afr',
+ 'sr' => 'ams',
+ 'ss' => 'afr',
+ 'st' => 'afr',
+ 'sv' => 'amc',
+ 'sx' => 'amc',
+ 'sy' => 'asi',
+ 'sz' => 'afr',
+ 'tc' => 'amc',
+ 'td' => 'afr',
+ 'tf' => 'ant',
+ 'tg' => 'afr',
+ 'th' => 'asi',
+ 'ti' => 'asi',
+ 'tj' => 'asi',
+ 'tk' => 'oce',
+ 'tl' => 'asi',
+ 'tm' => 'asi',
+ 'tn' => 'afr',
+ 'to' => 'oce',
+ 'tr' => 'eur',
+ 'tt' => 'amc',
+ 'tv' => 'oce',
+ 'tw' => 'asi',
+ 'tz' => 'afr',
+ 'ua' => 'eur',
+ 'ug' => 'afr',
+ 'um' => 'oce',
+ 'us' => 'amn',
+ 'uy' => 'ams',
+ 'uz' => 'asi',
+ 'va' => 'eur',
+ 'vc' => 'amc',
+ 've' => 'ams',
+ 'vg' => 'amc',
+ 'vi' => 'amc',
+ 'vn' => 'asi',
+ 'vu' => 'oce',
+ 'wf' => 'oce',
+ 'ws' => 'oce',
+ 'ye' => 'asi',
+ 'yt' => 'afr',
+ 'za' => 'afr',
+ 'zm' => 'afr',
+ 'zw' => 'afr',
+);
diff --git a/core/Intl/Data/Resources/currencies.php b/core/Intl/Data/Resources/currencies.php
new file mode 100644
index 0000000000..6190c3108f
--- /dev/null
+++ b/core/Intl/Data/Resources/currencies.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * International currencies in circulation.
+ *
+ * @see http://en.wikipedia.org/wiki/List_of_circulating_currencies
+ */
+return array(
+ // 'ISO-4217 CODE' => array('currency symbol', 'description'),
+
+ // Top 5 by global trading volume
+ 'USD' => array('$', 'US dollar'),
+ 'EUR' => array('€', 'Euro'),
+ 'JPY' => array('¥', 'Japanese yen'),
+ 'GBP' => array('£', 'British pound'),
+ 'CHF' => array('Fr', 'Swiss franc'),
+
+ 'AFN' => array('؋', 'Afghan afghani'),
+ 'ALL' => array('L', 'Albanian lek'),
+ 'DZD' => array('د.ج', 'Algerian dinar'),
+ 'AOA' => array('Kz', 'Angolan kwanza'),
+ 'ARS' => array('$', 'Argentine peso'),
+ 'AMD' => array('դր.', 'Armenian dram'),
+ 'AWG' => array('ƒ', 'Aruban florin'),
+ 'AUD' => array('$', 'Australian dollar'),
+ 'AZN' => array('m', 'Azerbaijani manat'),
+ 'BSD' => array('$', 'Bahamian dollar'),
+ 'BHD' => array('.د.ب', 'Bahraini dinar'),
+ 'BDT' => array('৳', 'Bangladeshi taka'),
+ 'BBD' => array('$', 'Barbadian dollar'),
+ 'BYR' => array('Br', 'Belarusian ruble'),
+ 'BZD' => array('$', 'Belize dollar'),
+ 'BMD' => array('$', 'Bermudian dollar'),
+ 'BTC' => array('BTC', 'Bitcoin'),
+ 'BTN' => array('Nu.', 'Bhutanese ngultrum'),
+ 'BOB' => array('Bs.', 'Bolivian boliviano'),
+ 'BAM' => array('KM', 'Bosnia Herzegovina mark'),
+ 'BWP' => array('P', 'Botswana pula'),
+ 'BRL' => array('R$', 'Brazilian real'),
+// 'GBP' => array('£', 'British pound'),
+ 'BND' => array('$', 'Brunei dollar'),
+ 'BGN' => array('лв', 'Bulgarian lev'),
+ 'BIF' => array('Fr', 'Burundian franc'),
+ 'KHR' => array('៛', 'Cambodian riel'),
+ 'CAD' => array('$', 'Canadian dollar'),
+ 'CVE' => array('$', 'Cape Verdean escudo'),
+ 'KYD' => array('$', 'Cayman Islands dollar'),
+ 'XAF' => array('Fr', 'Central African CFA franc'),
+ 'CLP' => array('$', 'Chilean peso'),
+ 'CNY' => array('元', 'Chinese yuan'),
+ 'COP' => array('$', 'Colombian peso'),
+ 'KMF' => array('Fr', 'Comorian franc'),
+ 'CDF' => array('Fr', 'Congolese franc'),
+ 'CRC' => array('₡', 'Costa Rican colón'),
+ 'HRK' => array('kn', 'Croatian kuna'),
+ 'XPF' => array('F', 'CFP franc'),
+ 'CUC' => array('$', 'Cuban convertible peso'),
+ 'CUP' => array('$', 'Cuban peso'),
+ 'CMG' => array('ƒ', 'Curaçao and Sint Maarten guilder'),
+ 'CZK' => array('Kč', 'Czech koruna'),
+ 'DKK' => array('kr', 'Danish krone'),
+ 'DJF' => array('Fr', 'Djiboutian franc'),
+ 'DOP' => array('$', 'Dominican peso'),
+ 'XCD' => array('$', 'East Caribbean dollar'),
+ 'EGP' => array('ج.م', 'Egyptian pound'),
+ 'ERN' => array('Nfk', 'Eritrean nakfa'),
+ 'ETB' => array('Br', 'Ethiopian birr'),
+// 'EUR' => array('€', 'Euro'),
+ 'FKP' => array('£', 'Falkland Islands pound'),
+ 'FJD' => array('$', 'Fijian dollar'),
+ 'GMD' => array('D', 'Gambian dalasi'),
+ 'GEL' => array('ლ', 'Georgian lari'),
+ 'GHS' => array('₵', 'Ghanaian cedi'),
+ 'GIP' => array('£', 'Gibraltar pound'),
+ 'GTQ' => array('Q', 'Guatemalan quetzal'),
+ 'GNF' => array('Fr', 'Guinean franc'),
+ 'GYD' => array('$', 'Guyanese dollar'),
+ 'HTG' => array('G', 'Haitian gourde'),
+ 'HNL' => array('L', 'Honduran lempira'),
+ 'HKD' => array('$', 'Hong Kong dollar'),
+ 'HUF' => array('Ft', 'Hungarian forint'),
+ 'ISK' => array('kr', 'Icelandic króna'),
+ 'INR' => array('‎₹', 'Indian rupee'),
+ 'IDR' => array('Rp', 'Indonesian rupiah'),
+ 'IRR' => array('﷼', 'Iranian rial'),
+ 'IQD' => array('ع.د', 'Iraqi dinar'),
+ 'ILS' => array('₪', 'Israeli new shekel'),
+ 'JMD' => array('$', 'Jamaican dollar'),
+// 'JPY' => array('¥', 'Japanese yen'),
+ 'JOD' => array('د.ا', 'Jordanian dinar'),
+ 'KZT' => array('₸', 'Kazakhstani tenge'),
+ 'KES' => array('Sh', 'Kenyan shilling'),
+ 'KWD' => array('د.ك', 'Kuwaiti dinar'),
+ 'KGS' => array('лв', 'Kyrgyzstani som'),
+ 'LAK' => array('₭', 'Lao kip'),
+ 'LBP' => array('ل.ل', 'Lebanese pound'),
+ 'LSL' => array('L', 'Lesotho loti'),
+ 'LRD' => array('$', 'Liberian dollar'),
+ 'LYD' => array('ل.د', 'Libyan dinar'),
+ 'LTL' => array('Lt', 'Lithuanian litas'),
+ 'MOP' => array('P', 'Macanese pataca'),
+ 'MKD' => array('ден', 'Macedonian denar'),
+ 'MGA' => array('Ar', 'Malagasy ariary'),
+ 'MWK' => array('MK', 'Malawian kwacha'),
+ 'MYR' => array('RM', 'Malaysian ringgit'),
+ 'MVR' => array('ރ.', 'Maldivian rufiyaa'),
+ 'MRO' => array('UM', 'Mauritanian ouguiya'),
+ 'MUR' => array('₨', 'Mauritian rupee'),
+ 'MXN' => array('$', 'Mexican peso'),
+ 'MDL' => array('L', 'Moldovan leu'),
+ 'MNT' => array('₮', 'Mongolian tögrög'),
+ 'MAD' => array('د.م.', 'Moroccan dirham'),
+ 'MZN' => array('MTn', 'Mozambican metical'),
+ 'MMK' => array('K', 'Myanma kyat'),
+ 'NAD' => array('$', 'Namibian dollar'),
+ 'NPR' => array('₨', 'Nepalese rupee'),
+ 'ANG' => array('ƒ', 'Netherlands Antillean guilder'),
+ 'TWD' => array('$', 'New Taiwan dollar'),
+ 'NZD' => array('$', 'New Zealand dollar'),
+ 'NIO' => array('C$', 'Nicaraguan córdoba'),
+ 'NGN' => array('₦', 'Nigerian naira'),
+ 'KPW' => array('₩', 'North Korean won'),
+ 'NOK' => array('kr', 'Norwegian krone'),
+ 'OMR' => array('ر.ع.', 'Omani rial'),
+ 'PKR' => array('₨', 'Pakistani rupee'),
+ 'PAB' => array('B/.', 'Panamanian balboa'),
+ 'PGK' => array('K', 'Papua New Guinean kina'),
+ 'PYG' => array('₲', 'Paraguayan guaraní'),
+ 'PEN' => array('S/.', 'Peruvian nuevo sol'),
+ 'PHP' => array('₱', 'Philippine peso'),
+ 'PLN' => array('zł', 'Polish złoty'),
+ 'QAR' => array('ر.ق', 'Qatari riyal'),
+ 'RON' => array('L', 'Romanian leu'),
+ 'RUB' => array('руб.', 'Russian ruble'),
+ 'RWF' => array('Fr', 'Rwandan franc'),
+ 'SHP' => array('£', 'Saint Helena pound'),
+ 'SVC' => array('₡', 'Salvadoran colón'),
+ 'WST' => array('T', 'Samoan tala'),
+ 'STD' => array('Db', 'São Tomé and Príncipe dobra'),
+ 'SAR' => array('ر.س', 'Saudi riyal'),
+ 'RSD' => array('дин. or din.', 'Serbian dinar'),
+ 'SCR' => array('₨', 'Seychellois rupee'),
+ 'SLL' => array('Le', 'Sierra Leonean leone'),
+ 'SGD' => array('$', 'Singapore dollar'),
+ 'SBD' => array('$', 'Solomon Islands dollar'),
+ 'SOS' => array('Sh', 'Somali shilling'),
+ 'ZAR' => array('R', 'South African rand'),
+ 'KRW' => array('₩', 'South Korean won'),
+ 'LKR' => array('Rs', 'Sri Lankan rupee'),
+ 'SDG' => array('جنيه سوداني', 'Sudanese pound'),
+ 'SRD' => array('$', 'Surinamese dollar'),
+ 'SZL' => array('L', 'Swazi lilangeni'),
+ 'SEK' => array('kr', 'Swedish krona'),
+// 'CHF' => array('Fr', 'Swiss franc'),
+ 'SYP' => array('ل.س', 'Syrian pound'),
+ 'TJS' => array('ЅМ', 'Tajikistani somoni'),
+ 'TZS' => array('Sh', 'Tanzanian shilling'),
+ 'THB' => array('฿', 'Thai baht'),
+ 'TOP' => array('T$', 'Tongan paʻanga'),
+ 'TTD' => array('$', 'Trinidad and Tobago dollar'),
+ 'TND' => array('د.ت', 'Tunisian dinar'),
+ 'TRY' => array('TL', 'Turkish lira'),
+ 'TMM' => array('m', 'Turkmenistani manat'),
+ 'UGX' => array('Sh', 'Ugandan shilling'),
+ 'UAH' => array('₴', 'Ukrainian hryvnia'),
+ 'AED' => array('د.إ', 'United Arab Emirates dirham'),
+// 'USD' => array('$', 'United States dollar'),
+ 'UYU' => array('$', 'Uruguayan peso'),
+ 'UZS' => array('лв', 'Uzbekistani som'),
+ 'VUV' => array('Vt', 'Vanuatu vatu'),
+ 'VEF' => array('Bs F', 'Venezuelan bolívar'),
+ 'VND' => array('₫', 'Vietnamese đồng'),
+ 'XOF' => array('Fr', 'West African CFA franc'),
+ 'YER' => array('﷼', 'Yemeni rial'),
+ 'ZMW' => array('ZK', 'Zambian kwacha'),
+ 'ZWL' => array('$', 'Zimbabwean dollar'),
+);
diff --git a/core/Intl/Data/Resources/languages-to-countries.php b/core/Intl/Data/Resources/languages-to-countries.php
new file mode 100644
index 0000000000..91ab0940c5
--- /dev/null
+++ b/core/Intl/Data/Resources/languages-to-countries.php
@@ -0,0 +1,60 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Language to Country mapping
+ *
+ * This list is used to guess the visitor's country when the region is
+ * not specified in the first language tag. Inclusion/exclusion is
+ * based on Piwik.org visitor statistics and probability of disambiguation.
+ * (Notably, "en" and "zh" are excluded.)
+ *
+ * If you want to add a new entry, please email us at hello at piwik.org
+ */
+return array(
+ 'bg' => 'bg', // Bulgarian => Bulgaria
+ 'ca' => 'es', // Catalan => Spain
+ 'cs' => 'cz', // Czech => Czech Republic
+ 'da' => 'dk', // Danish => Denmark
+ 'de' => 'de', // German => Germany
+ 'el' => 'gr', // Greek => Greece
+ 'es' => 'es', // Spanish => Spain
+ 'et' => 'ee', // Estonian => Estonia
+ 'fa' => 'ir', // Farsi => Iran
+ 'fi' => 'fi', // Finnish => Finland
+ 'fr' => 'fr', // French => France
+ 'he' => 'il', // Hebrew => Israel
+ 'hr' => 'hr', // Croatian => Croatia
+ 'hu' => 'hu', // Hungarian => Hungary
+ 'id' => 'id', // Indonesian => Indonesia
+ 'is' => 'is', // Icelandic => Iceland
+ 'it' => 'it', // Italian => Italy
+ 'ja' => 'jp', // Japanese => Japan
+ 'ko' => 'kr', // Korean => South Korea
+ 'lt' => 'lt', // Lithuanian => Lithuania
+ 'lv' => 'lv', // Latvian => Latvia
+ 'mk' => 'mk', // Macedonian => Macedonia
+ 'ms' => 'my', // Malay => Malaysia
+ 'nb' => 'no', // Bokmål => Norway
+ 'nl' => 'nl', // Dutch => Netherlands
+ 'nn' => 'no', // Nynorsk => Norway
+ 'no' => 'no', // Norwegian => Norway
+ 'pl' => 'pl', // Polish => Poland
+ 'pt' => 'pt', // Portugese => Portugal
+ 'ro' => 'ro', // Romanian => Romania
+ 'ru' => 'ru', // Russian => Russia
+ 'sk' => 'sk', // Slovak => Slovakia
+ 'sl' => 'si', // Slovene => Slovenia
+ 'sq' => 'al', // Albanian => Albania
+ 'sr' => 'rs', // Serbian => Serbia
+ 'sv' => 'se', // Swedish => Sweden
+ 'th' => 'th', // Thai => Thailand
+ 'bo' => 'ti', // Tibetan => Tibet
+ 'tr' => 'tr', // Turkish => Turkey
+ 'uk' => 'ua', // Ukrainian => Ukraine
+);
diff --git a/core/Intl/Data/Resources/languages.php b/core/Intl/Data/Resources/languages.php
new file mode 100644
index 0000000000..ca6930f369
--- /dev/null
+++ b/core/Intl/Data/Resources/languages.php
@@ -0,0 +1,201 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+/**
+ * Language database
+ *
+ * Reference: ISO 639-1 alpha-2
+ */
+return array(
+ 'aa' => array('Afar'),
+ 'ab' => array('Abkhazian'),
+ 'ae' => array('Avestan'),
+ 'af' => array('Afrikaans'),
+ 'ak' => array('Akan'),
+ 'am' => array('Amharic'),
+ 'an' => array('Aragonese'),
+ 'ar' => array('Arabic'),
+ 'as' => array('Assamese'),
+ 'av' => array('Avaric'),
+ 'ay' => array('Aymara'),
+ 'az' => array('Azerbaijani'),
+ 'ba' => array('Bashkir'),
+ 'be' => array('Belarusian'),
+ 'bg' => array('Bulgarian'),
+ 'bh' => array('Bihari'), // 'Bihari languages'
+ 'bi' => array('Bislama'),
+ 'bm' => array('Bambara'),
+ 'bn' => array('Bengali'),
+ 'bo' => array('Tibetan'),
+ 'br' => array('Breton'),
+ 'bs' => array('Bosnian'),
+ 'ca' => array('Catalan', 'Valencian'),
+ 'ce' => array('Chechen'),
+ 'ch' => array('Chamorro'),
+ 'co' => array('Corsican'),
+ 'cr' => array('Cree'),
+ 'cs' => array('Czech'),
+ 'cu' => array('Church Slavic', 'Old Slavonic', 'Church Slavonic', 'Old Bulgarian', 'Old Church Slavonic'),
+ 'cv' => array('Chuvash'),
+ 'cy' => array('Welsh'),
+ 'da' => array('Danish'),
+ 'de' => array('German'),
+ 'dv' => array('Divehi', 'Dhivehi', 'Maldivian'),
+ 'dz' => array('Dzongkha'),
+ 'ee' => array('Ewe'),
+ 'el' => array('Greek', 'Modern Greek', 'Hellenic'), // Greek, Modern (1453-)
+ 'en' => array('English'),
+ 'eo' => array('Esperanto'),
+ 'es' => array('Spanish', 'Castilian'),
+ 'et' => array('Estonian'),
+ 'eu' => array('Basque'),
+ 'fa' => array('Persian'),
+ 'ff' => array('Fulah'),
+ 'fi' => array('Finnish'),
+ 'fj' => array('Fijian'),
+ 'fo' => array('Faroese'),
+ 'fr' => array('French'),
+ 'fy' => array('Western Frisian'),
+ 'ga' => array('Irish'),
+ 'gd' => array('Gaelic', 'Scottish Gaelic'),
+ 'gl' => array('Galician'),
+ 'gn' => array('Guarani'),
+ 'gu' => array('Gujarati'),
+ 'gv' => array('Manx'),
+ 'ha' => array('Hausa'),
+ 'he' => array('Hebrew'),
+ 'hi' => array('Hindi'),
+ 'ho' => array('Hiri Motu'),
+ 'hr' => array('Croatian'),
+ 'ht' => array('Haitian', 'Haitian Creole'),
+ 'hu' => array('Hungarian'),
+ 'hy' => array('Armenian'),
+ 'hz' => array('Herero'),
+ 'ia' => array('Interlingua'), // 'Interlingua (International Auxiliary Language Association)'
+ 'id' => array('Indonesian'),
+ 'ie' => array('Interlingue', 'Occidental'),
+ 'ig' => array('Igbo'),
+ 'ii' => array('Sichuan Yi', 'Nuosu'),
+ 'ik' => array('Inupiaq'),
+ 'io' => array('Ido'),
+ 'is' => array('Icelandic'),
+ 'it' => array('Italian'),
+ 'iu' => array('Inuktitut'),
+ 'ja' => array('Japanese'),
+ 'jv' => array('Javanese'),
+ 'ka' => array('Georgian'),
+ 'kg' => array('Kongo'),
+ 'ki' => array('Kikuyu', 'Gikuyu'),
+ 'kj' => array('Kuanyama', 'Kwanyama'),
+ 'kk' => array('Kazakh'),
+ 'kl' => array('Kalaallisut', 'Greenlandic'),
+ 'km' => array('Central Khmer'),
+ 'kn' => array('Kannada'),
+ 'ko' => array('Korean'),
+ 'kr' => array('Kanuri'),
+ 'ks' => array('Kashmiri'),
+ 'ku' => array('Kurdish'),
+ 'kv' => array('Komi'),
+ 'kw' => array('Cornish'),
+ 'ky' => array('Kirghiz', 'Kyrgyz'),
+ 'la' => array('Latin'),
+ 'lb' => array('Luxembourgish', 'Letzeburgesch'),
+ 'lg' => array('Ganda'),
+ 'li' => array('Limburgan', 'Limburger', 'Limburgish'),
+ 'ln' => array('Lingala'),
+ 'lo' => array('Lao'),
+ 'lt' => array('Lithuanian'),
+ 'lu' => array('Luba-Katanga'),
+ 'lv' => array('Latvian'),
+ 'mg' => array('Malagasy'),
+ 'mh' => array('Marshallese'),
+ 'mi' => array('Maori'),
+ 'mk' => array('Macedonian'),
+ 'ml' => array('Malayalam'),
+ 'mn' => array('Mongolian'),
+// 'mo' => array('Moldavian'), // deprecated
+ 'mr' => array('Marathi'),
+ 'ms' => array('Malay'),
+ 'mt' => array('Maltese'),
+ 'my' => array('Burmese'),
+ 'na' => array('Nauru'),
+ 'nb' => array('Norwegian Bokmål'),
+ 'nd' => array('North Ndebele'),
+ 'ne' => array('Nepali'),
+ 'ng' => array('Ndonga'),
+ 'nl' => array('Dutch', 'Flemish'),
+ 'nn' => array('Norwegian Nynorsk'),
+ 'no' => array('Norwegian'),
+ 'nr' => array('South Ndebele'),
+ 'nv' => array('Navajo', 'Navaho'),
+ 'ny' => array('Chichewa', 'Chewa', 'Nyanja'),
+ 'oc' => array('Occitan', 'Provençal'), // Occitan (post 1500)
+ 'oj' => array('Ojibwa'),
+ 'om' => array('Oromo'),
+ 'or' => array('Oriya'),
+ 'os' => array('Ossetian', 'Ossetic'),
+ 'pa' => array('Panjabi', 'Punjabi'),
+ 'pi' => array('Pali'),
+ 'pl' => array('Polish'),
+ 'ps' => array('Pushto', 'Pashto'),
+ 'pt' => array('Portuguese'),
+ 'qu' => array('Quechua'),
+ 'rm' => array('Romansh'),
+ 'rn' => array('Rundi'),
+ 'ro' => array('Romanian', 'Moldavian', 'Moldovan'),
+ 'ru' => array('Russian'),
+ 'rw' => array('Kinyarwanda'),
+ 'sa' => array('Sanskrit'),
+ 'sc' => array('Sardinian'),
+ 'sd' => array('Sindhi'),
+ 'se' => array('Northern Sami'),
+ 'sg' => array('Sango'),
+// 'sh' => array('Serbo-Croatian'), // deprecated
+ 'si' => array('Sinhala', 'Sinhalese'),
+ 'sk' => array('Slovak'),
+ 'sl' => array('Slovenian'),
+ 'sm' => array('Samoan'),
+ 'sn' => array('Shona'),
+ 'so' => array('Somali'),
+ 'sq' => array('Albanian'),
+ 'sr' => array('Serbian'),
+ 'ss' => array('Swati'),
+ 'st' => array('Southern Soth'),
+ 'su' => array('Sundanese'),
+ 'sv' => array('Swedish'),
+ 'sw' => array('Swahili'),
+ 'ta' => array('Tamil'),
+ 'te' => array('Telugu'),
+ 'tg' => array('Tajik'),
+ 'th' => array('Thai'),
+ 'ti' => array('Tigrinya'),
+ 'tk' => array('Turkmen'),
+ 'tl' => array('Tagalog'),
+ 'tn' => array('Tswana'),
+ 'to' => array('Tonga'), // Tonga (Tonga Islands)
+ 'tr' => array('Turkish'),
+ 'ts' => array('Tsonga'),
+ 'tt' => array('Tatar'),
+ 'tw' => array('Twi'),
+ 'ty' => array('Tahitian'),
+ 'ug' => array('Uighur', 'Uyghur'),
+ 'uk' => array('Ukrainian'),
+ 'ur' => array('Urdu'),
+ 'uz' => array('Uzbek'),
+ 've' => array('Venda'),
+ 'vi' => array('Vietnamese'),
+ 'vo' => array('Volapük'),
+ 'wa' => array('Walloon'),
+ 'wo' => array('Wolof'),
+ 'xh' => array('Xhosa'),
+ 'yi' => array('Yiddish'),
+ 'yo' => array('Yoruba'),
+ 'za' => array('Zhuang', 'Chuang'),
+ 'zh' => array('Chinese'),
+ 'zu' => array('Zulu'),
+);
diff --git a/core/Intl/Locale.php b/core/Intl/Locale.php
new file mode 100644
index 0000000000..7c18b727c5
--- /dev/null
+++ b/core/Intl/Locale.php
@@ -0,0 +1,19 @@
+<?php
+
+namespace Piwik\Intl;
+
+class Locale
+{
+ public static function setLocale($locale)
+ {
+ $localeVariant = str_replace('UTF-8', 'UTF8', $locale);
+
+ setlocale(LC_ALL, $locale, $localeVariant);
+ setlocale(LC_CTYPE, '');
+ }
+
+ public static function setDefaultLocale()
+ {
+ self::setLocale('en_US.UTF-8');
+ }
+}
diff --git a/core/Loader.php b/core/Loader.php
deleted file mode 100644
index b89e7743f9..0000000000
--- a/core/Loader.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?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;
-
-/**
- * Initializes the Composer Autoloader
- * @package Piwik
- */
-class Loader
-{
- public static function init()
- {
- return self::getLoader();
- }
-
- /**
- * @return \Composer\Autoload\ClassLoader
- */
- private static function getLoader()
- {
- if (file_exists(PIWIK_INCLUDE_PATH . '/vendor/autoload.php')) {
- $path = PIWIK_INCLUDE_PATH . '/vendor/autoload.php'; // Piwik is the main project
- } else {
- $path = PIWIK_INCLUDE_PATH . '/../../autoload.php'; // Piwik is installed as a dependency
- }
-
- $loader = require $path;
-
- return $loader;
- }
-}
diff --git a/core/Log.php b/core/Log.php
index 1315362c38..260e57fea6 100644
--- a/core/Log.php
+++ b/core/Log.php
@@ -4,12 +4,13 @@
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
*/
+
namespace Piwik;
+use Monolog\Logger;
use Piwik\Container\StaticContainer;
-use Piwik\Db;
+use Psr\Log\LoggerInterface;
/**
* Logging utility class.
@@ -19,9 +20,7 @@ use Piwik\Db;
* the name of the current class is used.
*
* You can log messages using one of the public static functions (eg, 'error', 'warning',
- * 'info', etc.). Messages logged with the **error** level will **always** be logged to
- * the screen, regardless of whether the [log] log_writer config option includes the
- * screen writer.
+ * 'info', etc.).
*
* Currently, Piwik supports the following logging backends:
*
@@ -29,6 +28,8 @@ use Piwik\Db;
* - **file**: logging to a file
* - **database**: logging to Piwik's MySQL database
*
+ * Messages logged in the console will always be logged to the console output.
+ *
* ### Logging configuration
*
* The logging utility can be configured by manipulating the INI config options in the
@@ -43,63 +44,14 @@ use Piwik\Db;
* or **VERBOSE**. Log entries made with a log level that is as or more
* severe than the current log level will be outputted. Others will be
* ignored. The default level is **WARN**.
- * - `log_only_when_cli`: 0 or 1. If 1, logging is only enabled when Piwik is executed
- * in the command line (for example, by the core:archive command
- * script). Default: 0.
- * - `log_only_when_debug_parameter`: 0 or 1. If 1, logging is only enabled when the
- * `debug` query parameter is 1. Default: 0.
* - `logger_file_path`: For the file log writer, specifies the path to the log file
* to log to or a path to a directory to store logs in. If a
* directory, the file name is piwik.log. Can be relative to
* Piwik's root dir or an absolute path. Defaults to **tmp/logs**.
*
- * ### Custom message formatting
- *
- * If you'd like to format log messages differently for different backends, you can use
- * one of the `'Log.format...Message'` events.
- *
- * These events are fired when an object is logged. You can create your own custom class
- * containing the information to log and listen to these events to format it correctly for
- * different backends.
- *
- * If you don't care about the backend when formatting an object, implement a `__toString()`
- * in the custom class.
- *
- * ### Custom log writers
- *
- * New logging backends can be added via the {@hook Log.getAvailableWriters}` event. A log
- * writer is just a callback that accepts log entry information (such as the message,
- * level, etc.), so any backend could conceivably be used (including existing PSR3
- * backends).
- *
- * ### Examples
- *
- * **Basic logging**
- *
- * Log::error("This log message will end up on the screen and in a file.")
- * Log::verbose("This log message uses %s params, but %s will only be called if the"
- * . " configured log level includes %s.", "sprintf", "sprintf", "verbose");
*
- * **Logging objects**
- *
- * class MyDebugInfo
- * {
- * // ...
- *
- * public function __toString()
- * {
- * return // ...
- * }
- * }
- *
- * try {
- * $myThirdPartyServiceClient->doSomething();
- * } catch (Exception $unexpectedError) {
- * $debugInfo = new MyDebugInfo($unexpectedError, $myThirdPartyServiceClient);
- * Log::debug($debugInfo);
- * }
- *
- * @method static \Piwik\Log getInstance()
+ * @deprecated Inject and use Psr\Log\LoggerInterface instead of this class.
+ * @see \Psr\Log\LoggerInterface
*/
class Log extends Singleton
{
@@ -117,78 +69,62 @@ class Log extends Singleton
const LOGGER_FILE_PATH_CONFIG_OPTION = 'logger_file_path';
const STRING_MESSAGE_FORMAT_OPTION = 'string_message_format';
- const FORMAT_FILE_MESSAGE_EVENT = 'Log.formatFileMessage';
-
- const FORMAT_SCREEN_MESSAGE_EVENT = 'Log.formatScreenMessage';
-
- const FORMAT_DATABASE_MESSAGE_EVENT = 'Log.formatDatabaseMessage';
-
- const GET_AVAILABLE_WRITERS_EVENT = 'Log.getAvailableWriters';
-
/**
- * The current logging level. Everything of equal or greater priority will be logged.
- * Everything else will be ignored.
- *
- * @var int
- */
- private $currentLogLevel = self::WARN;
-
- /**
- * The array of callbacks executed when logging to a file. Each callback writes a log
- * message to a logging backend.
- *
- * @var array
- */
- private $writers = array();
-
- /**
- * The log message format string that turns a tag name, date-time and message into
- * one string to log.
+ * The backtrace string to use when testing.
*
* @var string
*/
- private $logMessageFormat = "%level% %tag%[%datetime%] %message%";
+ public static $debugBacktraceForTests;
/**
- * If we're logging to a file, this is the path to the file to log to.
+ * Singleton instance.
*
- * @var string
+ * @var Log
*/
- private $logToFilePath;
+ private static $instance;
/**
- * True if we're currently setup to log to a screen, false if otherwise.
- *
- * @var bool
+ * @var LoggerInterface
*/
- private $loggingToScreen;
+ private $logger;
+
+ public static function getInstance()
+ {
+ if (self::$instance === null) {
+ self::$instance = StaticContainer::get(__CLASS__);
+ }
+ return self::$instance;
+ }
+ public static function unsetInstance()
+ {
+ self::$instance = null;
+ }
+ public static function setSingletonInstance($instance)
+ {
+ self::$instance = $instance;
+ }
/**
- * Constructor.
+ * @param LoggerInterface $logger
*/
- protected function __construct()
+ public function __construct(LoggerInterface $logger)
{
- $logConfig = Config::getInstance()->log;
- $this->setCurrentLogLevelFromConfig($logConfig);
- $this->setLogWritersFromConfig($logConfig);
- $this->setLogFilePathFromConfig($logConfig);
- $this->setStringLogMessageFormat($logConfig);
- $this->disableLoggingBasedOnConfig($logConfig);
+ $this->logger = $logger;
}
/**
* Logs a message using the ERROR log level.
*
- * _Note: Messages logged with the ERROR level are always logged to the screen in addition
- * to configured writers._
- *
* @param string $message The log message. This can be a sprintf format string.
* @param ... mixed Optional sprintf params.
* @api
+ *
+ * @deprecated Inject and call Psr\Log\LoggerInterface::error() instead.
+ * @see \Psr\Log\LoggerInterface::error()
*/
public static function error($message /* ... */)
{
- self::logMessage(self::ERROR, $message, array_slice(func_get_args(), 1));
+ self::logMessage(Logger::ERROR, $message, array_slice(func_get_args(), 1));
}
/**
@@ -197,10 +133,13 @@ class Log extends Singleton
* @param string $message The log message. This can be a sprintf format string.
* @param ... mixed Optional sprintf params.
* @api
+ *
+ * @deprecated Inject and call Psr\Log\LoggerInterface::warning() instead.
+ * @see \Psr\Log\LoggerInterface::warning()
*/
public static function warning($message /* ... */)
{
- self::logMessage(self::WARN, $message, array_slice(func_get_args(), 1));
+ self::logMessage(Logger::WARNING, $message, array_slice(func_get_args(), 1));
}
/**
@@ -209,10 +148,13 @@ class Log extends Singleton
* @param string $message The log message. This can be a sprintf format string.
* @param ... mixed Optional sprintf params.
* @api
+ *
+ * @deprecated Inject and call Psr\Log\LoggerInterface::info() instead.
+ * @see \Psr\Log\LoggerInterface::info()
*/
public static function info($message /* ... */)
{
- self::logMessage(self::INFO, $message, array_slice(func_get_args(), 1));
+ self::logMessage(Logger::INFO, $message, array_slice(func_get_args(), 1));
}
/**
@@ -221,10 +163,13 @@ class Log extends Singleton
* @param string $message The log message. This can be a sprintf format string.
* @param ... mixed Optional sprintf params.
* @api
+ *
+ * @deprecated Inject and call Psr\Log\LoggerInterface::debug() instead.
+ * @see \Psr\Log\LoggerInterface::debug()
*/
public static function debug($message /* ... */)
{
- self::logMessage(self::DEBUG, $message, array_slice(func_get_args(), 1));
+ self::logMessage(Logger::DEBUG, $message, array_slice(func_get_args(), 1));
}
/**
@@ -233,481 +178,66 @@ class Log extends Singleton
* @param string $message The log message. This can be a sprintf format string.
* @param ... mixed Optional sprintf params.
* @api
+ *
+ * @deprecated Inject and call Psr\Log\LoggerInterface::debug() instead (the verbose level doesn't exist in the PSR standard).
+ * @see \Psr\Log\LoggerInterface::debug()
*/
public static function verbose($message /* ... */)
{
- self::logMessage(self::VERBOSE, $message, array_slice(func_get_args(), 1));
+ self::logMessage(Logger::DEBUG, $message, array_slice(func_get_args(), 1));
}
/**
- * Creates log message combining logging info including a log level, tag name,
- * date time, and caller-provided log message. The log message can be set through
- * the `[log] string_message_format` INI config option. By default it will
- * create log messages like:
- *
- * **LEVEL [tag:datetime] log message**
- *
- * @param int $level
- * @param string $tag
- * @param string $datetime
- * @param string $message
- * @return string
+ * @param int $logLevel
+ * @deprecated Will be removed, log levels are now applied on each Monolog handler.
*/
- public function formatMessage($level, $tag, $datetime, $message)
- {
- return str_replace(
- array("%tag%", "%message%", "%datetime%", "%level%"),
- array($tag, trim($message), $datetime, $this->getStringLevel($level)),
- $this->logMessageFormat
- );
- }
-
- private function setLogWritersFromConfig($logConfig)
- {
- // set the log writers
- $logWriters = @$logConfig[self::LOG_WRITERS_CONFIG_OPTION];
- if (empty($logWriters)) {
- return;
- }
-
- $logWriters = array_map('trim', $logWriters);
- foreach ($logWriters as $writerName) {
- $this->addLogWriter($writerName);
- }
- }
-
- public function addLogWriter($writerName)
- {
- if (array_key_exists($writerName, $this->writers)) {
- return;
- }
-
- $availableWritersByName = $this->getAvailableWriters();
-
- if (empty($availableWritersByName[$writerName])) {
- return;
- }
-
- $this->writers[$writerName] = $availableWritersByName[$writerName];
- }
-
- private function setCurrentLogLevelFromConfig($logConfig)
- {
- if (!empty($logConfig[self::LOG_LEVEL_CONFIG_OPTION])) {
- $logLevel = $this->getLogLevelFromStringName($logConfig[self::LOG_LEVEL_CONFIG_OPTION]);
-
- if ($logLevel >= self::NONE // sanity check
- && $logLevel <= self::VERBOSE
- ) {
- $this->setLogLevel($logLevel);
- }
- }
- }
-
- private function setStringLogMessageFormat($logConfig)
- {
- if (isset($logConfig['string_message_format'])) {
- $this->logMessageFormat = $logConfig['string_message_format'];
- }
- }
-
- private function setLogFilePathFromConfig($logConfig)
- {
- $logPath = @$logConfig[self::LOGGER_FILE_PATH_CONFIG_OPTION];
-
- // Absolute path
- if (strpos($logPath, '/') === 0) {
- $this->logToFilePath = $logPath;
- return;
- }
-
- // Remove 'tmp/' at the beginning
- if (strpos($logPath, 'tmp/') === 0) {
- $logPath = substr($logPath, strlen('tmp'));
- }
-
- if (empty($logPath)) {
- $logPath = $this->getDefaultFileLogPath();
- }
-
- $logPath = StaticContainer::getContainer()->get('path.tmp') . $logPath;
- if (is_dir($logPath)) {
- $logPath .= '/piwik.log';
- }
- $this->logToFilePath = $logPath;
- }
-
- private function getDefaultFileLogPath()
- {
- return '/logs/piwik.log';
- }
-
- private function getAvailableWriters()
- {
- $writers = array();
-
- /**
- * This event is called when the Log instance is created. Plugins can use this event to
- * make new logging writers available.
- *
- * A logging writer is a callback with the following signature:
- *
- * function (int $level, string $tag, string $datetime, string $message)
- *
- * `$level` is the log level to use, `$tag` is the log tag used, `$datetime` is the date time
- * of the logging call and `$message` is the formatted log message.
- *
- * Logging writers must be associated by name in the array passed to event handlers. The
- * name specified can be used in Piwik's INI configuration.
- *
- * **Example**
- *
- * public function getAvailableWriters(&$writers) {
- * $writers['myloggername'] = function ($level, $tag, $datetime, $message) {
- * // ...
- * };
- * }
- *
- * // 'myloggername' can now be used in the log_writers config option.
- *
- * @param array $writers Array mapping writer names with logging writers.
- */
- Piwik::postEvent(self::GET_AVAILABLE_WRITERS_EVENT, array(&$writers));
-
- $writers['file'] = array($this, 'logToFile');
- $writers['screen'] = array($this, 'logToScreen');
- $writers['database'] = array($this, 'logToDatabase');
- return $writers;
- }
-
public function setLogLevel($logLevel)
{
- $this->currentLogLevel = $logLevel;
}
+ /**
+ * @deprecated Will be removed, log levels are now applied on each Monolog handler.
+ */
public function getLogLevel()
{
- return $this->currentLogLevel;
}
- private function logToFile($level, $tag, $datetime, $message)
+ private function doLog($level, $message, $parameters = array())
{
- $message = $this->getMessageFormattedFile($level, $tag, $datetime, $message);
- if (empty($message)) {
- return;
- }
-
- if (!@file_put_contents($this->logToFilePath, $message, FILE_APPEND)
- && !defined('PIWIK_TEST_MODE')
- ) {
- $message = Filechecks::getErrorMessageMissingPermissions($this->logToFilePath);
- throw new \Exception($message);
+ // To ensure the compatibility with PSR-3, the message must be a string
+ if ($message instanceof \Exception) {
+ $parameters['exception'] = $message;
+ $message = $message->getMessage();
}
- }
-
- private function logToScreen($level, $tag, $datetime, $message)
- {
- $message = $this->getMessageFormattedScreen($level, $tag, $datetime, $message);
- if (empty($message)) {
- return;
+ if (! is_string($message)) {
+ throw new \InvalidArgumentException('Trying to log a message that is not a string');
}
- echo $message;
+ $this->logger->log($level, $message, $parameters);
}
- private function logToDatabase($level, $tag, $datetime, $message)
+ private static function logMessage($level, $message, $parameters)
{
- $message = $this->getMessageFormattedDatabase($level, $tag, $datetime, $message);
- if (empty($message)) {
- return;
- }
-
- $sql = "INSERT INTO " . Common::prefixTable('logger_message')
- . " (tag, timestamp, level, message)"
- . " VALUES (?, ?, ?, ?)";
- Db::query($sql, array($tag, $datetime, self::getStringLevel($level), (string)$message));
+ self::getInstance()->doLog($level, $message, $parameters);
}
- private function doLog($level, $message, $sprintfParams = array())
+ public static function getMonologLevel($level)
{
- if (!$this->shouldLoggerLog($level)) {
- return;
- }
-
- $datetime = date("Y-m-d H:i:s");
- if (is_string($message)
- && !empty($sprintfParams)
- ) {
- // handle array sprintf parameters
- foreach ($sprintfParams as &$param) {
- if (is_array($param)) {
- $param = json_encode($param);
- }
- }
-
- $message = vsprintf($message, $sprintfParams);
- }
-
- if (version_compare(phpversion(), '5.3.6', '>=')) {
- $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS | DEBUG_BACKTRACE_PROVIDE_OBJECT);
- } else {
- $backtrace = debug_backtrace();
- }
- $tag = Plugin::getPluginNameFromBacktrace($backtrace);
-
- // if we can't determine the plugin, use the name of the calling class
- if ($tag == false) {
- $tag = $this->getClassNameThatIsLogging($backtrace);
- }
-
- $this->writeMessage($level, $tag, $datetime, $message);
- }
-
- private function writeMessage($level, $tag, $datetime, $message)
- {
- foreach ($this->writers as $writer) {
- call_user_func($writer, $level, $tag, $datetime, $message);
- }
-
- if ($level == self::ERROR) {
- $message = $this->getMessageFormattedScreen($level, $tag, $datetime, $message);
- $this->writeErrorToStandardErrorOutput($message);
- if (!isset($this->writers['screen'])) {
- echo $message;
- }
- }
- }
-
- private static function logMessage($level, $message, $sprintfParams)
- {
- self::getInstance()->doLog($level, $message, $sprintfParams);
- }
-
- private function shouldLoggerLog($level)
- {
- return $level <= $this->currentLogLevel;
- }
-
- private function disableLoggingBasedOnConfig($logConfig)
- {
- $disableLogging = false;
-
- if (!empty($logConfig['log_only_when_cli'])
- && !Common::isPhpCliMode()
- ) {
- $disableLogging = true;
- }
-
- if (!empty($logConfig['log_only_when_debug_parameter'])
- && !isset($_REQUEST['debug'])
- ) {
- $disableLogging = true;
- }
-
- if ($disableLogging) {
- $this->currentLogLevel = self::NONE;
- }
- }
-
- private function getLogLevelFromStringName($name)
- {
- $name = strtoupper($name);
- switch ($name) {
- case 'NONE':
- return self::NONE;
- case 'ERROR':
- return self::ERROR;
- case 'WARN':
- return self::WARN;
- case 'INFO':
- return self::INFO;
- case 'DEBUG':
- return self::DEBUG;
- case 'VERBOSE':
- return self::VERBOSE;
+ switch ($level) {
+ case self::ERROR:
+ return Logger::ERROR;
+ case self::WARN:
+ return Logger::WARNING;
+ case self::INFO:
+ return Logger::INFO;
+ case self::DEBUG:
+ return Logger::DEBUG;
+ case self::VERBOSE:
+ return Logger::DEBUG;
+ case self::NONE:
default:
- return -1;
- }
- }
-
- private function getStringLevel($level)
- {
- static $levelToName = array(
- self::NONE => 'NONE',
- self::ERROR => 'ERROR',
- self::WARN => 'WARN',
- self::INFO => 'INFO',
- self::DEBUG => 'DEBUG',
- self::VERBOSE => 'VERBOSE'
- );
- return $levelToName[$level];
- }
-
- private function getClassNameThatIsLogging($backtrace)
- {
- foreach ($backtrace as $tracepoint) {
- if (isset($tracepoint['class'])
- && $tracepoint['class'] != "Piwik\\Log"
- && $tracepoint['class'] != "Piwik\\Piwik"
- && $tracepoint['class'] != "Piwik\\CronArchive"
- ) {
- return $tracepoint['class'];
- }
- }
- return false;
- }
-
- /**
- * @param $level
- * @param $tag
- * @param $datetime
- * @param $message
- * @return string
- */
- private function getMessageFormattedScreen($level, $tag, $datetime, $message)
- {
- static $currentRequestKey;
- if (empty($currentRequestKey)) {
- $currentRequestKey = substr(Common::generateUniqId(), 0, 5);
- }
-
- if (is_string($message)) {
- if (!defined('PIWIK_TEST_MODE')) {
- $message = '[' . $currentRequestKey . '] ' . $message;
- }
- $message = $this->formatMessage($level, $tag, $datetime, $message);
-
- if (!Common::isPhpCliMode()) {
- $message = Common::sanitizeInputValue($message);
- $message = '<pre>' . $message . '</pre>';
- }
- } else {
- $logger = $this;
-
- /**
- * Triggered when trying to log an object to the screen. Plugins can use
- * this event to convert objects to strings before they are logged.
- *
- * The result of this callback can be HTML so no sanitization is done on the result.
- * This means **YOU MUST SANITIZE THE MESSAGE YOURSELF** if you use this event.
- *
- * **Example**
- *
- * public function formatScreenMessage(&$message, $level, $tag, $datetime, $logger) {
- * if ($message instanceof MyCustomDebugInfo) {
- * $message = Common::sanitizeInputValue($message->formatForScreen());
- * }
- * }
- *
- * @param mixed &$message The object that is being logged. Event handlers should
- * check if the object is of a certain type and if it is,
- * set `$message` to the string that should be logged.
- * @param int $level The log level used with this log entry.
- * @param string $tag The current plugin that started logging (or if no plugin,
- * the current class).
- * @param string $datetime Datetime of the logging call.
- * @param Log $logger The Log singleton.
- */
- Piwik::postEvent(self::FORMAT_SCREEN_MESSAGE_EVENT, array(&$message, $level, $tag, $datetime, $logger));
+ // Highest level possible, need to do better in the future...
+ return Logger::EMERGENCY;
}
- $message = trim($message);
- return $message . "\n";
- }
-
- /**
- * @param $message
- */
- private function writeErrorToStandardErrorOutput($message)
- {
- if (defined('PIWIK_TEST_MODE')) {
- // do not log on stderr during tests (prevent display of errors in CI output)
- return;
- }
- $fe = fopen('php://stderr', 'w');
- fwrite($fe, $message);
- }
-
- /**
- * @param $level
- * @param $tag
- * @param $datetime
- * @param $message
- * @return string
- */
- private function getMessageFormattedDatabase($level, $tag, $datetime, $message)
- {
- if (is_string($message)) {
- $message = $this->formatMessage($level, $tag, $datetime, $message);
- } else {
- $logger = $this;
-
- /**
- * Triggered when trying to log an object to a database table. Plugins can use
- * this event to convert objects to strings before they are logged.
- *
- * **Example**
- *
- * public function formatDatabaseMessage(&$message, $level, $tag, $datetime, $logger) {
- * if ($message instanceof MyCustomDebugInfo) {
- * $message = $message->formatForDatabase();
- * }
- * }
- *
- * @param mixed &$message The object that is being logged. Event handlers should
- * check if the object is of a certain type and if it is,
- * set `$message` to the string that should be logged.
- * @param int $level The log level used with this log entry.
- * @param string $tag The current plugin that started logging (or if no plugin,
- * the current class).
- * @param string $datetime Datetime of the logging call.
- * @param Log $logger The Log singleton.
- */
- Piwik::postEvent(self::FORMAT_DATABASE_MESSAGE_EVENT, array(&$message, $level, $tag, $datetime, $logger));
- }
- $message = trim($message);
- return $message;
- }
-
- /**
- * @param $level
- * @param $tag
- * @param $datetime
- * @param $message
- * @return string
- */
- private function getMessageFormattedFile($level, $tag, $datetime, $message)
- {
- if (is_string($message)) {
- $message = $this->formatMessage($level, $tag, $datetime, $message);
- } else {
- $logger = $this;
-
- /**
- * Triggered when trying to log an object to a file. Plugins can use
- * this event to convert objects to strings before they are logged.
- *
- * **Example**
- *
- * public function formatFileMessage(&$message, $level, $tag, $datetime, $logger) {
- * if ($message instanceof MyCustomDebugInfo) {
- * $message = $message->formatForFile();
- * }
- * }
- *
- * @param mixed &$message The object that is being logged. Event handlers should
- * check if the object is of a certain type and if it is,
- * set `$message` to the string that should be logged.
- * @param int $level The log level used with this log entry.
- * @param string $tag The current plugin that started logging (or if no plugin,
- * the current class).
- * @param string $datetime Datetime of the logging call.
- * @param Log $logger The Log singleton.
- */
- Piwik::postEvent(self::FORMAT_FILE_MESSAGE_EVENT, array(&$message, $level, $tag, $datetime, $logger));
- }
-
- $message = trim($message);
- $message = str_replace("\n", "\n ", $message);
- return $message . "\n";
}
}
diff --git a/core/Mail.php b/core/Mail.php
index 1e9ef0bb52..9825eb3378 100644
--- a/core/Mail.php
+++ b/core/Mail.php
@@ -8,7 +8,9 @@
*/
namespace Piwik;
+use Piwik\Container\StaticContainer;
use Piwik\Plugins\CoreAdminHome\CustomLogo;
+use Piwik\Translation\Translator;
use Zend_Mail;
/**
@@ -35,10 +37,13 @@ class Mail extends Zend_Mail
{
$customLogo = new CustomLogo();
+ /** @var Translator $translator */
+ $translator = StaticContainer::get('Piwik\Translation\Translator');
+
if ($customLogo->isEnabled()) {
- $fromEmailName = Piwik::translate('CoreHome_WebAnalyticsReports');
+ $fromEmailName = $translator->translate('CoreHome_WebAnalyticsReports');
} else {
- $fromEmailName = Piwik::translate('ScheduledReports_PiwikReports');
+ $fromEmailName = $translator->translate('ScheduledReports_PiwikReports');
}
$fromEmailAddress = Config::getInstance()->General['noreply_email_address'];
@@ -105,7 +110,8 @@ class Mail extends Zend_Mail
$smtpConfig['ssl'] = $mailConfig['encryption'];
}
- $tr = new \Zend_Mail_Transport_Smtp($mailConfig['host'], $smtpConfig);
+ $host = trim($mailConfig['host']);
+ $tr = new \Zend_Mail_Transport_Smtp($host, $smtpConfig);
Mail::setDefaultTransport($tr);
@ini_set("smtp_port", $mailConfig['port']);
}
diff --git a/core/Menu/MenuAdmin.php b/core/Menu/MenuAdmin.php
index 13e8480596..5335091285 100644
--- a/core/Menu/MenuAdmin.php
+++ b/core/Menu/MenuAdmin.php
@@ -117,7 +117,7 @@ class MenuAdmin extends MenuAbstract
*/
public function addManageItem($menuName, $url, $order = 50, $tooltip = false)
{
- $this->addItem('CoreAdminHome_MenuManage', $menuName, $url, $order, $tooltip);
+ $this->addItem('CoreAdminHome_Administration', $menuName, $url, $order, $tooltip);
}
/**
@@ -144,29 +144,6 @@ class MenuAdmin extends MenuAbstract
}
/**
- * Returns the current AdminMenu name
- *
- * @return boolean
- */
- public function getCurrentAdminMenuName()
- {
- $menu = MenuAdmin::getInstance()->getMenu();
- $currentModule = Piwik::getModule();
- $currentAction = Piwik::getAction();
- foreach ($menu as $submenu) {
- foreach ($submenu as $subMenuName => $parameters) {
- if (strpos($subMenuName, '_') !== 0 &&
- $parameters['_url']['module'] == $currentModule
- && $parameters['_url']['action'] == $currentAction
- ) {
- return $subMenuName;
- }
- }
- }
- return false;
- }
-
- /**
* @deprecated since version 2.4.0. See {@link Piwik\Plugin\Menu} for new implementation.
*/
public static function removeEntry($menuName, $subMenuName = false)
diff --git a/core/Menu/MenuUser.php b/core/Menu/MenuUser.php
index 758ac3d578..ac3bc295ab 100755
--- a/core/Menu/MenuUser.php
+++ b/core/Menu/MenuUser.php
@@ -40,6 +40,20 @@ class MenuUser extends MenuAbstract
* @api
* @since 2.5.0
*/
+ public function addPersonalItem($menuName, $url, $order = 50, $tooltip = false)
+ {
+ $this->addItem('UsersManager_MenuPersonal', $menuName, $url, $order, $tooltip);
+ }
+
+ /**
+ * See {@link add()}. Adds a new menu item to the manage section of the user menu.
+ * @param string $menuName
+ * @param array $url
+ * @param int $order
+ * @param bool|string $tooltip
+ * @api
+ * @since 2.5.0
+ */
public function addManageItem($menuName, $url, $order = 50, $tooltip = false)
{
$this->addItem('CoreAdminHome_MenuManage', $menuName, $url, $order, $tooltip);
diff --git a/core/Metrics.php b/core/Metrics.php
index 009fa36151..4b48bf13f2 100644
--- a/core/Metrics.php
+++ b/core/Metrics.php
@@ -8,8 +8,7 @@
*/
namespace Piwik;
-use Piwik\Cache\LanguageAwareStaticCache;
-use Piwik\Cache\PluginAwareStaticCache;
+use Piwik\Cache as PiwikCache;
use Piwik\Metrics\Formatter;
require_once PIWIK_INCLUDE_PATH . "/core/Piwik.php";
@@ -87,6 +86,9 @@ class Metrics
const INDEX_CONTENT_NB_IMPRESSIONS = 41;
const INDEX_CONTENT_NB_INTERACTIONS = 42;
+ // Unique visitors fingerprints (useful to process unique visitors across websites)
+ const INDEX_NB_UNIQ_FINGERPRINTS = 43;
+
// Goal reports
const INDEX_GOAL_NB_CONVERSIONS = 1;
const INDEX_GOAL_REVENUE = 2;
@@ -99,6 +101,7 @@ class Metrics
public static $mappingFromIdToName = array(
Metrics::INDEX_NB_UNIQ_VISITORS => 'nb_uniq_visitors',
+ Metrics::INDEX_NB_UNIQ_FINGERPRINTS => 'nb_uniq_fingerprints',
Metrics::INDEX_NB_VISITS => 'nb_visits',
Metrics::INDEX_NB_ACTIONS => 'nb_actions',
Metrics::INDEX_NB_USERS => 'nb_users',
@@ -250,10 +253,11 @@ class Metrics
public static function getDefaultMetricTranslations()
{
- $cache = new PluginAwareStaticCache('DefaultMetricTranslations');
+ $cacheId = CacheId::pluginAware('DefaultMetricTranslations');
+ $cache = PiwikCache::getTransientCache();
- if ($cache->has()) {
- return $cache->get();
+ if ($cache->contains($cacheId)) {
+ return $cache->fetch($cacheId);
}
$translations = array(
@@ -302,17 +306,18 @@ class Metrics
$translations = array_map(array('\\Piwik\\Piwik','translate'), $translations);
- $cache->set($translations);
+ $cache->save($cacheId, $translations);
return $translations;
}
public static function getDefaultMetrics()
{
- $cache = new LanguageAwareStaticCache('DefaultMetrics');
+ $cacheId = CacheId::languageAware('DefaultMetrics');
+ $cache = PiwikCache::getTransientCache();
- if ($cache->has()) {
- return $cache->get();
+ if ($cache->contains($cacheId)) {
+ return $cache->fetch($cacheId);
}
$translations = array(
@@ -323,17 +328,18 @@ class Metrics
);
$translations = array_map(array('\\Piwik\\Piwik','translate'), $translations);
- $cache->set($translations);
+ $cache->save($cacheId, $translations);
return $translations;
}
public static function getDefaultProcessedMetrics()
{
- $cache = new LanguageAwareStaticCache('DefaultProcessedMetrics');
+ $cacheId = CacheId::languageAware('DefaultProcessedMetrics');
+ $cache = PiwikCache::getTransientCache();
- if ($cache->has()) {
- return $cache->get();
+ if ($cache->contains($cacheId)) {
+ return $cache->fetch($cacheId);
}
$translations = array(
@@ -345,7 +351,7 @@ class Metrics
);
$translations = array_map(array('\\Piwik\\Piwik','translate'), $translations);
- $cache->set($translations);
+ $cache->save($cacheId, $translations);
return $translations;
}
@@ -383,10 +389,11 @@ class Metrics
public static function getDefaultMetricsDocumentation()
{
- $cache = new PluginAwareStaticCache('DefaultMetricsDocumentation');
+ $cacheId = CacheId::pluginAware('DefaultMetricsDocumentation');
+ $cache = PiwikCache::getTransientCache();
- if ($cache->has()) {
- return $cache->get();
+ if ($cache->contains($cacheId)) {
+ return $cache->fetch($cacheId);
}
$translations = array(
@@ -412,7 +419,7 @@ class Metrics
$translations = array_map(array('\\Piwik\\Piwik','translate'), $translations);
- $cache->set($translations);
+ $cache->save($cacheId, $translations);
return $translations;
}
diff --git a/core/Metrics/Formatter.php b/core/Metrics/Formatter.php
index 0e5bbe719e..bce37e3be4 100644
--- a/core/Metrics/Formatter.php
+++ b/core/Metrics/Formatter.php
@@ -8,7 +8,9 @@
namespace Piwik\Metrics;
use Piwik\Common;
+use Piwik\Container\StaticContainer;
use Piwik\DataTable;
+use Piwik\Intl\Data\Provider\CurrencyDataProvider;
use Piwik\Piwik;
use Piwik\Plugin\Metric;
use Piwik\Plugin\ProcessedMetric;
@@ -19,8 +21,6 @@ use Piwik\Tracker\GoalManager;
/**
* Contains methods to format metric values. Passed to the {@link \Piwik\Plugin\Metric::format()}
* method when formatting Metrics.
- *
- * @api
*/
class Formatter
{
@@ -36,6 +36,7 @@ class Formatter
*
* @param number $value
* @return string
+ * @api
*/
public function getPrettyNumber($value, $precision = 0)
{
@@ -56,6 +57,7 @@ class Formatter
* @param bool $displayTimeAsSentence If set to true, will output `"5min 17s"`, if false `"00:05:17"`.
* @param bool $round Whether to round to the nearest second or not.
* @return string
+ * @api
*/
public function getPrettyTimeFromSeconds($numberOfSeconds, $displayTimeAsSentence = false, $round = false)
{
@@ -124,6 +126,7 @@ class Formatter
* @param string $unit The specific unit to use, if any. If null, the unit is determined by $size.
* @param int $precision The precision to use when rounding.
* @return string eg, `'128 M'` or `'256 K'`.
+ * @api
*/
public function getPrettySizeFromBytes($size, $unit = null, $precision = 1)
{
@@ -131,18 +134,8 @@ class Formatter
return '0 M';
}
- $units = array('B', 'K', 'M', 'G', 'T');
-
- $currentUnit = null;
- foreach ($units as $idx => $currentUnit) {
- if ($size >= 1024 && $unit != $currentUnit && $idx != count($units) - 1) {
- $size = $size / 1024;
- } else {
- break;
- }
- }
-
- return round($size, $precision) . " " . $currentUnit;
+ list($size, $sizeUnit) = $this->getPrettySizeFromBytesWithUnit($size, $unit, $precision);
+ return $size . " " . $sizeUnit;
}
/**
@@ -151,6 +144,7 @@ class Formatter
* @param int|string $value The monetary value to format.
* @param int $idSite The ID of the site whose currency will be used.
* @return string
+ * @api
*/
public function getPrettyMoney($value, $idSite)
{
@@ -192,6 +186,7 @@ class Formatter
*
* @param float $value
* @return string
+ * @api
*/
public function getPrettyPercentFromQuotient($value)
{
@@ -204,6 +199,7 @@ class Formatter
*
* @param int $idSite The ID of the site to return the currency symbol for.
* @return string eg, `'$'`.
+ * @api
*/
public static function getCurrencySymbol($idSite)
{
@@ -222,17 +218,16 @@ class Formatter
*
* @return array An array mapping currency codes to their respective currency symbols
* and a description, eg, `array('USD' => array('$', 'US dollar'))`.
+ *
+ * @deprecated Use Piwik\Intl\Data\Provider\CurrencyDataProvider instead.
+ * @see \Piwik\Intl\Data\Provider\CurrencyDataProvider::getCurrencyList()
+ * @api
*/
public static function getCurrencyList()
{
- static $currenciesList = null;
-
- if (is_null($currenciesList)) {
- require_once PIWIK_INCLUDE_PATH . '/core/DataFiles/Currencies.php';
- $currenciesList = $GLOBALS['Piwik_CurrencyList'];
- }
-
- return $currenciesList;
+ /** @var CurrencyDataProvider $dataProvider */
+ $dataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\CurrencyDataProvider');
+ return $dataProvider->getCurrencyList();
}
/**
@@ -242,6 +237,7 @@ class Formatter
* @param DataTable $dataTable The table to format metrics for.
* @param Report|null $report The report the table belongs to.
* @param string[]|null $metricsToFormat Whitelist of names of metrics to format.
+ * @api
*/
public function formatMetrics(DataTable $dataTable, Report $report = null, $metricsToFormat = null)
{
@@ -280,6 +276,29 @@ class Formatter
}
}
+ protected function getPrettySizeFromBytesWithUnit($size, $unit = null, $precision = 1)
+ {
+ $units = array('B', 'K', 'M', 'G', 'T');
+ $numUnits = count($units) - 1;
+
+ $currentUnit = null;
+ foreach ($units as $idx => $currentUnit) {
+ if ($unit && $unit !== $currentUnit) {
+ $size = $size / 1024;
+ } elseif ($unit && $unit === $currentUnit) {
+ break;
+ } elseif ($size >= 1024 && $idx != $numUnits) {
+ $size = $size / 1024;
+ } else {
+ break;
+ }
+ }
+
+ $size = round($size, $precision);
+
+ return array($size, $currentUnit);
+ }
+
private function makeRegexToMatchMetrics($metricsToFormat)
{
$metricsRegexParts = array();
diff --git a/core/Notification/Manager.php b/core/Notification/Manager.php
index 921dc3fb82..402f1aeaf0 100644
--- a/core/Notification/Manager.php
+++ b/core/Notification/Manager.php
@@ -9,6 +9,7 @@
namespace Piwik\Notification;
use Piwik\Notification;
+use Piwik\Session;
use Piwik\Session\SessionNamespace;
/**
@@ -103,12 +104,20 @@ class Manager
private static function addNotification($id, Notification $notification)
{
+ if (!self::isEnabled()) {
+ return;
+ }
+
$session = static::getSession();
$session->notifications[$id] = $notification;
}
private static function getAllNotifications()
{
+ if (!self::isEnabled()) {
+ return array();
+ }
+
$session = static::getSession();
return $session->notifications;
@@ -116,12 +125,21 @@ class Manager
private static function removeNotification($id)
{
+ if (!self::isEnabled()) {
+ return;
+ }
+
$session = static::getSession();
if (array_key_exists($id, $session->notifications)) {
unset($session->notifications[$id]);
}
}
+ private static function isEnabled()
+ {
+ return Session::isWritable() && Session::isReadable();
+ }
+
/**
* @return SessionNamespace
*/
@@ -131,7 +149,7 @@ class Manager
static::$session = new SessionNamespace('notification');
}
- if (empty(static::$session->notifications)) {
+ if (empty(static::$session->notifications) && self::isEnabled()) {
static::$session->notifications = array();
}
diff --git a/core/Period.php b/core/Period.php
index eb3c0fda89..d4044ed68e 100644
--- a/core/Period.php
+++ b/core/Period.php
@@ -8,8 +8,10 @@
*/
namespace Piwik;
+use Piwik\Container\StaticContainer;
use Piwik\Period\Factory as PeriodFactory;
use Piwik\Period\Range;
+use Piwik\Translation\Translator;
/**
* Date range representation.
@@ -30,7 +32,7 @@ abstract class Period
{
/**
* Array of subperiods
- * @var \Piwik\Period[]
+ * @var Period[]
*/
protected $subperiods = array();
protected $subperiodsProcessed = false;
@@ -46,6 +48,11 @@ abstract class Period
protected $date = null;
/**
+ * @var Translator
+ */
+ protected $translator;
+
+ /**
* Constructor.
*
* @param Date $date
@@ -54,6 +61,8 @@ abstract class Period
public function __construct(Date $date)
{
$this->date = clone $date;
+
+ $this->translator = StaticContainer::get('Piwik\Translation\Translator');
}
/**
diff --git a/core/Period/Day.php b/core/Period/Day.php
index b8212844cd..551e5e65aa 100644
--- a/core/Period/Day.php
+++ b/core/Period/Day.php
@@ -38,7 +38,7 @@ class Day extends Period
{
//"Mon 15 Aug"
$date = $this->getDateStart();
- $template = Piwik::translate('CoreHome_ShortDateFormat');
+ $template = $this->translator->translate('CoreHome_ShortDateFormat');
$out = $date->getLocalized($template);
return $out;
@@ -53,7 +53,7 @@ class Day extends Period
{
//"Mon 15 Aug"
$date = $this->getDateStart();
- $template = Piwik::translate('CoreHome_DateFormat');
+ $template = $this->translator->translate('CoreHome_DateFormat');
$out = $date->getLocalized($template);
return $out;
diff --git a/core/Period/Month.php b/core/Period/Month.php
index 4609180980..252d394db2 100644
--- a/core/Period/Month.php
+++ b/core/Period/Month.php
@@ -25,7 +25,7 @@ class Month extends Period
public function getLocalizedShortString()
{
//"Aug 09"
- $out = $this->getDateStart()->getLocalized(Piwik::translate('CoreHome_ShortMonthFormat'));
+ $out = $this->getDateStart()->getLocalized($this->translator->translate('CoreHome_ShortMonthFormat'));
return $out;
}
@@ -37,7 +37,7 @@ class Month extends Period
public function getLocalizedLongString()
{
//"August 2009"
- $out = $this->getDateStart()->getLocalized(Piwik::translate('CoreHome_LongMonthFormat'));
+ $out = $this->getDateStart()->getLocalized($this->translator->translate('CoreHome_LongMonthFormat'));
return $out;
}
diff --git a/core/Period/Range.php b/core/Period/Range.php
index 36692dc0de..77d7afdcbd 100644
--- a/core/Period/Range.php
+++ b/core/Period/Range.php
@@ -9,7 +9,9 @@
namespace Piwik\Period;
use Exception;
+use Piwik\Cache;
use Piwik\Common;
+use Piwik\Container\StaticContainer;
use Piwik\Date;
use Piwik\Period;
use Piwik\Piwik;
@@ -33,6 +35,11 @@ class Range extends Period
protected $today;
/**
+ * @var null|Date
+ */
+ protected $defaultEndDate;
+
+ /**
* Constructor.
*
* @param string $strPeriod The type of period each subperiod is. Either `'day'`, `'week'`,
@@ -54,6 +61,43 @@ class Range extends Period
}
$this->today = $today;
+
+ $this->translator = StaticContainer::get('Piwik\Translation\Translator');
+ }
+
+ private function getCache()
+ {
+ return Cache::getTransientCache();
+ }
+
+ private function getCacheId()
+ {
+ $end = '';
+ if ($this->defaultEndDate) {
+ $end = $this->defaultEndDate->getTimestamp();
+ }
+
+ $today = $this->today->getTimestamp();
+
+ return 'range' . $this->strPeriod . $this->strDate . $this->timezone . $end . $today;
+ }
+
+ private function loadAllFromCache()
+ {
+ $range = $this->getCache()->fetch($this->getCacheId());
+
+ if (!empty($range)) {
+ foreach ($range as $key => $val) {
+ $this->$key = $val;
+ }
+ }
+ }
+
+ private function cacheAll()
+ {
+ $props = get_object_vars($this);
+
+ $this->getCache()->save($this->getCacheId(), $props);
}
/**
@@ -66,7 +110,7 @@ class Range extends Period
//"30 Dec 08 - 26 Feb 09"
$dateStart = $this->getDateStart();
$dateEnd = $this->getDateEnd();
- $template = Piwik::translate('CoreHome_ShortDateFormatWithYear');
+ $template = $this->translator->translate('CoreHome_ShortDateFormatWithYear');
$shortDateStart = $dateStart->getLocalized($template);
$shortDateEnd = $dateEnd->getLocalized($template);
@@ -109,7 +153,7 @@ class Range extends Period
*/
public function getPrettyString()
{
- $out = Piwik::translate('General_DateRangeFromTo', array($this->getDateStart()->toString(), $this->getDateEnd()->toString()));
+ $out = $this->translator->translate('General_DateRangeFromTo', array($this->getDateStart()->toString(), $this->getDateEnd()->toString()));
return $out;
}
@@ -156,6 +200,12 @@ class Range extends Period
return;
}
+ $this->loadAllFromCache();
+
+ if ($this->subperiodsProcessed) {
+ return;
+ }
+
parent::generate();
if (preg_match('/(last|previous)([0-9]*)/', $this->strDate, $regs)) {
@@ -186,7 +236,9 @@ class Range extends Period
// last1 means only one result ; last2 means 2 results so we remove only 1 to the days/weeks/etc
$lastN--;
- $lastN = abs($lastN);
+ if ($lastN < 0) {
+ $lastN = 0;
+ }
$startDate = $endDate->addPeriod(-1 * $lastN, $period);
@@ -195,11 +247,6 @@ class Range extends Period
$strDateEnd = $dateRange[2];
$startDate = Date::factory($strDateStart);
- if ($strDateEnd == 'today') {
- $strDateEnd = 'now';
- } elseif ($strDateEnd == 'yesterday') {
- $strDateEnd = 'yesterdaySameTime';
- }
// we set the timezone in the Date object only if the date is relative eg. 'today', 'yesterday', 'now'
$timezone = null;
if (strpos($strDateEnd, '-') === false) {
@@ -207,11 +254,12 @@ class Range extends Period
}
$endDate = Date::factory($strDateEnd, $timezone);
} else {
- throw new Exception(Piwik::translate('General_ExceptionInvalidDateRange', array($this->strDate, ' \'lastN\', \'previousN\', \'YYYY-MM-DD,YYYY-MM-DD\'')));
+ throw new Exception($this->translator->translate('General_ExceptionInvalidDateRange', array($this->strDate, ' \'lastN\', \'previousN\', \'YYYY-MM-DD,YYYY-MM-DD\'')));
}
if ($this->strPeriod != 'range') {
$this->fillArraySubPeriods($startDate, $endDate, $this->strPeriod);
+ $this->cacheAll();
return;
}
@@ -219,6 +267,7 @@ class Range extends Period
// When period=range, we want End Date to be the actual specified end date,
// rather than the end of the month / week / whatever is used for processing this range
$this->endDate = $endDate;
+ $this->cacheAll();
}
/**
@@ -460,4 +509,17 @@ class Range extends Period
return $isEndOfWeekLaterThanEndDate;
}
+
+ /**
+ * Returns the date range string comprising two dates
+ *
+ * @return string eg, `'2012-01-01,2012-01-31'`.
+ */
+ public function getRangeString()
+ {
+ $dateStart = $this->getDateStart();
+ $dateEnd = $this->getDateEnd();
+
+ return $dateStart->toString("Y-m-d") . "," . $dateEnd->toString("Y-m-d");
+ }
}
diff --git a/core/Period/Week.php b/core/Period/Week.php
index 23f7ab32b6..a8af2fbed8 100644
--- a/core/Period/Week.php
+++ b/core/Period/Week.php
@@ -28,7 +28,7 @@ class Week extends Period
$dateStart = $this->getDateStart();
$dateEnd = $this->getDateEnd();
- $string = Piwik::translate('CoreHome_ShortWeekFormat');
+ $string = $this->translator->translate('CoreHome_ShortWeekFormat');
$string = self::getTranslatedRange($string, $dateStart, $dateEnd);
return $string;
}
@@ -40,10 +40,10 @@ class Week extends Period
*/
public function getLocalizedLongString()
{
- $format = Piwik::translate('CoreHome_LongWeekFormat');
+ $format = $this->translator->translate('CoreHome_LongWeekFormat');
$string = self::getTranslatedRange($format, $this->getDateStart(), $this->getDateEnd());
- return Piwik::translate('CoreHome_PeriodWeek') . " " . $string;
+ return $this->translator->translate('CoreHome_PeriodWeek') . " " . $string;
}
/**
@@ -73,7 +73,7 @@ class Week extends Period
$dateStart = $this->getDateStart();
$dateEnd = $this->getDateEnd();
- $out = Piwik::translate('General_DateRangeFromTo', array($dateStart->toString(), $dateEnd->toString()));
+ $out = $this->translator->translate('General_DateRangeFromTo', array($dateStart->toString(), $dateEnd->toString()));
return $out;
}
diff --git a/core/Piwik.php b/core/Piwik.php
index bc5244dd52..c846acec82 100644
--- a/core/Piwik.php
+++ b/core/Piwik.php
@@ -9,6 +9,7 @@
namespace Piwik;
use Exception;
+use Piwik\Container\StaticContainer;
use Piwik\Db\Adapter;
use Piwik\Db\Schema;
use Piwik\Db;
@@ -16,6 +17,7 @@ use Piwik\Plugin;
use Piwik\Plugins\UsersManager\API as APIUsersManager;
use Piwik\Session;
use Piwik\Tracker;
+use Piwik\Translation\Translator;
use Piwik\View;
/**
@@ -299,8 +301,10 @@ class Piwik
public static function hasUserSuperUserAccess()
{
try {
- self::checkUserHasSuperUserAccess();
- return true;
+ $hasAccess = Access::getInstance()->hasSuperUserAccess();
+
+ return $hasAccess;
+
} catch (Exception $e) {
return false;
}
@@ -485,7 +489,7 @@ class Piwik
*/
public static function getLoginPluginName()
{
- return Registry::get('auth')->getName();
+ return StaticContainer::get('Piwik\Auth')->getName();
}
/**
@@ -499,7 +503,7 @@ class Piwik
}
/**
- * Returns the current module read from the URL (eg. 'API', 'UserSettings', etc.)
+ * Returns the current module read from the URL (eg. 'API', 'DevicesDetection', etc.)
*
* @return string
*/
@@ -589,7 +593,7 @@ class Piwik
) {
return;
}
- $loginMinimumLength = 3;
+ $loginMinimumLength = 2;
$loginMaximumLength = 100;
$l = strlen($userLogin);
if (!($l >= $loginMinimumLength
@@ -731,25 +735,16 @@ class Piwik
* @param string $translationId Translation ID, eg, `'General_Date'`.
* @param array|string|int $args `sprintf` arguments to be applied to the internationalized
* string.
+ * @param string|null $language Optionally force the language.
* @return string The translated string or `$translationId`.
* @api
*/
- public static function translate($translationId, $args = array())
+ public static function translate($translationId, $args = array(), $language = null)
{
- if (!is_array($args)) {
- $args = array($args);
- }
+ /** @var Translator $translator */
+ $translator = StaticContainer::get('Piwik\Translation\Translator');
- if (strpos($translationId, "_") !== false) {
- list($plugin, $key) = explode("_", $translationId, 2);
- if (isset($GLOBALS['Piwik_translations'][$plugin]) && isset($GLOBALS['Piwik_translations'][$plugin][$key])) {
- $translationId = $GLOBALS['Piwik_translations'][$plugin][$key];
- }
- }
- if (count($args) == 0) {
- return $translationId;
- }
- return vsprintf($translationId, $args);
+ return $translator->translate($translationId, $args, $language);
}
/**
diff --git a/core/Plugin.php b/core/Plugin.php
index 55a70ab501..af564a002b 100644
--- a/core/Plugin.php
+++ b/core/Plugin.php
@@ -8,7 +8,6 @@
*/
namespace Piwik;
-use Piwik\Cache\PersistentCache;
use Piwik\Plugin\Dependency;
use Piwik\Plugin\MetadataLoader;
@@ -108,10 +107,10 @@ class Plugin
/**
* As the cache is used quite often we avoid having to create instances all the time. We reuse it which is not
- * perfect but efficient. If the cache is used we need to make sure to call setCacheKey() before usage as there
+ * perfect but efficient. If the cache is used we need to make sure to call setId() before usage as there
* is maybe a different key set since last usage.
*
- * @var PersistentCache
+ * @var \Piwik\Cache\Eager
*/
private $cache;
@@ -131,14 +130,28 @@ class Plugin
}
$this->pluginName = $pluginName;
- $metadataLoader = new MetadataLoader($pluginName);
- $this->pluginInformation = $metadataLoader->load();
+ $cacheId = 'Plugin' . $pluginName . 'Metadata';
+ $cache = Cache::getEagerCache();
- if ($this->hasDefinedPluginInformationInPluginClass() && $metadataLoader->hasPluginJson()) {
- throw new \Exception('Plugin ' . $pluginName . ' has defined the method getInformation() and as well as having a plugin.json file. Please delete the getInformation() method from the plugin class. Alternatively, you may delete the plugin directory from plugins/' . $pluginName);
+ if ($cache->contains($cacheId)) {
+ $this->pluginInformation = $cache->fetch($cacheId);
+ } else {
+ $metadataLoader = new MetadataLoader($pluginName);
+ $this->pluginInformation = $metadataLoader->load();
+
+ if ($this->hasDefinedPluginInformationInPluginClass() && $metadataLoader->hasPluginJson()) {
+ throw new \Exception('Plugin ' . $pluginName . ' has defined the method getInformation() and as well as having a plugin.json file. Please delete the getInformation() method from the plugin class. Alternatively, you may delete the plugin directory from plugins/' . $pluginName);
+ }
+
+ $cache->save($cacheId, $this->pluginInformation);
}
+ }
- $this->cache = new PersistentCache('Plugin' . $pluginName);
+ private function createCacheIfNeeded()
+ {
+ if (is_null($this->cache)) {
+ $this->cache = Cache::getEagerCache();
+ }
}
private function hasDefinedPluginInformationInPluginClass()
@@ -305,15 +318,17 @@ class Plugin
*/
public function findComponent($componentName, $expectedSubclass)
{
- $this->cache->setCacheKey('Plugin' . $this->pluginName . $componentName . $expectedSubclass);
+ $this->createCacheIfNeeded();
+
+ $cacheId = 'Plugin' . $this->pluginName . $componentName . $expectedSubclass;
$componentFile = sprintf('%s/plugins/%s/%s.php', PIWIK_INCLUDE_PATH, $this->pluginName, $componentName);
- if ($this->cache->has()) {
- $klassName = $this->cache->get();
+ if ($this->cache->contains($cacheId)) {
+ $classname = $this->cache->fetch($cacheId);
- if (empty($klassName)) {
- return; // might by "false" in case has no menu, widget, ...
+ if (empty($classname)) {
+ return null; // might by "false" in case has no menu, widget, ...
}
if (file_exists($componentFile)) {
@@ -321,38 +336,40 @@ class Plugin
}
} else {
- $this->cache->set(false); // prevent from trying to load over and over again for instance if there is no Menu for a plugin
+ $this->cache->save($cacheId, false); // prevent from trying to load over and over again for instance if there is no Menu for a plugin
if (!file_exists($componentFile)) {
- return;
+ return null;
}
require_once $componentFile;
- $klassName = sprintf('Piwik\\Plugins\\%s\\%s', $this->pluginName, $componentName);
+ $classname = sprintf('Piwik\\Plugins\\%s\\%s', $this->pluginName, $componentName);
- if (!class_exists($klassName)) {
- return;
+ if (!class_exists($classname)) {
+ return null;
}
- if (!empty($expectedSubclass) && !is_subclass_of($klassName, $expectedSubclass)) {
+ if (!empty($expectedSubclass) && !is_subclass_of($classname, $expectedSubclass)) {
Log::warning(sprintf('Cannot use component %s for plugin %s, class %s does not extend %s',
- $componentName, $this->pluginName, $klassName, $expectedSubclass));
- return;
+ $componentName, $this->pluginName, $classname, $expectedSubclass));
+ return null;
}
- $this->cache->set($klassName);
+ $this->cache->save($cacheId, $classname);
}
- return new $klassName;
+ return new $classname;
}
public function findMultipleComponents($directoryWithinPlugin, $expectedSubclass)
{
- $this->cache->setCacheKey('Plugin' . $this->pluginName . $directoryWithinPlugin . $expectedSubclass);
+ $this->createCacheIfNeeded();
+
+ $cacheId = 'Plugin' . $this->pluginName . $directoryWithinPlugin . $expectedSubclass;
- if ($this->cache->has()) {
- $components = $this->cache->get();
+ if ($this->cache->contains($cacheId)) {
+ $components = $this->cache->fetch($cacheId);
if ($this->includeComponents($components)) {
return $components;
@@ -363,7 +380,7 @@ class Plugin
$components = $this->doFindMultipleComponents($directoryWithinPlugin, $expectedSubclass);
- $this->cache->set($components);
+ $this->cache->save($cacheId, $components);
return $components;
}
diff --git a/core/Plugin/ComponentFactory.php b/core/Plugin/ComponentFactory.php
index 68415e9397..751f1157d4 100644
--- a/core/Plugin/ComponentFactory.php
+++ b/core/Plugin/ComponentFactory.php
@@ -24,7 +24,7 @@ class ComponentFactory
* associated subdirectory.
*
* @param string $pluginName The name of the plugin the component is expected to belong to,
- * eg, `'UserSettings'`.
+ * eg, `'DevicesDetection'`.
* @param string $componentClassSimpleName The component's class name w/o namespace, eg,
* `"GetKeywords"`.
* @param string $componentTypeClass The fully qualified class name of the component type, eg,
@@ -70,7 +70,7 @@ class ComponentFactory
* @param string $componentTypeClass The fully qualified class name of the component type, eg,
* `"Piwik\Plugin\Report"`.
* @param string $pluginName|false The name of the plugin the component is expected to belong to,
- * eg, `'UserSettings'`.
+ * eg, `'DevicesDetection'`.
* @param callback $predicate
* @return mixed The component that satisfies $predicate or null if not found.
*/
diff --git a/core/Plugin/Controller.php b/core/Plugin/Controller.php
index 4afcbf25ad..a810d38545 100644
--- a/core/Plugin/Controller.php
+++ b/core/Plugin/Controller.php
@@ -15,6 +15,7 @@ use Piwik\API\Request;
use Piwik\Common;
use Piwik\Config as PiwikConfig;
use Piwik\Config;
+use Piwik\Container\StaticContainer;
use Piwik\DataTable\Filter\CalculateEvolutionFilter;
use Piwik\Date;
use Piwik\Exception\NoPrivilegesException;
@@ -31,8 +32,6 @@ use Piwik\Piwik;
use Piwik\Plugins\CoreAdminHome\CustomLogo;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
-use Piwik\Plugins\UsersManager\UserPreferences;
-use Piwik\Registry;
use Piwik\SettingsPiwik;
use Piwik\Site;
use Piwik\Url;
@@ -581,43 +580,55 @@ abstract class Controller
*/
protected function setGeneralVariablesView($view)
{
- $view->date = $this->strDate;
-
$view->idSite = $this->idSite;
$this->checkSitePermission();
$this->setPeriodVariablesView($view);
- $rawDate = Common::getRequestVar('date');
- $periodStr = Common::getRequestVar('period');
- if ($periodStr != 'range') {
- $date = Date::factory($this->strDate);
- $period = Period\Factory::build($periodStr, $date);
- } else {
- $period = new Range($periodStr, $rawDate, $this->site->getTimezone());
- }
- $view->rawDate = $rawDate;
- $view->prettyDate = self::getCalendarPrettyDate($period);
-
$view->siteName = $this->site->getName();
$view->siteMainUrl = $this->site->getMainUrl();
+ $siteTimezone = $this->site->getTimezone();
+
$datetimeMinDate = $this->site->getCreationDate()->getDatetime();
- $minDate = Date::factory($datetimeMinDate, $this->site->getTimezone());
+ $minDate = Date::factory($datetimeMinDate, $siteTimezone);
$this->setMinDateView($minDate, $view);
- $maxDate = Date::factory('now', $this->site->getTimezone());
+ $maxDate = Date::factory('now', $siteTimezone);
$this->setMaxDateView($maxDate, $view);
+ $rawDate = Common::getRequestVar('date');
+ $periodStr = Common::getRequestVar('period');
+
+ if ($periodStr != 'range') {
+ $date = Date::factory($this->strDate);
+ $validDate = $this->getValidDate($date, $minDate, $maxDate);
+ $period = Period\Factory::build($periodStr, $validDate);
+
+ if ($date->toString() !== $validDate->toString()) {
+ // we to not always change date since it could convert a strDate "today" to "YYYY-MM-DD"
+ // only change $this->strDate if it was not valid before
+ $this->setDate($validDate);
+ }
+ } else {
+ $period = new Range($periodStr, $rawDate, $siteTimezone);
+ }
+
// Setting current period start & end dates, for pre-setting the calendar when "Date Range" is selected
$dateStart = $period->getDateStart();
- if ($dateStart->isEarlier($minDate)) {
- $dateStart = $minDate;
- }
- $dateEnd = $period->getDateEnd();
- if ($dateEnd->isLater($maxDate)) {
- $dateEnd = $maxDate;
+ $dateStart = $this->getValidDate($dateStart, $minDate, $maxDate);
+
+ $dateEnd = $period->getDateEnd();
+ $dateEnd = $this->getValidDate($dateEnd, $minDate, $maxDate);
+
+ if ($periodStr == 'range') {
+ // make sure we actually display the correct calendar pretty date
+ $newRawDate = $dateStart->toString() . ',' . $dateEnd->toString();
+ $period = new Range($periodStr, $newRawDate, $siteTimezone);
}
+ $view->date = $this->strDate;
+ $view->prettyDate = self::getCalendarPrettyDate($period);
+ $view->rawDate = $rawDate;
$view->startDate = $dateStart;
$view->endDate = $dateEnd;
@@ -636,6 +647,19 @@ abstract class Controller
}
}
+ private function getValidDate(Date $date, Date $minDate, Date $maxDate)
+ {
+ if ($date->isEarlier($minDate)) {
+ $date = $minDate;
+ }
+
+ if ($date->isLater($maxDate)) {
+ $date = $maxDate;
+ }
+
+ return $date;
+ }
+
/**
* Assigns a set of generally useful variables to a {@link Piwik\View} instance.
*
@@ -852,7 +876,7 @@ abstract class Controller
$currentLogin = Piwik::getCurrentUserLogin();
$emails = implode(',', Piwik::getAllSuperUserAccessEmailAddresses());
$errorMessage = sprintf(Piwik::translate('CoreHome_NoPrivilegesAskPiwikAdmin'), $currentLogin, "<br/><a href='mailto:" . $emails . "?subject=Access to Piwik for user $currentLogin'>", "</a>");
- $errorMessage .= "<br /><br />&nbsp;&nbsp;&nbsp;<b><a href='index.php?module=" . Registry::get('auth')->getName() . "&amp;action=logout'>&rsaquo; " . Piwik::translate('General_Logout') . "</a></b><br />";
+ $errorMessage .= "<br /><br />&nbsp;&nbsp;&nbsp;<b><a href='index.php?module=" . StaticContainer::get('Piwik\Auth')->getName() . "&amp;action=logout'>&rsaquo; " . Piwik::translate('General_Logout') . "</a></b><br />";
$ex = new NoPrivilegesException($errorMessage);
$ex->setIsHtmlMessage();
@@ -979,7 +1003,9 @@ abstract class Controller
protected function checkSitePermission()
{
- if (empty($this->site) || empty($this->idSite)) {
+ if (!empty($this->idSite) && empty($this->site)) {
+ throw new NoAccessException(Piwik::translate('General_ExceptionPrivilegeAccessWebsite', array("'view'", $this->idSite)));
+ } else if (empty($this->site) || empty($this->idSite)) {
throw new Exception("The requested website idSite is not found in the request, or is invalid.
Please check that you are logged in Piwik and have permission to access the specified website.");
}
diff --git a/core/Plugin/ControllerAdmin.php b/core/Plugin/ControllerAdmin.php
index 008cb4d2f4..b0c86a6b1a 100644
--- a/core/Plugin/ControllerAdmin.php
+++ b/core/Plugin/ControllerAdmin.php
@@ -29,8 +29,6 @@ use Piwik\View;
*/
abstract class ControllerAdmin extends Controller
{
- private static $isEacceleratorUsed = false;
-
private static function notifyWhenTrackingStatisticsDisabled()
{
$statsEnabled = PiwikConfig::getInstance()->Tracker['record_statistics'];
@@ -105,27 +103,10 @@ abstract class ControllerAdmin extends Controller
}
}
- /**
- * See https://github.com/piwik/piwik/issues/4439#comment:8 and https://github.com/eaccelerator/eaccelerator/issues/12
- *
- * Eaccelerator does not support closures and is known to be not comptabile with Piwik. Therefore we are disabling
- * it automatically. At this point it looks like Eaccelerator is no longer under development and the bug has not
- * been fixed within a year.
- */
- public static function disableEacceleratorIfEnabled()
- {
- $isEacceleratorUsed = ini_get('eaccelerator.enable');
-
- if (!empty($isEacceleratorUsed)) {
- self::$isEacceleratorUsed = true;
-
- @ini_set('eaccelerator.enable', 0);
- }
- }
-
private static function notifyIfEAcceleratorIsUsed()
{
- if (!self::$isEacceleratorUsed) {
+ $isEacceleratorUsed = ini_get('eaccelerator.enable');
+ if (empty($isEacceleratorUsed)) {
return;
}
$message = sprintf("You are using the PHP accelerator & optimizer eAccelerator which is known to be not compatible with Piwik.
@@ -167,7 +148,6 @@ abstract class ControllerAdmin extends Controller
* - **statisticsNotRecorded** - Set to true if the `[Tracker] record_statistics` INI
* config is `0`. If not `0`, this variable will not be defined.
* - **topMenu** - The result of `MenuTop::getInstance()->getMenu()`.
- * - **currentAdminMenuName** - The currently selected admin menu name.
* - **enableFrames** - The value of the `[General] enable_framed_pages` INI config option. If
* true, {@link Piwik\View::setXFrameOptions()} is called on the view.
* - **isSuperUser** - Whether the current user is a superuser or not.
@@ -189,7 +169,6 @@ abstract class ControllerAdmin extends Controller
$view->topMenu = MenuTop::getInstance()->getMenu();
$view->userMenu = MenuUser::getInstance()->getMenu();
- $view->currentAdminMenuName = MenuAdmin::getInstance()->getCurrentAdminMenuName();
$view->isDataPurgeSettingsEnabled = self::isDataPurgeSettingsEnabled();
$enableFrames = PiwikConfig::getInstance()->General['enable_framed_settings'];
diff --git a/core/Plugin/Dimension/ActionDimension.php b/core/Plugin/Dimension/ActionDimension.php
index bc09b01fc3..f0daaf547c 100644
--- a/core/Plugin/Dimension/ActionDimension.php
+++ b/core/Plugin/Dimension/ActionDimension.php
@@ -8,7 +8,8 @@
*/
namespace Piwik\Plugin\Dimension;
-use Piwik\Cache\PluginAwareStaticCache;
+use Piwik\CacheId;
+use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugin\Segment;
@@ -212,9 +213,10 @@ abstract class ActionDimension extends Dimension
*/
public static function getAllDimensions()
{
- $cache = new PluginAwareStaticCache('ActionDimensions');
+ $cacheId = CacheId::pluginAware('ActionDimensions');
+ $cache = PiwikCache::getTransientCache();
- if (!$cache->has()) {
+ if (!$cache->contains($cacheId)) {
$plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated();
$instances = array();
@@ -225,10 +227,10 @@ abstract class ActionDimension extends Dimension
}
}
- $cache->set($instances);
+ $cache->save($cacheId, $instances);
}
- return $cache->get();
+ return $cache->fetch($cacheId);
}
/**
diff --git a/core/Plugin/Dimension/ConversionDimension.php b/core/Plugin/Dimension/ConversionDimension.php
index 7cf2a813c0..34a8006111 100644
--- a/core/Plugin/Dimension/ConversionDimension.php
+++ b/core/Plugin/Dimension/ConversionDimension.php
@@ -8,7 +8,8 @@
*/
namespace Piwik\Plugin\Dimension;
-use Piwik\Cache\PluginAwareStaticCache;
+use Piwik\CacheId;
+use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Common;
@@ -155,9 +156,10 @@ abstract class ConversionDimension extends Dimension
*/
public static function getAllDimensions()
{
- $cache = new PluginAwareStaticCache('ConversionDimensions');
+ $cacheId = CacheId::pluginAware('ConversionDimensions');
+ $cache = PiwikCache::getTransientCache();
- if (!$cache->has()) {
+ if (!$cache->contains($cacheId)) {
$plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated();
$instances = array();
@@ -168,10 +170,10 @@ abstract class ConversionDimension extends Dimension
}
}
- $cache->set($instances);
+ $cache->save($cacheId, $instances);
}
- return $cache->get();
+ return $cache->fetch($cacheId);
}
/**
diff --git a/core/Plugin/Dimension/VisitDimension.php b/core/Plugin/Dimension/VisitDimension.php
index 09a58554c0..97dfd6f57b 100644
--- a/core/Plugin/Dimension/VisitDimension.php
+++ b/core/Plugin/Dimension/VisitDimension.php
@@ -8,7 +8,8 @@
*/
namespace Piwik\Plugin\Dimension;
-use Piwik\Cache\PluginAwareStaticCache;
+use Piwik\CacheId;
+use Piwik\Cache as PiwikCache;
use Piwik\Columns\Dimension;
use Piwik\Common;
use Piwik\Db;
@@ -270,14 +271,34 @@ abstract class VisitDimension extends Dimension
}
/**
+ * This hook is executed by the tracker when determining if an action is the start of a new visit
+ * or part of an existing one. Derived classes can use it to force new visits based on dimension
+ * data.
+ *
+ * For example, the Campaign dimension in the Referrers plugin will force a new visit if the
+ * campaign information for the current action is different from the last.
+ *
+ * @param Request $request The current tracker request information.
+ * @param Visitor $visitor The information for the currently recognized visitor.
+ * @param Action|null $action The current action information (if any).
+ * @return bool Return true to force a visit, false if otherwise.
+ * @api
+ */
+ public function shouldForceNewVisit(Request $request, Visitor $visitor, Action $action = null)
+ {
+ return false;
+ }
+
+ /**
* Get all visit dimensions that are defined by all activated plugins.
* @return VisitDimension[]
*/
public static function getAllDimensions()
{
- $cache = new PluginAwareStaticCache('VisitDimensions');
+ $cacheId = CacheId::pluginAware('VisitDimensions');
+ $cache = PiwikCache::getTransientCache();
- if (!$cache->has()) {
+ if (!$cache->contains($cacheId)) {
$plugins = PluginManager::getInstance()->getPluginsLoadedAndActivated();
$instances = array();
@@ -290,10 +311,10 @@ abstract class VisitDimension extends Dimension
usort($instances, array('self', 'sortByRequiredFields'));
- $cache->set($instances);
+ $cache->save($cacheId, $instances);
}
- return $cache->get();
+ return $cache->fetch($cacheId);
}
/**
diff --git a/core/Plugin/Manager.php b/core/Plugin/Manager.php
index 9f8941170b..6e0d5ba91a 100644
--- a/core/Plugin/Manager.php
+++ b/core/Plugin/Manager.php
@@ -9,14 +9,12 @@
namespace Piwik\Plugin;
-use Piwik\Cache\PersistentCache;
-use Piwik\CacheFile;
+use Piwik\Cache;
use Piwik\Columns\Dimension;
-use Piwik\Common;
use Piwik\Config as PiwikConfig;
use Piwik\Config;
+use Piwik\Container\StaticContainer;
use Piwik\Db;
-use Piwik\Development;
use Piwik\EventDispatcher;
use Piwik\Filesystem;
use Piwik\Log;
@@ -26,9 +24,8 @@ use Piwik\Plugin;
use Piwik\Singleton;
use Piwik\Theme;
use Piwik\Tracker;
-use Piwik\Translate;
+use Piwik\Translation\Translator;
use Piwik\Updater;
-use Piwik\SettingsServer;
use Piwik\Plugin\Dimension\ActionDimension;
use Piwik\Plugin\Dimension\ConversionDimension;
use Piwik\Plugin\Dimension\VisitDimension;
@@ -93,12 +90,14 @@ class Manager extends Singleton
'ExampleTheme'
);
+ private $trackerPluginsNotToLoad = array();
+
/**
* Loads plugin that are enabled
*/
public function loadActivatedPlugins()
{
- $pluginsToLoad = Config::getInstance()->Plugins['Plugins'];
+ $pluginsToLoad = $this->getActivatedPluginsFromConfig();
$this->loadPlugins($pluginsToLoad);
}
@@ -108,7 +107,7 @@ class Manager extends Singleton
public function loadCorePluginsDuringTracker()
{
$pluginsToLoad = Config::getInstance()->Plugins['Plugins'];
- $pluginsToLoad = array_diff($pluginsToLoad, Tracker::getPluginsNotToLoad());
+ $pluginsToLoad = array_diff($pluginsToLoad, $this->getTrackerPluginsNotToLoad());
$this->loadPlugins($pluginsToLoad);
}
@@ -117,10 +116,11 @@ class Manager extends Singleton
*/
public function loadTrackerPlugins()
{
- $cache = new PersistentCache('PluginsTracker');
+ $cacheId = 'PluginsTracker';
+ $cache = Cache::getEagerCache();
- if ($cache->has()) {
- $pluginsTracker = $cache->get();
+ if ($cache->contains($cacheId)) {
+ $pluginsTracker = $cache->fetch($cacheId);
} else {
$this->unloadPlugins();
@@ -135,22 +135,46 @@ class Manager extends Singleton
}
if (!empty($pluginsTracker)) {
- $cache->set($pluginsTracker);
+ $cache->save($cacheId, $pluginsTracker);
}
}
- $this->unloadPlugins();
-
if (empty($pluginsTracker)) {
+ $this->unloadPlugins();
return array();
}
- $pluginsTracker = array_diff($pluginsTracker, Tracker::getPluginsNotToLoad());
+ $pluginsTracker = array_diff($pluginsTracker, $this->getTrackerPluginsNotToLoad());
$this->doNotLoadAlwaysActivatedPlugins();
$this->loadPlugins($pluginsTracker);
+
+ // we could simply unload all plugins first before loading plugins but this way it is much faster
+ // since we won't have to create each plugin again and we won't have to parse each plugin metadata file
+ // again etc
+ $this->makeSureOnlyActivatedPluginsAreLoaded();
+
return $pluginsTracker;
}
+ /**
+ * Do not load the specified plugins (used during testing, to disable Provider plugin)
+ * @param array $plugins
+ */
+ public function setTrackerPluginsNotToLoad($plugins)
+ {
+ $this->trackerPluginsNotToLoad = $plugins;
+ }
+
+ /**
+ * Get list of plugins to not load
+ *
+ * @return array
+ */
+ public function getTrackerPluginsNotToLoad()
+ {
+ return $this->trackerPluginsNotToLoad;
+ }
+
public function getCorePluginsDisabledByDefault()
{
return array_merge( $this->corePluginsDisabledByDefault, $this->coreThemesDisabledByDefault);
@@ -177,10 +201,11 @@ class Manager extends Singleton
/**
* Update Plugins config
*
- * @param array $plugins Plugins
+ * @param array $pluginsToLoad Plugins
*/
private function updatePluginsConfig($pluginsToLoad)
{
+ $pluginsToLoad = $this->sortPluginsSameOrderAsGlobalConfig($pluginsToLoad);
$section = PiwikConfig::getInstance()->Plugins;
$section['Plugins'] = $pluginsToLoad;
PiwikConfig::getInstance()->Plugins = $section;
@@ -237,7 +262,7 @@ class Manager extends Singleton
public function isPluginActivated($name)
{
return in_array($name, $this->pluginsToLoad)
- || $this->isPluginAlwaysActivated($name);
+ || ($this->doLoadAlwaysActivatedPlugins && $this->isPluginAlwaysActivated($name));
}
/**
@@ -403,7 +428,7 @@ class Manager extends Singleton
*/
public function installLoadedPlugins()
{
- Log::verbose("Loaded plugins: " . implode(", ", array_keys($this->getLoadedPlugins())));
+ Log::debug("Loaded plugins: " . implode(", ", array_keys($this->getLoadedPlugins())));
$messages = array();
foreach ($this->getLoadedPlugins() as $plugin) {
try {
@@ -543,7 +568,8 @@ class Manager extends Singleton
*/
public function loadAllPluginsAndGetTheirInfo()
{
- $language = Translate::getLanguageToLoad();
+ /** @var Translator $translator */
+ $translator = StaticContainer::get('Piwik\Translation\Translator');
$plugins = array();
@@ -567,7 +593,7 @@ class Manager extends Singleton
'uninstallable' => true,
);
} else {
- $this->loadTranslation($pluginName, $language);
+ $translator->addDirectory(self::getPluginsDirectory() . $pluginName . '/lang');
$this->loadPlugin($pluginName);
$info = array(
'activated' => $this->isPluginActivated($pluginName),
@@ -578,7 +604,6 @@ class Manager extends Singleton
$plugins[$pluginName] = $info;
}
- $this->loadPluginTranslations();
$loadedPlugins = $this->getLoadedPlugins();
foreach ($loadedPlugins as $oPlugin) {
@@ -610,12 +635,7 @@ class Manager extends Singleton
*/
public function isPluginBundledWithCore($name)
{
- // Reading the plugins from the global.ini.php config file
- $pluginsBundledWithPiwik = PiwikConfig::getInstance()->getFromGlobalConfig('Plugins');
- $pluginsBundledWithPiwik = $pluginsBundledWithPiwik['Plugins'];
-
- return (!empty($pluginsBundledWithPiwik)
- && in_array($name, $pluginsBundledWithPiwik))
+ return $this->isPluginEnabledByDefault($name)
|| in_array($name, $this->getCorePluginsDisabledByDefault())
|| $name == self::DEFAULT_THEME;
}
@@ -644,8 +664,7 @@ class Manager extends Singleton
*/
public function loadPlugins(array $pluginsToLoad)
{
- $pluginsToLoad = array_unique($pluginsToLoad);
- $this->pluginsToLoad = $pluginsToLoad;
+ $this->pluginsToLoad = $this->makePluginsToLoad($pluginsToLoad);
$this->reloadActivatedPlugins();
}
@@ -666,57 +685,6 @@ class Manager extends Singleton
}
/**
- * Load translations for loaded plugins
- *
- * @param bool|string $language Optional language code
- */
- public function loadPluginTranslations($language = false)
- {
- if (empty($language)) {
- $language = Translate::getLanguageToLoad();
- }
-
- $cache = new CacheFile('tracker', 43200); // ttl=12hours
- $cacheKey = 'PluginTranslations';
-
- if (!empty($language)) {
- $cacheKey .= '-' . trim($language);
- }
-
- if (!empty($this->loadedPlugins)) {
- // makes sure to create a translation in case loaded plugins change (ie Tests vs Tracker vs UI etc)
- $cacheKey .= '-' . md5(implode('', $this->getLoadedPluginsName()));
- }
-
- $translations = $cache->get($cacheKey);
-
- if (!empty($translations) &&
- is_array($translations) &&
- !Development::isEnabled()) {
-
- Translate::mergeTranslationArray($translations);
- return;
- }
-
- $translations = array();
- $pluginNames = self::getAllPluginsNames();
-
- foreach ($pluginNames as $pluginName) {
- if ($this->isPluginLoaded($pluginName) ||
- $this->isPluginBundledWithCore($pluginName)) {
-
- $this->loadTranslation($pluginName, $language);
-
- if (isset($GLOBALS['Piwik_translations'][$pluginName])) {
- $translations[$pluginName] = $GLOBALS['Piwik_translations'][$pluginName];
- }
- }
- }
-
- $cache->set($cacheKey, $translations);
- }
-
- /**
* Execute postLoad() hook for loaded plugins
*/
public function postLoadPlugins()
@@ -742,7 +710,7 @@ class Manager extends Singleton
*
* array(
* 'UserCountry' => Plugin $pluginObject,
- * 'UserSettings' => Plugin $pluginObject,
+ * 'UserLanguage' => Plugin $pluginObject,
* );
*
* @return Plugin[]
@@ -776,7 +744,7 @@ class Manager extends Singleton
*
* array(
* 'UserCountry' => Plugin $pluginObject,
- * 'UserSettings' => Plugin $pluginObject,
+ * 'UserLanguage' => Plugin $pluginObject,
* );
*
* @return Plugin[]
@@ -799,7 +767,7 @@ class Manager extends Singleton
*
* array(
* 'UserCountry'
- * 'UserSettings'
+ * 'UserLanguage'
* );
*
* @return string[]
@@ -809,6 +777,13 @@ class Manager extends Singleton
return $this->pluginsToLoad;
}
+ public function getActivatedPluginsFromConfig()
+ {
+ $plugins = @Config::getInstance()->Plugins['Plugins'];
+
+ return $this->makePluginsToLoad($plugins);
+ }
+
/**
* Returns a Plugin object by name.
*
@@ -830,12 +805,6 @@ class Manager extends Singleton
*/
private function reloadActivatedPlugins()
{
- if ($this->doLoadAlwaysActivatedPlugins) {
- $this->pluginsToLoad = array_merge($this->pluginsToLoad, $this->pluginToAlwaysActivate);
- }
-
- $this->pluginsToLoad = array_unique($this->pluginsToLoad);
-
$pluginsToPostPendingEventsTo = array();
foreach ($this->pluginsToLoad as $pluginName) {
if (!$this->isPluginLoaded($pluginName)
@@ -1005,67 +974,14 @@ class Manager extends Singleton
*
* @param string $pluginName plugin name without prefix (eg. 'UserCountry')
* @param Plugin $newPlugin
+ * @internal
*/
- private function addLoadedPlugin($pluginName, Plugin $newPlugin)
+ public function addLoadedPlugin($pluginName, Plugin $newPlugin)
{
$this->loadedPlugins[$pluginName] = $newPlugin;
}
/**
- * Load translation
- *
- * @param Plugin $plugin
- * @param string $langCode
- * @throws \Exception
- * @return bool whether the translation was found and loaded
- */
- private function loadTranslation($plugin, $langCode)
- {
- // we are in Tracker mode if Loader is not (yet) loaded
- if (SettingsServer::isTrackerApiRequest()) {
- return false;
- }
-
- if (is_string($plugin)) {
- $pluginName = $plugin;
- } else {
- $pluginName = $plugin->getPluginName();
- }
-
- $path = self::getPluginsDirectory() . $pluginName . '/lang/%s.json';
-
- $defaultLangPath = sprintf($path, $langCode);
- $defaultEnglishLangPath = sprintf($path, 'en');
-
- $translationsLoaded = false;
-
- // merge in english translations as default first
- if (file_exists($defaultEnglishLangPath)) {
- $translations = $this->getTranslationsFromFile($defaultEnglishLangPath);
- $translationsLoaded = true;
- if (isset($translations[$pluginName])) {
- // only merge translations of plugin - prevents overwritten strings
- Translate::mergeTranslationArray(array($pluginName => $translations[$pluginName]));
- }
- }
-
- // merge in specific language translations (to overwrite english defaults)
- if (!empty($langCode) &&
- $defaultEnglishLangPath != $defaultLangPath &&
- file_exists($defaultLangPath)) {
-
- $translations = $this->getTranslationsFromFile($defaultLangPath);
- $translationsLoaded = true;
- if (isset($translations[$pluginName])) {
- // only merge translations of plugin - prevents overwritten strings
- Translate::mergeTranslationArray(array($pluginName => $translations[$pluginName]));
- }
- }
-
- return $translationsLoaded;
- }
-
- /**
* Return names of all installed plugins.
*
* @return array
@@ -1193,27 +1109,6 @@ class Manager extends Singleton
}
/**
- * @param string $pathToTranslationFile
- * @throws \Exception
- * @return mixed
- */
- private function getTranslationsFromFile($pathToTranslationFile)
- {
- $data = file_get_contents($pathToTranslationFile);
- $translations = json_decode($data, true);
-
- if (is_null($translations) && Common::hasJsonErrorOccurred()) {
- $jsonError = Common::getLastJsonError();
-
- $message = sprintf('Not able to load translation file %s: %s', $pathToTranslationFile, $jsonError);
-
- throw new \Exception($message);
- }
-
- return $translations;
- }
-
- /**
* @param $pluginName
* @return bool
*/
@@ -1335,18 +1230,84 @@ class Manager extends Singleton
{
Option::delete('version_' . $version);
}
-}
-/**
- */
-class PluginException extends \Exception
-{
- function __construct($pluginName, $message)
+ private function makeSureOnlyActivatedPluginsAreLoaded()
+ {
+ foreach ($this->getLoadedPlugins() as $pluginName => $plugin) {
+ if (!in_array($pluginName, $this->pluginsToLoad)) {
+ $this->unloadPlugin($plugin);
+ }
+ }
+ }
+
+ /**
+ * Reading the plugins from the global.ini.php config file
+ *
+ * @return array
+ */
+ protected function getPluginsFromGlobalIniConfigFile()
+ {
+ $pluginsBundledWithPiwik = PiwikConfig::getInstance()->getFromGlobalConfig('Plugins');
+ $pluginsBundledWithPiwik = $pluginsBundledWithPiwik['Plugins'];
+ return $pluginsBundledWithPiwik;
+ }
+
+ /**
+ * @param $name
+ * @return bool
+ */
+ protected function isPluginEnabledByDefault($name)
+ {
+ $pluginsBundledWithPiwik = $this->getPluginsFromGlobalIniConfigFile();
+
+ if(empty($pluginsBundledWithPiwik)) {
+ return false;
+ }
+ return in_array($name, $pluginsBundledWithPiwik);
+ }
+
+ /**
+ * @param array $pluginsToLoad
+ * @return array
+ */
+ private function makePluginsToLoad(array $pluginsToLoad)
{
- parent::__construct("There was a problem installing the plugin " . $pluginName . ": " . $message . "
- If this plugin has already been installed, and if you want to hide this message</b>, you must add the following line under the
- [PluginsInstalled]
- entry in your config/config.ini.php file:
- PluginsInstalled[] = $pluginName");
+ $pluginsToLoad = array_unique($pluginsToLoad);
+ if ($this->doLoadAlwaysActivatedPlugins) {
+ $pluginsToLoad = array_merge($pluginsToLoad, $this->pluginToAlwaysActivate);
+ }
+ $pluginsToLoad = array_unique($pluginsToLoad);
+ $pluginsToLoad = $this->sortPluginsSameOrderAsGlobalConfig($pluginsToLoad);
+ return $pluginsToLoad;
+ }
+
+ private function sortPluginsSameOrderAsGlobalConfig(array $plugins)
+ {
+ $global = $this->getPluginsFromGlobalIniConfigFile();
+ if(empty($global)) {
+ return $plugins;
+ }
+ $global = array_values($global);
+ $plugins = array_values($plugins);
+
+ $defaultPluginsLoadedFirst = array_intersect($global, $plugins);
+
+ $otherPluginsToLoadAfterDefaultPlugins = array_diff($plugins, $defaultPluginsLoadedFirst);
+
+ // sort by name to have a predictable order for those extra plugins
+ sort($otherPluginsToLoadAfterDefaultPlugins);
+
+ $sorted = array_merge($defaultPluginsLoadedFirst, $otherPluginsToLoadAfterDefaultPlugins);
+
+ return $sorted;
+ }
+
+ public function loadPluginTranslations()
+ {
+ /** @var Translator $translator */
+ $translator = StaticContainer::get('Piwik\Translation\Translator');
+ foreach ($this->getAllPluginsNames() as $pluginName) {
+ $translator->addDirectory(self::getPluginsDirectory() . $pluginName . '/lang');
+ }
}
}
diff --git a/core/Plugin/Menu.php b/core/Plugin/Menu.php
index 0cdc1878df..d2c54645f1 100644
--- a/core/Plugin/Menu.php
+++ b/core/Plugin/Menu.php
@@ -195,7 +195,7 @@ class Menu
$defaultDate = $userPreferences->getDefaultDate();
}
if (empty($defaultPeriod)) {
- $defaultPeriod = $userPreferences->getDefaultPeriod();
+ $defaultPeriod = $userPreferences->getDefaultPeriod(false);
}
return array(
'idSite' => $websiteId,
diff --git a/core/Plugin/PluginException.php b/core/Plugin/PluginException.php
new file mode 100644
index 0000000000..90aa03a342
--- /dev/null
+++ b/core/Plugin/PluginException.php
@@ -0,0 +1,21 @@
+<?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;
+
+class PluginException extends \Exception
+{
+ public function __construct($pluginName, $message)
+ {
+ parent::__construct("There was a problem installing the plugin " . $pluginName . ": " . $message . "
+ If this plugin has already been installed, and if you want to hide this message</b>, you must add the following line under the
+ [PluginsInstalled]
+ entry in your config/config.ini.php file:
+ PluginsInstalled[] = $pluginName");
+ }
+}
diff --git a/core/Plugin/Report.php b/core/Plugin/Report.php
index 1824b6774f..62ed594f8c 100644
--- a/core/Plugin/Report.php
+++ b/core/Plugin/Report.php
@@ -10,15 +10,16 @@ namespace Piwik\Plugin;
use Piwik\API\Proxy;
use Piwik\API\Request;
-use Piwik\Cache\LanguageAwareStaticCache;
+use Piwik\Cache;
+use Piwik\CacheId;
use Piwik\Columns\Dimension;
use Piwik\DataTable;
use Piwik\Menu\MenuReporting;
use Piwik\Metrics;
+use Piwik\Cache as PiwikCache;
use Piwik\Piwik;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugins\CoreVisualizations\Visualizations\HtmlTable;
-use Piwik\Translate;
use Piwik\WidgetsList;
use Piwik\ViewDataTable\Factory as ViewDataTableFactory;
use Exception;
@@ -195,7 +196,7 @@ class Report
'Goals_Goals',
'General_Visitors',
'DevicesDetection_DevicesDetection',
- 'UserSettings_VisitorSettings',
+ 'General_VisitorSettings',
'API'
);
@@ -270,6 +271,16 @@ class Report
}
/**
+ * Returns if the default viewDataTable type should always be used. e.g. the type won't be changeable through config or url params.
+ * Defaults to false
+ * @return bool
+ */
+ public function alwaysUseDefaultViewDataTable ()
+ {
+ return false;
+ }
+
+ /**
* Here you can configure how your report should be displayed and which capabilities your report has. For instance
* whether your report supports a "search" or not. EG `$view->config->show_search = false`. You can also change the
* default request config. For instance you can change how many rows are displayed by default:
@@ -301,7 +312,8 @@ class Report
$apiAction = $apiProxy->buildApiActionName($this->module, $this->action);
- $view = ViewDataTableFactory::build(null, $apiAction, $this->module . '.' . $this->action);
+ $view = ViewDataTableFactory::build(null, $apiAction, $this->module . '.' . $this->action);
+
$rendered = $view->render();
return $rendered;
@@ -404,7 +416,7 @@ class Report
* default metric translation for this metric using the {@hook Metrics.getDefaultMetricTranslations} event. If you
* want to overwrite any default metric translation you should overwrite this method, call this parent method to
* get all default translations and overwrite any custom metric translations.
- * @return array
+ * @return array|mixed
* @api
*/
public function getProcessedMetrics()
@@ -723,7 +735,38 @@ class Report
*/
public static function factory($module, $action)
{
- return ComponentFactory::factory($module, ucfirst($action), __CLASS__);
+ $listApiToReport = self::getMapOfModuleActionsToReport();
+ $api = $module . '.' . ucfirst($action);
+
+ if (!array_key_exists($api, $listApiToReport)) {
+ return null;
+ }
+
+ $klassName = $listApiToReport[$api];
+
+ return new $klassName;
+ }
+
+ private static function getMapOfModuleActionsToReport()
+ {
+ $cacheId = CacheId::pluginAware('ReportFactoryMap');
+
+ $cache = Cache::getEagerCache();
+ if ($cache->contains($cacheId)) {
+ $mapApiToReport = $cache->fetch($cacheId);
+ } else {
+ $reports = self::getAllReports();
+
+ $mapApiToReport = array();
+ foreach ($reports as $report) {
+ $key = $report->getModule() . '.' . ucfirst($report->getAction());
+ $mapApiToReport[$key] = get_class($report);
+ }
+
+ $cache->save($cacheId, $mapApiToReport);
+ }
+
+ return $mapApiToReport;
}
/**
@@ -735,9 +778,10 @@ class Report
public static function getAllReports()
{
$reports = self::getAllReportClasses();
- $cache = new LanguageAwareStaticCache('Reports' . implode('', $reports));
+ $cacheId = CacheId::languageAware('Reports' . md5(implode('', $reports)));
+ $cache = PiwikCache::getTransientCache();
- if (!$cache->has()) {
+ if (!$cache->contains($cacheId)) {
$instances = array();
foreach ($reports as $report) {
@@ -746,10 +790,10 @@ class Report
usort($instances, array('self', 'sort'));
- $cache->set($instances);
+ $cache->save($cacheId, $instances);
}
- return $cache->get();
+ return $cache->fetch($cacheId);
}
/**
@@ -785,10 +829,10 @@ class Report
foreach ($metricsToTranslate as $metric) {
if ($metric instanceof Metric) {
- $metricName = $metric->getName();
+ $metricName = $metric->getName();
$translation = $metric->getTranslatedName();
} else {
- $metricName = $metric;
+ $metricName = $metric;
$translation = @$translations[$metric];
}
diff --git a/core/Plugin/Segment.php b/core/Plugin/Segment.php
index 795d4da157..2f0c84db63 100644
--- a/core/Plugin/Segment.php
+++ b/core/Plugin/Segment.php
@@ -96,7 +96,7 @@ class Segment
/**
* Set (overwrite) the segment display name. This name will be visible in the API and the UI. It should be a
- * translation key such as 'Actions_ColumnEntryPageTitle' or 'UserSettings_ColumnResolution'.
+ * translation key such as 'Actions_ColumnEntryPageTitle' or 'Resolution_ColumnResolution'.
* @param string $name
* @api
*/
@@ -253,4 +253,4 @@ class Segment
return $segment;
}
-}
+} \ No newline at end of file
diff --git a/core/Plugin/Settings.php b/core/Plugin/Settings.php
index 23d1472ca9..8a674ae370 100644
--- a/core/Plugin/Settings.php
+++ b/core/Plugin/Settings.php
@@ -180,12 +180,14 @@ abstract class Settings
*/
protected function addSetting(Setting $setting)
{
- if (!ctype_alnum($setting->getName())) {
+ $name = $setting->getName();
+
+ if (!ctype_alnum($name)) {
$msg = sprintf('The setting name "%s" in plugin "%s" is not valid. Only alpha and numerical characters are allowed', $setting->getName(), $this->pluginName);
throw new \Exception($msg);
}
- if (array_key_exists($setting->getName(), $this->settings)) {
+ if (array_key_exists($name, $this->settings)) {
throw new \Exception(sprintf('A setting with name "%s" does already exist for plugin "%s"', $setting->getName(), $this->pluginName));
}
@@ -195,7 +197,7 @@ abstract class Settings
$setting->setStorage($this->storage);
$setting->setPluginName($this->pluginName);
- $this->settings[$setting->getName()] = $setting;
+ $this->settings[$name] = $setting;
}
/**
@@ -265,11 +267,14 @@ abstract class Settings
private function setDefaultTypeAndFieldIfNeeded(Setting $setting)
{
- if (!is_null($setting->uiControlType) && is_null($setting->type)) {
+ $hasControl = !is_null($setting->uiControlType);
+ $hasType = !is_null($setting->type);
+
+ if ($hasControl && !$hasType) {
$setting->type = $this->getDefaultType($setting->uiControlType);
- } elseif (!is_null($setting->type) && is_null($setting->uiControlType)) {
+ } elseif ($hasType && !$hasControl) {
$setting->uiControlType = $this->getDefaultCONTROL($setting->type);
- } elseif (is_null($setting->uiControlType) && is_null($setting->type)) {
+ } elseif (!$hasControl && !$hasType) {
$setting->type = static::TYPE_STRING;
$setting->uiControlType = static::CONTROL_TEXT;
}
diff --git a/core/Plugin/Tasks.php b/core/Plugin/Tasks.php
index 87b2c34d41..2d131d86c4 100644
--- a/core/Plugin/Tasks.php
+++ b/core/Plugin/Tasks.php
@@ -9,8 +9,8 @@
namespace Piwik\Plugin;
use Piwik\Development;
-use Piwik\ScheduledTask;
-use Piwik\ScheduledTime;
+use Piwik\Scheduler\Schedule\Schedule;
+use Piwik\Scheduler\Task;
/**
* Base class for all Tasks declarations.
@@ -21,15 +21,15 @@ use Piwik\ScheduledTime;
class Tasks
{
/**
- * @var ScheduledTask[]
+ * @var Task[]
*/
private $tasks = array();
- const LOWEST_PRIORITY = ScheduledTask::LOWEST_PRIORITY;
- const LOW_PRIORITY = ScheduledTask::LOW_PRIORITY;
- const NORMAL_PRIORITY = ScheduledTask::NORMAL_PRIORITY;
- const HIGH_PRIORITY = ScheduledTask::HIGH_PRIORITY;
- const HIGHEST_PRIORITY = ScheduledTask::HIGHEST_PRIORITY;
+ const LOWEST_PRIORITY = Task::LOWEST_PRIORITY;
+ const LOW_PRIORITY = Task::LOW_PRIORITY;
+ const NORMAL_PRIORITY = Task::NORMAL_PRIORITY;
+ const HIGH_PRIORITY = Task::HIGH_PRIORITY;
+ const HIGHEST_PRIORITY = Task::HIGHEST_PRIORITY;
/**
* This method is called to collect all schedule tasks. Register all your tasks here that should be executed
@@ -41,7 +41,7 @@ class Tasks
}
/**
- * @return ScheduledTask[] $tasks
+ * @return Task[] $tasks
*/
public function getScheduledTasks()
{
@@ -60,7 +60,7 @@ class Tasks
* For instance '$param1###$param2###$param3'
* @param int $priority Can be any constant such as self::LOW_PRIORITY
*
- * @return ScheduledTime
+ * @return Schedule
* @api
*/
protected function hourly($methodName, $methodParameter = null, $priority = self::NORMAL_PRIORITY)
@@ -109,13 +109,13 @@ class Tasks
* @param string|object $objectOrClassName
* @param string $methodName
* @param null|string $methodParameter
- * @param string|ScheduledTime $time
+ * @param string|Schedule $time
* @param int $priority
*
- * @return ScheduledTime
+ * @return \Piwik\Scheduler\Schedule\Schedule
*
* @throws \Exception If a wrong time format is given. Needs to be either a string such as 'daily', 'weekly', ...
- * or an instance of {@link Piwik\ScheduledTime}
+ * or an instance of {@link Piwik\Scheduler\Schedule\Schedule}
*
* @api
*/
@@ -124,14 +124,14 @@ class Tasks
$this->checkIsValidTask($objectOrClassName, $methodName);
if (is_string($time)) {
- $time = ScheduledTime::factory($time);
+ $time = Schedule::factory($time);
}
- if (!($time instanceof ScheduledTime)) {
- throw new \Exception('$time should be an instance of ScheduledTime');
+ if (!($time instanceof Schedule)) {
+ throw new \Exception('$time should be an instance of Schedule');
}
- $this->scheduleTask(new ScheduledTask($objectOrClassName, $methodName, $methodParameter, $time, $priority));
+ $this->scheduleTask(new Task($objectOrClassName, $methodName, $methodParameter, $time, $priority));
return $time;
}
@@ -140,9 +140,9 @@ class Tasks
* In case you need very high flexibility and none of the other convenient methods such as {@link hourly()} or
* {@link custom()} suit you, you can use this method to add a custom scheduled task.
*
- * @param ScheduledTask $task
+ * @param Task $task
*/
- protected function scheduleTask(ScheduledTask $task)
+ protected function scheduleTask(Task $task)
{
$this->tasks[] = $task;
}
diff --git a/core/Plugin/Visualization.php b/core/Plugin/Visualization.php
index 8c843806de..15b468e362 100644
--- a/core/Plugin/Visualization.php
+++ b/core/Plugin/Visualization.php
@@ -19,6 +19,7 @@ use Piwik\NoAccessException;
use Piwik\Option;
use Piwik\Period;
use Piwik\Piwik;
+use Piwik\Plugins\API\API as ApiApi;
use Piwik\Plugins\PrivacyManager\PrivacyManager;
use Piwik\View;
use Piwik\ViewDataTable\Manager as ViewDataTableManager;
@@ -207,6 +208,7 @@ class Visualization extends ViewDataTable
$view->visualization = $this;
$view->visualizationTemplate = static::TEMPLATE_FILE;
$view->visualizationCssClass = $this->getDefaultDataTableCssClass();
+ $view->reportMetdadata = $this->getReportMetadata();
if (null === $this->dataTable) {
$view->dataTable = null;
@@ -231,6 +233,22 @@ class Visualization extends ViewDataTable
return $view;
}
+ private function getReportMetadata()
+ {
+ $request = $this->request->getRequestArray() + $_GET + $_POST;
+
+ $idSite = Common::getRequestVar('idSite', null, 'string', $request);
+ $module = $this->requestConfig->getApiModuleToRequest();
+ $action = $this->requestConfig->getApiMethodToRequest();
+ $metadata = ApiApi::getInstance()->getMetadata($idSite, $module, $action);
+
+ if (!empty($metadata)) {
+ return array_shift($metadata);
+ }
+
+ return false;
+ }
+
private function overrideSomeConfigPropertiesIfNeeded()
{
if (empty($this->config->footer_icons)) {
diff --git a/core/Profiler.php b/core/Profiler.php
index cb68432725..b6cbfb67dd 100644
--- a/core/Profiler.php
+++ b/core/Profiler.php
@@ -338,6 +338,6 @@ class Profiler
*/
private static function getPathToXHProfRunIds()
{
- return StaticContainer::getContainer()->get('path.tmp') . '/cache/tests-xhprof-runs';
+ return StaticContainer::get('path.tmp') . '/cache/tests-xhprof-runs';
}
}
diff --git a/core/RankingQuery.php b/core/RankingQuery.php
index f44845f075..cd4f830669 100644
--- a/core/RankingQuery.php
+++ b/core/RankingQuery.php
@@ -214,7 +214,7 @@ class RankingQuery
*/
public function execute($innerQuery, $bind = array())
{
- $query = $this->generateQuery($innerQuery);
+ $query = $this->generateRankingQuery($innerQuery);
$data = Db::fetchAll($query, $bind);
if ($this->columnToMarkExcludedRows !== false) {
@@ -268,7 +268,7 @@ class RankingQuery
* itself.
* @return string The entire ranking query SQL.
*/
- public function generateQuery($innerQuery)
+ public function generateRankingQuery($innerQuery)
{
// +1 to include "Others"
$limit = $this->limit + 1;
diff --git a/core/Registry.php b/core/Registry.php
index 5022bf09e2..bc80f22518 100644
--- a/core/Registry.php
+++ b/core/Registry.php
@@ -4,25 +4,21 @@
*
* @link http://piwik.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
*/
+
namespace Piwik;
+use Piwik\Container\StaticContainer;
+
/**
* Registry class.
*
* @method static Registry getInstance()
* @api
+ * @deprecated This class will be removed, use the container instead.
*/
class Registry extends Singleton
{
- private $data;
-
- protected function __construct()
- {
- $this->data = array();
- }
-
public static function isRegistered($key)
{
return self::getInstance()->hasKey($key);
@@ -40,19 +36,28 @@ class Registry extends Singleton
public function setKey($key, $value)
{
- $this->data[$key] = $value;
+ if ($key === 'auth') {
+ $key = 'Piwik\Auth';
+ }
+
+ StaticContainer::getContainer()->set($key, $value);
}
public function getKey($key)
{
- if (!$this->hasKey($key)) {
- throw new \Exception(sprintf("Key '%s' doesn't exist in Registry", $key));
+ if ($key === 'auth') {
+ $key = 'Piwik\Auth';
}
- return $this->data[$key];
+
+ return StaticContainer::get($key);
}
public function hasKey($key)
{
- return array_key_exists($key, $this->data);
+ if ($key === 'auth') {
+ $key = 'Piwik\Auth';
+ }
+
+ return StaticContainer::getContainer()->has($key);
}
}
diff --git a/core/ReportRenderer.php b/core/ReportRenderer.php
index 5486d5cd21..98cc547cd7 100644
--- a/core/ReportRenderer.php
+++ b/core/ReportRenderer.php
@@ -23,13 +23,15 @@ use Piwik\BaseFactory;
*/
abstract class ReportRenderer extends BaseFactory
{
- const DEFAULT_REPORT_FONT = 'dejavusans';
- const REPORT_TEXT_COLOR = "68,68,68";
- const REPORT_TITLE_TEXT_COLOR = "126,115,99";
- const TABLE_HEADER_BG_COLOR = "228,226,215";
- const TABLE_HEADER_TEXT_COLOR = "37,87,146";
- const TABLE_CELL_BORDER_COLOR = "231,231,231";
- const TABLE_BG_COLOR = "249,250,250";
+ const DEFAULT_REPORT_FONT_FAMILY = 'dejavusans';
+ const REPORT_TEXT_COLOR = "13,13,13";
+ const REPORT_TITLE_TEXT_COLOR = "13,13,13";
+ const TABLE_HEADER_BG_COLOR = "255,255,255";
+ const TABLE_HEADER_TEXT_COLOR = "13,13,13";
+ const TABLE_HEADER_TEXT_TRANSFORM = "uppercase";
+ const TABLE_HEADER_TEXT_WEIGHT = "normal";
+ const TABLE_CELL_BORDER_COLOR = "217,217,217";
+ const TABLE_BG_COLOR = "242,242,242";
const HTML_FORMAT = 'html';
const PDF_FORMAT = 'pdf';
@@ -145,7 +147,7 @@ abstract class ReportRenderer extends BaseFactory
*/
protected static function getOutputPath($filename)
{
- $outputFilename = StaticContainer::getContainer()->get('path.tmp') . '/assets/' . $filename;
+ $outputFilename = StaticContainer::get('path.tmp') . '/assets/' . $filename;
@chmod($outputFilename, 0600);
@unlink($outputFilename);
diff --git a/core/ReportRenderer/Html.php b/core/ReportRenderer/Html.php
index d8ff678bc8..6c0e950c3e 100644
--- a/core/ReportRenderer/Html.php
+++ b/core/ReportRenderer/Html.php
@@ -22,9 +22,9 @@ class Html extends ReportRenderer
const IMAGE_GRAPH_WIDTH = 700;
const IMAGE_GRAPH_HEIGHT = 200;
- const REPORT_TITLE_TEXT_SIZE = 11;
+ const REPORT_TITLE_TEXT_SIZE = 24;
const REPORT_TABLE_HEADER_TEXT_SIZE = 11;
- const REPORT_TABLE_ROW_TEXT_SIZE = 11;
+ const REPORT_TABLE_ROW_TEXT_SIZE = '13px';
const REPORT_BACK_TO_TOP_TEXT_SIZE = 9;
const HTML_CONTENT_TYPE = 'text/html';
@@ -107,6 +107,7 @@ class Html extends ReportRenderer
private function assignCommonParameters(View $view)
{
+ $view->assign("reportFontFamily", ReportRenderer::DEFAULT_REPORT_FONT_FAMILY);
$view->assign("reportTitleTextColor", ReportRenderer::REPORT_TITLE_TEXT_COLOR);
$view->assign("reportTitleTextSize", self::REPORT_TITLE_TEXT_SIZE);
$view->assign("reportTextColor", ReportRenderer::REPORT_TEXT_COLOR);
@@ -114,7 +115,9 @@ class Html extends ReportRenderer
$view->assign("tableHeaderTextColor", ReportRenderer::TABLE_HEADER_TEXT_COLOR);
$view->assign("tableCellBorderColor", ReportRenderer::TABLE_CELL_BORDER_COLOR);
$view->assign("tableBgColor", ReportRenderer::TABLE_BG_COLOR);
+ $view->assign("reportTableHeaderTextWeight", self::TABLE_HEADER_TEXT_WEIGHT);
$view->assign("reportTableHeaderTextSize", self::REPORT_TABLE_HEADER_TEXT_SIZE);
+ $view->assign("reportTableHeaderTextTransform", ReportRenderer::TABLE_HEADER_TEXT_TRANSFORM);
$view->assign("reportTableRowTextSize", self::REPORT_TABLE_ROW_TEXT_SIZE);
$view->assign("reportBackToTopTextSize", self::REPORT_BACK_TO_TOP_TEXT_SIZE);
$view->assign("currentPath", SettingsPiwik::getPiwikUrl());
diff --git a/core/ReportRenderer/Pdf.php b/core/ReportRenderer/Pdf.php
index 47c2ebf900..3843cd942c 100644
--- a/core/ReportRenderer/Pdf.php
+++ b/core/ReportRenderer/Pdf.php
@@ -75,7 +75,7 @@ class Pdf extends ReportRenderer
private $reportColumns;
private $reportRowsMetadata;
private $currentPage = 0;
- private $reportFont = ReportRenderer::DEFAULT_REPORT_FONT;
+ private $reportFont = ReportRenderer::DEFAULT_REPORT_FONT_FAMILY;
private $TCPDF;
private $orientation = self::PORTRAIT;
@@ -115,7 +115,7 @@ class Pdf extends ReportRenderer
case 'en':
default:
- $reportFont = ReportRenderer::DEFAULT_REPORT_FONT;
+ $reportFont = ReportRenderer::DEFAULT_REPORT_FONT_FAMILY;
break;
}
$this->reportFont = $reportFont;
@@ -328,7 +328,7 @@ class Pdf extends ReportRenderer
$this->TCPDF->SetTextColor($this->reportTextColor[0], $this->reportTextColor[1], $this->reportTextColor[2]);
$this->TCPDF->SetFont('');
- $fill = false;
+ $fill = true;
$url = false;
$leftSpacesBeforeLogo = str_repeat(' ', $this->leftSpacesBeforeLogo);
@@ -494,12 +494,13 @@ class Pdf extends ReportRenderer
$posX = $initPosX;
foreach ($this->reportColumns as $columnName) {
$columnName = $this->formatText($columnName);
+
//Label column
if ($countColumns == 0) {
- $this->TCPDF->MultiCell($this->labelCellWidth, $maxCellHeight, $columnName, 1, 'C', true);
+ $this->TCPDF->MultiCell($this->labelCellWidth, $maxCellHeight, $columnName, $border = 0, $align = 'L', true);
$this->TCPDF->SetXY($posX + $this->labelCellWidth, $posY);
} else {
- $this->TCPDF->MultiCell($this->cellWidth, $maxCellHeight, $columnName, 1, 'C', true);
+ $this->TCPDF->MultiCell($this->cellWidth, $maxCellHeight, $columnName, $border = 0, $align = 'L', true);
$this->TCPDF->SetXY($posX + $this->cellWidth, $posY);
}
$countColumns++;
diff --git a/core/ScheduledTask.php b/core/ScheduledTask.php
index d570f5bea2..dd4063ef88 100644
--- a/core/ScheduledTask.php
+++ b/core/ScheduledTask.php
@@ -9,8 +9,7 @@
namespace Piwik;
-use Exception;
-use Piwik\ScheduledTime;
+use Piwik\Scheduler\Task;
/**
* Contains metadata referencing PHP code that should be executed at regular
@@ -18,188 +17,11 @@ use Piwik\ScheduledTime;
*
* See the {@link TaskScheduler} docs to learn more about scheduled tasks.
*
- *
* @api
+ *
+ * @deprecated Use Piwik\Scheduler\Task instead
+ * @see \Piwik\Scheduler\Task
*/
-class ScheduledTask
+class ScheduledTask extends Task
{
- const LOWEST_PRIORITY = 12;
- const LOW_PRIORITY = 9;
- const NORMAL_PRIORITY = 6;
- const HIGH_PRIORITY = 3;
- const HIGHEST_PRIORITY = 0;
-
- /**
- * Object instance on which the method will be executed by the task scheduler
- * @var string
- */
- private $objectInstance;
-
- /**
- * Class name where the specified method is located
- * @var string
- */
- private $className;
-
- /**
- * Class method to run when task is scheduled
- * @var string
- */
- private $methodName;
-
- /**
- * Parameter to pass to the executed method
- * @var string
- */
- private $methodParameter;
-
- /**
- * The scheduled time policy
- * @var ScheduledTime
- */
- private $scheduledTime;
-
- /**
- * The priority of a task. Affects the order in which this task will be run.
- * @var int
- */
- private $priority;
-
- /**
- * Constructor.
- *
- * @param mixed $objectInstance The object or class that contains the method to execute regularly.
- * Usually this will be a {@link Plugin} instance.
- * @param string $methodName The name of the method that will be regularly executed.
- * @param mixed|null $methodParameter An optional parameter to pass to the method when executed.
- * Must be convertible to string.
- * @param ScheduledTime|null $scheduledTime A {@link ScheduledTime} instance that describes when the method
- * should be executed and how long before the next execution.
- * @param int $priority The priority of the task. Tasks with a higher priority will be executed first.
- * Tasks with low priority will be executed last.
- * @throws Exception
- */
- public function __construct($objectInstance, $methodName, $methodParameter, $scheduledTime,
- $priority = self::NORMAL_PRIORITY)
- {
- $this->className = $this->getClassNameFromInstance($objectInstance);
-
- if ($priority < self::HIGHEST_PRIORITY || $priority > self::LOWEST_PRIORITY) {
- throw new Exception("Invalid priority for ScheduledTask '$this->className.$methodName': $priority");
- }
-
- $this->objectInstance = $objectInstance;
- $this->methodName = $methodName;
- $this->scheduledTime = $scheduledTime;
- $this->methodParameter = $methodParameter;
- $this->priority = $priority;
- }
-
- protected function getClassNameFromInstance($_objectInstance)
- {
- if (is_string($_objectInstance)) {
- return $_objectInstance;
- }
-
- $namespaced = get_class($_objectInstance);
-
- return $namespaced;
- }
-
- /**
- * Returns the object instance that contains the method to execute. Returns a class
- * name if the method is static.
- *
- * @return mixed
- */
- public function getObjectInstance()
- {
- return $this->objectInstance;
- }
-
- /**
- * Returns the name of the class that contains the method to execute.
- *
- * @return string
- */
- public function getClassName()
- {
- return $this->className;
- }
-
- /**
- * Returns the name of the method that will be executed.
- *
- * @return string
- */
- public function getMethodName()
- {
- return $this->methodName;
- }
-
- /**
- * Returns the value that will be passed to the method when executed, or `null` if
- * no value will be supplied.
- *
- * @return string|null
- */
- public function getMethodParameter()
- {
- return $this->methodParameter;
- }
-
- /**
- * Returns a {@link ScheduledTime} instance that describes when the method should be executed
- * and how long before the next execution.
- *
- * @return ScheduledTime
- */
- public function getScheduledTime()
- {
- return $this->scheduledTime;
- }
-
- /**
- * Returns the time in milliseconds when this task will be executed next.
- *
- * @return int
- */
- public function getRescheduledTime()
- {
- return $this->getScheduledTime()->getRescheduledTime();
- }
-
- /**
- * Returns the task priority. The priority will be an integer whose value is
- * between {@link HIGH_PRIORITY} and {@link LOW_PRIORITY}.
- *
- * @return int
- */
- public function getPriority()
- {
- return $this->priority;
- }
-
- /**
- * Returns a unique name for this scheduled task. The name is stored in the DB and is used
- * to store a task's previous execution time. The name is created using:
- *
- * - the name of the class that contains the method to execute,
- * - the name of the method to regularly execute,
- * - and the value that is passed to the executed task.
- *
- * @return string
- */
- public function getName()
- {
- return self::getTaskName($this->getClassName(), $this->getMethodName(), $this->getMethodParameter());
- }
-
- /**
- * @ignore
- */
- public static function getTaskName($className, $methodName, $methodParameter = null)
- {
- return $className . '.' . $methodName . ($methodParameter == null ? '' : '_' . $methodParameter);
- }
}
diff --git a/core/ScheduledTime/Daily.php b/core/Scheduler/Schedule/Daily.php
index df05dc2f38..7b1248f187 100644
--- a/core/ScheduledTime/Daily.php
+++ b/core/Scheduler/Schedule/Daily.php
@@ -6,17 +6,17 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
-namespace Piwik\ScheduledTime;
+
+namespace Piwik\Scheduler\Schedule;
use Exception;
-use Piwik\ScheduledTime;
/**
* Daily class is used to schedule tasks every day.
*
- * @see ScheduledTask
+ * @see \Piwik\Scheduler\Task
*/
-class Daily extends ScheduledTime
+class Daily extends Schedule
{
/**
* @see ScheduledTime::getRescheduledTime
diff --git a/core/ScheduledTime/Hourly.php b/core/Scheduler/Schedule/Hourly.php
index 4fc8c3ed16..5830d378ed 100644
--- a/core/ScheduledTime/Hourly.php
+++ b/core/Scheduler/Schedule/Hourly.php
@@ -6,18 +6,17 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
-namespace Piwik\ScheduledTime;
+
+namespace Piwik\Scheduler\Schedule;
use Exception;
-use Piwik\ScheduledTime;
/**
* Hourly class is used to schedule tasks every hour.
*
- * @see ScheduledTask
- *
+ * @see \Piwik\Scheduler\Task
*/
-class Hourly extends ScheduledTime
+class Hourly extends Schedule
{
/**
* @see ScheduledTime::getRescheduledTime
diff --git a/core/ScheduledTime/Monthly.php b/core/Scheduler/Schedule/Monthly.php
index 84189f9bad..64a96f69dd 100644
--- a/core/ScheduledTime/Monthly.php
+++ b/core/Scheduler/Schedule/Monthly.php
@@ -6,18 +6,17 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
-namespace Piwik\ScheduledTime;
+
+namespace Piwik\Scheduler\Schedule;
use Exception;
-use Piwik\ScheduledTime;
/**
* Monthly class is used to schedule tasks every month.
*
- * @see ScheduledTask
- *
+ * @see \Piwik\Scheduler\Task
*/
-class Monthly extends ScheduledTime
+class Monthly extends Schedule
{
/**
* List of available week number strings used in setDayOfWeekFromString.
@@ -50,7 +49,7 @@ class Monthly extends ScheduledTime
if (isset(self::$weekNumberStringToInt[$weekNumberString])) {
$week = self::$weekNumberStringToInt[$weekNumberString];
} else {
- throw new Exception("Invalid week describer in ScheduledTime\\Monthly::setDayOfWeekFromString: '$weekNumberString'. "
+ throw new Exception("Invalid week describer in Schedule\\Monthly::setDayOfWeekFromString: '$weekNumberString'. "
. "Supported values are 'first', 'second', 'third', 'fourth'.");
}
diff --git a/core/ScheduledTime.php b/core/Scheduler/Schedule/Schedule.php
index 3da4afe98b..4642389b44 100644
--- a/core/ScheduledTime.php
+++ b/core/Scheduler/Schedule/Schedule.php
@@ -7,21 +7,18 @@
*
*/
-namespace Piwik;
+namespace Piwik\Scheduler\Schedule;
use Exception;
-use Piwik\ScheduledTime\Daily;
-use Piwik\ScheduledTime\Hourly;
-use Piwik\ScheduledTime\Monthly;
-use Piwik\ScheduledTime\Weekly;
+use Piwik\Date;
/**
* Describes the interval on which a scheduled task is executed. Use the {@link factory()} method
- * to create ScheduledTime instances.
+ * to create Schedule instances.
*
- * @see \Piwik\ScheduledTask
+ * @see \Piwik\Scheduler\Task
*/
-abstract class ScheduledTime
+abstract class Schedule
{
const PERIOD_NEVER = 'never';
const PERIOD_DAY = 'day';
@@ -95,7 +92,7 @@ abstract class ScheduledTime
* Sets the day of the period to execute the scheduled task. Not a valid operation for all period types.
*
* @abstract
- * @param int $_day a number describing the day to set. Its meaning depends on the ScheduledTime's period type.
+ * @param int $_day a number describing the day to set. Its meaning depends on the Schedule's period type.
* @throws Exception if method not supported by subclass or parameter _day is invalid
*/
abstract public function setDay($_day);
@@ -179,7 +176,7 @@ abstract class ScheduledTime
}
/**
- * Returns a new ScheduledTime instance using a string description of the scheduled period type
+ * Returns a new Schedule instance using a string description of the scheduled period type
* and a string description of the day within the period to execute the task on.
*
* @param string $periodType The scheduled period type. Can be `'hourly'`, `'daily'`, `'weekly'`, or `'monthly'`.
@@ -192,6 +189,7 @@ abstract class ScheduledTime
* If `'monthly'` is supplied for `$periodType`, this can be a numeric
* day in the month or a day in one week of the month. For example,
* `12`, `23`, `'first sunday'` or `'fourth tuesday'`.
+ * @return Hourly|Daily|Weekly|Monthly
* @throws Exception
* @api
*/
diff --git a/core/ScheduledTime/Weekly.php b/core/Scheduler/Schedule/Weekly.php
index 5ae499b9eb..2f984c2db6 100644
--- a/core/ScheduledTime/Weekly.php
+++ b/core/Scheduler/Schedule/Weekly.php
@@ -6,18 +6,17 @@
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*
*/
-namespace Piwik\ScheduledTime;
+
+namespace Piwik\Scheduler\Schedule;
use Exception;
-use Piwik\ScheduledTime;
/**
* Weekly class is used to schedule tasks every week.
*
- * @see ScheduledTask
- *
+ * @see \Piwik\Scheduler\Task
*/
-class Weekly extends ScheduledTime
+class Weekly extends Schedule
{
/**
* @see ScheduledTime::getRescheduledTime
diff --git a/core/Scheduler/Scheduler.php b/core/Scheduler/Scheduler.php
new file mode 100644
index 0000000000..7156e1be9c
--- /dev/null
+++ b/core/Scheduler/Scheduler.php
@@ -0,0 +1,192 @@
+<?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\Scheduler;
+
+use Exception;
+use Piwik\Timer;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Schedules task execution.
+ *
+ * A scheduled task is a callback that should be executed every so often (such as daily,
+ * weekly, monthly, etc.). They are registered by extending {@link \Piwik\Plugin\Tasks}.
+ *
+ * Tasks are executed when the `core:archive` command is executed.
+ *
+ * ### Examples
+ *
+ * **Scheduling a task**
+ *
+ * class Tasks extends \Piwik\Plugin\Tasks
+ * {
+ * public function schedule()
+ * {
+ * $this->hourly('myTask'); // myTask() will be executed once every hour
+ * }
+ * public function myTask()
+ * {
+ * // do something
+ * }
+ * }
+ *
+ * **Executing all pending tasks**
+ *
+ * $results = $scheduler->run();
+ * $task1Result = $results[0];
+ * $task1Name = $task1Result['task'];
+ * $task1Output = $task1Result['output'];
+ *
+ * echo "Executed task '$task1Name'. Task output:\n$task1Output";
+ */
+class Scheduler
+{
+ /**
+ * Is the scheduler running any task.
+ * @var bool
+ */
+ private $isRunningTask = false;
+
+ /**
+ * @var Timetable
+ */
+ private $timetable;
+
+ /**
+ * @var TaskLoader
+ */
+ private $loader;
+
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ public function __construct(TaskLoader $loader, LoggerInterface $logger)
+ {
+ $this->timetable = new Timetable();
+ $this->loader = $loader;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Executes tasks that are scheduled to run, then reschedules them.
+ *
+ * @return array An array describing the results of scheduled task execution. Each element
+ * in the array will have the following format:
+ *
+ * ```
+ * array(
+ * 'task' => 'task name',
+ * 'output' => '... task output ...'
+ * )
+ * ```
+ */
+ public function run()
+ {
+ $tasks = $this->loader->loadTasks();
+
+ $this->logger->debug('{count} scheduled tasks loaded', array('count' => count($tasks)));
+
+ // remove from timetable tasks that are not active anymore
+ $this->timetable->removeInactiveTasks($tasks);
+
+ // for every priority level, starting with the highest and concluding with the lowest
+ $executionResults = array();
+ for ($priority = Task::HIGHEST_PRIORITY;
+ $priority <= Task::LOWEST_PRIORITY;
+ ++$priority) {
+ // loop through each task
+ foreach ($tasks as $task) {
+ // if the task does not have the current priority level, don't execute it yet
+ if ($task->getPriority() != $priority) {
+ continue;
+ }
+
+ $taskName = $task->getName();
+ $shouldExecuteTask = $this->timetable->shouldExecuteTask($taskName);
+
+ if ($this->timetable->taskShouldBeRescheduled($taskName)) {
+ $this->timetable->rescheduleTask($task);
+ }
+
+ if ($shouldExecuteTask) {
+ $this->isRunningTask = true;
+ $message = $this->executeTask($task);
+ $this->isRunningTask = false;
+
+ $executionResults[] = array('task' => $taskName, 'output' => $message);
+ }
+ }
+ }
+
+ return $executionResults;
+ }
+
+ /**
+ * Determines a task's scheduled time and persists it, overwriting the previous scheduled time.
+ *
+ * Call this method if your task's scheduled time has changed due to, for example, an option that
+ * was changed.
+ *
+ * @param Task $task Describes the scheduled task being rescheduled.
+ * @api
+ */
+ public function rescheduleTask(Task $task)
+ {
+ $this->logger->debug('Rescheduling task {task}', array('task' => $task->getName()));
+
+ $this->timetable->rescheduleTask($task);
+ }
+
+ /**
+ * Returns true if the scheduler is currently running a task.
+ *
+ * @return bool
+ */
+ public function isRunningTask()
+ {
+ return $this->isRunningTask;
+ }
+
+ /**
+ * Return the next scheduled time given the class and method names of a scheduled task.
+ *
+ * @param string $className The name of the class that contains the scheduled task method.
+ * @param string $methodName The name of the scheduled task method.
+ * @param string|null $methodParameter Optional method parameter.
+ * @return mixed int|bool The time in miliseconds when the scheduled task will be executed
+ * next or false if it is not scheduled to run.
+ */
+ public function getScheduledTimeForMethod($className, $methodName, $methodParameter = null)
+ {
+ return $this->timetable->getScheduledTimeForMethod($className, $methodName, $methodParameter);
+ }
+
+ /**
+ * Executes the given task
+ *
+ * @param Task $task
+ * @return string
+ */
+ private function executeTask($task)
+ {
+ $this->logger->debug('Running task {task}', array('task' => $task->getName()));
+
+ try {
+ $timer = new Timer();
+ call_user_func(array($task->getObjectInstance(), $task->getMethodName()), $task->getMethodParameter());
+ $message = $timer->__toString();
+ } catch (Exception $e) {
+ $message = 'ERROR: ' . $e->getMessage();
+ }
+
+ return $message;
+ }
+}
diff --git a/core/Scheduler/Task.php b/core/Scheduler/Task.php
new file mode 100644
index 0000000000..f3c4381a22
--- /dev/null
+++ b/core/Scheduler/Task.php
@@ -0,0 +1,200 @@
+<?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\Scheduler;
+
+use Exception;
+use Piwik\Scheduler\Schedule\Schedule;
+
+/**
+ * Describes a task that should be executed on a given time.
+ *
+ * See the {@link TaskScheduler} docs to learn more about scheduled tasks.
+ *
+ * @api
+ */
+class Task
+{
+ const LOWEST_PRIORITY = 12;
+ const LOW_PRIORITY = 9;
+ const NORMAL_PRIORITY = 6;
+ const HIGH_PRIORITY = 3;
+ const HIGHEST_PRIORITY = 0;
+
+ /**
+ * Object instance on which the method will be executed by the task scheduler
+ * @var string
+ */
+ private $objectInstance;
+
+ /**
+ * Class name where the specified method is located
+ * @var string
+ */
+ private $className;
+
+ /**
+ * Class method to run when task is scheduled
+ * @var string
+ */
+ private $methodName;
+
+ /**
+ * Parameter to pass to the executed method
+ * @var string
+ */
+ private $methodParameter;
+
+ /**
+ * The scheduled time policy
+ * @var Schedule
+ */
+ private $scheduledTime;
+
+ /**
+ * The priority of a task. Affects the order in which this task will be run.
+ * @var int
+ */
+ private $priority;
+
+ /**
+ * @param mixed $objectInstance The object or class that contains the method to execute regularly.
+ * Usually this will be a {@link Plugin} instance.
+ * @param string $methodName The name of the method that will be regularly executed.
+ * @param mixed|null $methodParameter An optional parameter to pass to the method when executed.
+ * Must be convertible to string.
+ * @param Schedule|null $scheduledTime A {@link Schedule} instance that describes when the method
+ * should be executed and how long before the next execution.
+ * @param int $priority The priority of the task. Tasks with a higher priority will be executed first.
+ * Tasks with low priority will be executed last.
+ * @throws Exception
+ */
+ public function __construct($objectInstance, $methodName, $methodParameter, $scheduledTime,
+ $priority = self::NORMAL_PRIORITY)
+ {
+ $this->className = $this->getClassNameFromInstance($objectInstance);
+
+ if ($priority < self::HIGHEST_PRIORITY || $priority > self::LOWEST_PRIORITY) {
+ throw new Exception("Invalid priority for ScheduledTask '$this->className.$methodName': $priority");
+ }
+
+ $this->objectInstance = $objectInstance;
+ $this->methodName = $methodName;
+ $this->scheduledTime = $scheduledTime;
+ $this->methodParameter = $methodParameter;
+ $this->priority = $priority;
+ }
+
+ protected function getClassNameFromInstance($_objectInstance)
+ {
+ if (is_string($_objectInstance)) {
+ return $_objectInstance;
+ }
+
+ $namespaced = get_class($_objectInstance);
+
+ return $namespaced;
+ }
+
+ /**
+ * Returns the object instance that contains the method to execute. Returns a class
+ * name if the method is static.
+ *
+ * @return mixed
+ */
+ public function getObjectInstance()
+ {
+ return $this->objectInstance;
+ }
+
+ /**
+ * Returns the name of the class that contains the method to execute.
+ *
+ * @return string
+ */
+ public function getClassName()
+ {
+ return $this->className;
+ }
+
+ /**
+ * Returns the name of the method that will be executed.
+ *
+ * @return string
+ */
+ public function getMethodName()
+ {
+ return $this->methodName;
+ }
+
+ /**
+ * Returns the value that will be passed to the method when executed, or `null` if
+ * no value will be supplied.
+ *
+ * @return string|null
+ */
+ public function getMethodParameter()
+ {
+ return $this->methodParameter;
+ }
+
+ /**
+ * Returns a {@link Schedule} instance that describes when the method should be executed
+ * and how long before the next execution.
+ *
+ * @return \Piwik\Scheduler\Schedule\Schedule
+ */
+ public function getScheduledTime()
+ {
+ return $this->scheduledTime;
+ }
+
+ /**
+ * Returns the time in milliseconds when this task will be executed next.
+ *
+ * @return int
+ */
+ public function getRescheduledTime()
+ {
+ return $this->getScheduledTime()->getRescheduledTime();
+ }
+
+ /**
+ * Returns the task priority. The priority will be an integer whose value is
+ * between {@link HIGH_PRIORITY} and {@link LOW_PRIORITY}.
+ *
+ * @return int
+ */
+ public function getPriority()
+ {
+ return $this->priority;
+ }
+
+ /**
+ * Returns a unique name for this scheduled task. The name is stored in the DB and is used
+ * to store a task's previous execution time. The name is created using:
+ *
+ * - the name of the class that contains the method to execute,
+ * - the name of the method to regularly execute,
+ * - and the value that is passed to the executed task.
+ *
+ * @return string
+ */
+ public function getName()
+ {
+ return self::getTaskName($this->getClassName(), $this->getMethodName(), $this->getMethodParameter());
+ }
+
+ /**
+ * @ignore
+ */
+ public static function getTaskName($className, $methodName, $methodParameter = null)
+ {
+ return $className . '.' . $methodName . ($methodParameter == null ? '' : '_' . $methodParameter);
+ }
+}
diff --git a/core/Scheduler/TaskLoader.php b/core/Scheduler/TaskLoader.php
new file mode 100644
index 0000000000..44ec80c54b
--- /dev/null
+++ b/core/Scheduler/TaskLoader.php
@@ -0,0 +1,40 @@
+<?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\Scheduler;
+
+use Piwik\Plugin\Manager as PluginManager;
+use Piwik\Plugin\Tasks;
+
+/**
+ * Loads scheduled tasks.
+ */
+class TaskLoader
+{
+ /**
+ * @return Task[]
+ */
+ public function loadTasks()
+ {
+ $tasks = array();
+
+ /** @var Tasks[] $pluginTasks */
+ $pluginTasks = PluginManager::getInstance()->findComponents('Tasks', 'Piwik\Plugin\Tasks');
+
+ foreach ($pluginTasks as $pluginTask) {
+
+ $pluginTask->schedule();
+
+ foreach ($pluginTask->getScheduledTasks() as $task) {
+ $tasks[] = $task;
+ }
+ }
+
+ return $tasks;
+ }
+}
diff --git a/core/ScheduledTaskTimetable.php b/core/Scheduler/Timetable.php
index 92aa1b340e..5c9bc69696 100644
--- a/core/ScheduledTaskTimetable.php
+++ b/core/Scheduler/Timetable.php
@@ -7,12 +7,15 @@
*
*/
-namespace Piwik;
+namespace Piwik\Scheduler;
+
+use Piwik\Option;
+use Piwik\Date;
/**
* This data structure holds the scheduled times for each active scheduled task.
*/
-class ScheduledTaskTimetable
+class Timetable
{
const TIMETABLE_OPTION_STRING = "TaskScheduler.timetable";
@@ -36,6 +39,9 @@ class ScheduledTaskTimetable
$this->timetable = $timetable;
}
+ /**
+ * @param Task[] $activeTasks
+ */
public function removeInactiveTasks($activeTasks)
{
$activeTaskNames = array();
@@ -74,7 +80,11 @@ class ScheduledTaskTimetable
{
$forceTaskExecution = (defined('DEBUG_FORCE_SCHEDULED_TASKS') && DEBUG_FORCE_SCHEDULED_TASKS);
- return $forceTaskExecution || ($this->taskHasBeenScheduledOnce($taskName) && time() >= $this->timetable[$taskName]);
+ if ($forceTaskExecution) {
+ return true;
+ }
+
+ return $this->taskHasBeenScheduledOnce($taskName) && time() >= $this->timetable[$taskName];
}
/**
@@ -93,7 +103,7 @@ class ScheduledTaskTimetable
return !$this->taskHasBeenScheduledOnce($taskName) || $this->shouldExecuteTask($taskName);
}
- public function rescheduleTask($task)
+ public function rescheduleTask(Task $task)
{
// update the scheduled time
$this->timetable[$task->getName()] = $task->getRescheduledTime();
@@ -107,7 +117,7 @@ class ScheduledTaskTimetable
public function getScheduledTimeForMethod($className, $methodName, $methodParameter = null)
{
- $taskName = ScheduledTask::getTaskName($className, $methodName, $methodParameter);
+ $taskName = Task::getTaskName($className, $methodName, $methodParameter);
return $this->taskHasBeenScheduledOnce($taskName) ? $this->timetable[$taskName] : false;
}
diff --git a/core/Segment.php b/core/Segment.php
index 420946794d..487245cf1c 100644
--- a/core/Segment.php
+++ b/core/Segment.php
@@ -9,7 +9,9 @@
namespace Piwik;
use Exception;
+use Piwik\DataAccess\LogQueryBuilder;
use Piwik\Plugins\API\API;
+use Piwik\Segment\SegmentExpression;
/**
* Limits the set of visits Piwik uses when aggregating analytics data.
@@ -57,7 +59,17 @@ class Segment
/**
* @var SegmentExpression
*/
- protected $segment = null;
+ protected $segmentExpression = null;
+
+ /**
+ * @var string
+ */
+ protected $string = null;
+
+ /**
+ * @var array
+ */
+ protected $idSites = null;
/**
* Truncate the Segments to 8k
@@ -70,7 +82,7 @@ class Segment
* @param string $segmentCondition The segment condition, eg, `'browserCode=ff;countryCode=CA'`.
* @param array $idSites The list of sites the segment will be used with. Some segments are
* dependent on the site, such as goal segments.
- * @throws Exception
+ * @throws
*/
public function __construct($segmentCondition, $idSites)
{
@@ -103,7 +115,7 @@ class Segment
$this->string = $string;
$this->idSites = $idSites;
$segment = new SegmentExpression($string);
- $this->segment = $segment;
+ $this->segmentExpression = $segment;
// parse segments
$expressions = $segment->parseSubExpressions();
@@ -127,7 +139,7 @@ class Segment
*/
public function isEmpty()
{
- return empty($this->string);
+ return $this->segmentExpression->isEmpty();
}
protected $availableSegments = array();
@@ -222,236 +234,16 @@ class Segment
* @param array|string $bind (optional) Bind parameters, eg, `array($col1Value, $col2Value)`.
* @param false|string $orderBy (optional) Order by clause, eg, `"t1.col1 ASC"`.
* @param false|string $groupBy (optional) Group by clause, eg, `"t2.col2"`.
+ * @param int $limit Limit by clause
+ * @param int If set to value >= 1 then the Select query (and All inner queries) will be LIMIT'ed by this value.
+ * Use only when you're not aggregating or it will sample the data.
* @return string The entire select query.
*/
- public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false)
- {
- if (!is_array($from)) {
- $from = array($from);
- }
-
- if (!$this->isEmpty()) {
- $this->segment->parseSubExpressionsIntoSqlExpressions($from);
-
- $joins = $this->generateJoins($from);
- $from = $joins['sql'];
- $joinWithSubSelect = $joins['joinWithSubSelect'];
-
- $segmentSql = $this->segment->getSql();
- $segmentWhere = $segmentSql['where'];
- if (!empty($segmentWhere)) {
- if (!empty($where)) {
- $where = "( $where )
- AND
- ($segmentWhere)";
- } else {
- $where = $segmentWhere;
- }
- }
-
- $bind = array_merge($bind, $segmentSql['bind']);
- } else {
- $joins = $this->generateJoins($from);
- $from = $joins['sql'];
- $joinWithSubSelect = $joins['joinWithSubSelect'];
- }
-
- if ($joinWithSubSelect) {
- $sql = $this->buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy);
- } else {
- $sql = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy);
- }
- return array(
- 'sql' => $sql,
- 'bind' => $bind
- );
- }
-
- /**
- * Generate the join sql based on the needed tables
- * @param array $tables tables to join
- * @throws Exception if tables can't be joined
- * @return array
- */
- private function generateJoins($tables)
- {
- $knownTables = array("log_visit", "log_link_visit_action", "log_conversion", "log_conversion_item");
- $visitsAvailable = $actionsAvailable = $conversionsAvailable = $conversionItemAvailable = false;
- $joinWithSubSelect = false;
- $sql = '';
-
- // make sure the tables are joined in the right order
- // base table first, then action before conversion
- // this way, conversions can be joined on idlink_va
- $actionIndex = array_search("log_link_visit_action", $tables);
- $conversionIndex = array_search("log_conversion", $tables);
- if ($actionIndex > 0 && $conversionIndex > 0 && $actionIndex > $conversionIndex) {
- $tables[$actionIndex] = "log_conversion";
- $tables[$conversionIndex] = "log_link_visit_action";
- }
-
- // same as above: action before visit
- $actionIndex = array_search("log_link_visit_action", $tables);
- $visitIndex = array_search("log_visit", $tables);
- if ($actionIndex > 0 && $visitIndex > 0 && $actionIndex > $visitIndex) {
- $tables[$actionIndex] = "log_visit";
- $tables[$visitIndex] = "log_link_visit_action";
- }
-
- foreach ($tables as $i => $table) {
- if (is_array($table)) {
- // join condition provided
- $alias = isset($table['tableAlias']) ? $table['tableAlias'] : $table['table'];
- $sql .= "
- LEFT JOIN " . Common::prefixTable($table['table']) . " AS " . $alias
- . " ON " . $table['joinOn'];
- continue;
- }
-
- if (!in_array($table, $knownTables)) {
- throw new Exception("Table '$table' can't be used for segmentation");
- }
-
- $tableSql = Common::prefixTable($table) . " AS $table";
-
- if ($i == 0) {
- // first table
- $sql .= $tableSql;
- } else {
- if ($actionsAvailable && $table == "log_conversion") {
- // have actions, need conversions => join on idlink_va
- $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va "
- . "AND log_conversion.idsite = log_link_visit_action.idsite";
- } else if ($actionsAvailable && $table == "log_visit") {
- // have actions, need visits => join on idvisit
- $join = "log_visit.idvisit = log_link_visit_action.idvisit";
- } else if ($visitsAvailable && $table == "log_link_visit_action") {
- // have visits, need actions => we have to use a more complex join
- // we don't hande this here, we just return joinWithSubSelect=true in this case
- $joinWithSubSelect = true;
- $join = "log_link_visit_action.idvisit = log_visit.idvisit";
- } else if ($conversionsAvailable && $table == "log_link_visit_action") {
- // have conversions, need actions => join on idlink_va
- $join = "log_conversion.idlink_va = log_link_visit_action.idlink_va";
- } else if (($visitsAvailable && $table == "log_conversion")
- || ($conversionsAvailable && $table == "log_visit")
- ) {
- // have visits, need conversion (or vice versa) => join on idvisit
- // notice that joining conversions on visits has lower priority than joining it on actions
- $join = "log_conversion.idvisit = log_visit.idvisit";
-
- // if conversions are joined on visits, we need a complex join
- if ($table == "log_conversion") {
- $joinWithSubSelect = true;
- }
- } elseif ($conversionItemAvailable && $table === 'log_visit') {
- $join = "log_conversion_item.idvisit = log_visit.idvisit";
- } elseif ($conversionItemAvailable && $table === 'log_link_visit_action') {
- $join = "log_conversion_item.idvisit = log_link_visit_action.idvisit";
- } elseif ($conversionItemAvailable && $table === 'log_conversion') {
- $join = "log_conversion_item.idvisit = log_conversion.idvisit";
- } else {
- throw new Exception("Table '$table' can't be joined for segmentation");
- }
-
- // the join sql the default way
- $sql .= "
- LEFT JOIN $tableSql ON $join";
- }
-
- // remember which tables are available
- $visitsAvailable = ($visitsAvailable || $table == "log_visit");
- $actionsAvailable = ($actionsAvailable || $table == "log_link_visit_action");
- $conversionsAvailable = ($conversionsAvailable || $table == "log_conversion");
- $conversionItemAvailable = ($conversionItemAvailable || $table == "log_conversion_item");
- }
-
- $return = array(
- 'sql' => $sql,
- 'joinWithSubSelect' => $joinWithSubSelect
- );
- return $return;
-
- }
-
- /**
- * Build select query the normal way
- * @param string $select fieldlist to be selected
- * @param string $from tablelist to select from
- * @param string $where where clause
- * @param string $orderBy order by clause
- * @param string $groupBy group by clause
- * @return string
- */
- private function buildSelectQuery($select, $from, $where, $orderBy, $groupBy)
- {
- $sql = "
- SELECT
- $select
- FROM
- $from";
-
- if ($where) {
- $sql .= "
- WHERE
- $where";
- }
-
- if ($groupBy) {
- $sql .= "
- GROUP BY
- $groupBy";
- }
-
- if ($orderBy) {
- $sql .= "
- ORDER BY
- $orderBy";
- }
-
- return $sql;
- }
-
- /**
- * Build a select query where actions have to be joined on visits (or conversions)
- * In this case, the query gets wrapped in another query so that grouping by visit is possible
- * @param string $select
- * @param string $from
- * @param string $where
- * @param string $orderBy
- * @param string $groupBy
- * @throws Exception
- * @return string
- */
- private function buildWrappedSelectQuery($select, $from, $where, $orderBy, $groupBy)
+ public function getSelectQuery($select, $from, $where = false, $bind = array(), $orderBy = false, $groupBy = false, $limit = 0)
{
- $matchTables = "(log_visit|log_conversion_item|log_conversion|log_action)";
- preg_match_all("/". $matchTables ."\.[a-z0-9_\*]+/", $select, $matches);
- $neededFields = array_unique($matches[0]);
-
- if (count($neededFields) == 0) {
- throw new Exception("No needed fields found in select expression. "
- . "Please use a table prefix.");
- }
-
- $select = preg_replace('/'.$matchTables.'\./', 'log_inner.', $select);
- $orderBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $orderBy);
- $groupBy = preg_replace('/'.$matchTables.'\./', 'log_inner.', $groupBy);
-
- $from = "(
- SELECT
- " . implode(",
- ", $neededFields) . "
- FROM
- $from
- WHERE
- $where
- GROUP BY log_visit.idvisit
- ) AS log_inner";
-
- $where = false;
- $query = $this->buildSelectQuery($select, $from, $where, $orderBy, $groupBy);
- return $query;
+ $segmentExpression = $this->segmentExpression;
+ $segmentQuery = new LogQueryBuilder($segmentExpression);
+ return $segmentQuery->getSelectQueryString($select, $from, $where, $bind, $groupBy, $orderBy, $limit);
}
/**
@@ -463,4 +255,5 @@ class Segment
{
return (string) $this->getString();
}
+
} \ No newline at end of file
diff --git a/core/SegmentExpression.php b/core/Segment/SegmentExpression.php
index cbd2a3eb9e..10cf0294e3 100644
--- a/core/SegmentExpression.php
+++ b/core/Segment/SegmentExpression.php
@@ -7,7 +7,7 @@
*
*/
-namespace Piwik;
+namespace Piwik\Segment;
use Exception;
@@ -46,6 +46,11 @@ class SegmentExpression
$this->tree = $this->parseTree();
}
+ public function isEmpty()
+ {
+ return count($this->tree) == 0;
+ }
+
protected $joins = array();
protected $valuesBind = array();
protected $parsedTree = array();
@@ -337,7 +342,7 @@ class SegmentExpression
*/
public function getSql()
{
- if (count($this->tree) == 0) {
+ if ($this->isEmpty()) {
throw new Exception("Invalid segment, please specify a valid segment.");
}
$sql = '';
diff --git a/core/Session/SessionNamespace.php b/core/Session/SessionNamespace.php
index 74b11e0ce8..90a46625ff 100644
--- a/core/Session/SessionNamespace.php
+++ b/core/Session/SessionNamespace.php
@@ -9,6 +9,7 @@
namespace Piwik\Session;
use Piwik\Common;
+use Piwik\Session;
use Zend_Session_Namespace;
/**
@@ -28,6 +29,8 @@ class SessionNamespace extends Zend_Session_Namespace
return;
}
+ Session::start();
+
parent::__construct($namespace, $singleInstance);
}
}
diff --git a/core/Settings/Manager.php b/core/Settings/Manager.php
index bbe696792b..f3862989e1 100644
--- a/core/Settings/Manager.php
+++ b/core/Settings/Manager.php
@@ -102,22 +102,57 @@ class Manager
return $settingsForUser;
}
- public static function hasPluginSettingsForCurrentUser($pluginName)
+ public static function hasSystemPluginSettingsForCurrentUser($pluginName)
{
- $pluginNames = array_keys(static::getPluginSettingsForCurrentUser());
+ $pluginNames = static::getPluginNamesHavingSystemSettings();
return in_array($pluginName, $pluginNames);
}
/**
- * Detects whether there are settings for activated plugins available that the current user can change.
+ * Detects whether there are user settings for activated plugins available that the current user can change.
*
* @return bool
*/
- public static function hasPluginsSettingsForCurrentUser()
+ public static function hasUserPluginsSettingsForCurrentUser()
{
$settings = static::getPluginSettingsForCurrentUser();
+ foreach ($settings as $setting) {
+ foreach ($setting->getSettingsForCurrentUser() as $set) {
+ if ($set instanceof UserSetting) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static function getPluginNamesHavingSystemSettings()
+ {
+ $settings = static::getPluginSettingsForCurrentUser();
+ $plugins = array();
+
+ foreach ($settings as $pluginName => $setting) {
+ foreach ($setting->getSettingsForCurrentUser() as $set) {
+ if ($set instanceof SystemSetting) {
+ $plugins[] = $pluginName;
+ }
+ }
+ }
+
+ return array_unique($plugins);
+ }
+ /**
+ * Detects whether there are system settings for activated plugins available that the current user can change.
+ *
+ * @return bool
+ */
+ public static function hasSystemPluginsSettingsForCurrentUser()
+ {
+ $settings = static::getPluginNamesHavingSystemSettings();
+
return !empty($settings);
}
diff --git a/core/Settings/Storage/StaticStorage.php b/core/Settings/Storage/StaticStorage.php
new file mode 100644
index 0000000000..ada437fa1c
--- /dev/null
+++ b/core/Settings/Storage/StaticStorage.php
@@ -0,0 +1,34 @@
+<?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\Settings\Storage;
+use Piwik\Settings\Storage;
+
+/**
+ * Static / temporary storage where a value will never be persisted meaning it will use the default value
+ * for each request until configured differently. Useful for tests.
+ *
+ * @api
+ */
+class StaticStorage extends Storage
+{
+
+ protected function loadSettings()
+ {
+ return array();
+ }
+
+ /**
+ * Saves (persists) the current setting values in the database.
+ */
+ public function save()
+ {
+ }
+
+}
diff --git a/core/SettingsPiwik.php b/core/SettingsPiwik.php
index 1291c7fe13..6a30ddfecb 100644
--- a/core/SettingsPiwik.php
+++ b/core/SettingsPiwik.php
@@ -9,7 +9,7 @@
namespace Piwik;
use Exception;
-use Piwik\Container\StaticContainer;
+use Piwik\Cache as PiwikCache;
/**
* Contains helper methods that can be used to get common Piwik settings.
@@ -18,6 +18,7 @@ use Piwik\Container\StaticContainer;
class SettingsPiwik
{
const OPTION_PIWIK_URL = 'piwikUrl';
+
/**
* Get salt from [General] section
*
@@ -43,56 +44,54 @@ class SettingsPiwik
}
/**
- * @see getKnownSegmentsToArchive
- *
- * @var array
- */
- public static $cachedKnownSegmentsToArchive = null;
-
- /**
* Returns every stored segment to pre-process for each site during cron archiving.
*
* @return array The list of stored segments that apply to all sites.
*/
public static function getKnownSegmentsToArchive()
{
- if (self::$cachedKnownSegmentsToArchive === null) {
- $segments = Config::getInstance()->Segments;
- $segmentsToProcess = isset($segments['Segments']) ? $segments['Segments'] : array();
-
- /**
- * Triggered during the cron archiving process to collect segments that
- * should be pre-processed for all websites. The archiving process will be launched
- * for each of these segments when archiving data.
- *
- * This event can be used to add segments to be pre-processed. If your plugin depends
- * on data from a specific segment, this event could be used to provide enhanced
- * performance.
- *
- * _Note: If you just want to add a segment that is managed by the user, use the
- * SegmentEditor API._
- *
- * **Example**
- *
- * Piwik::addAction('Segments.getKnownSegmentsToArchiveAllSites', function (&$segments) {
- * $segments[] = 'country=jp;city=Tokyo';
- * });
- *
- * @param array &$segmentsToProcess List of segment definitions, eg,
- *
- * array(
- * 'browserCode=ff;resolution=800x600',
- * 'country=jp;city=Tokyo'
- * )
- *
- * Add segments to this array in your event handler.
- */
- Piwik::postEvent('Segments.getKnownSegmentsToArchiveAllSites', array(&$segmentsToProcess));
-
- self::$cachedKnownSegmentsToArchive = array_unique($segmentsToProcess);
+ $cacheId = 'KnownSegmentsToArchive';
+ $cache = PiwikCache::getTransientCache();
+ if ($cache->contains($cacheId)) {
+ return $cache->fetch($cacheId);
}
- return self::$cachedKnownSegmentsToArchive;
+ $segments = Config::getInstance()->Segments;
+ $segmentsToProcess = isset($segments['Segments']) ? $segments['Segments'] : array();
+
+ /**
+ * Triggered during the cron archiving process to collect segments that
+ * should be pre-processed for all websites. The archiving process will be launched
+ * for each of these segments when archiving data.
+ *
+ * This event can be used to add segments to be pre-processed. If your plugin depends
+ * on data from a specific segment, this event could be used to provide enhanced
+ * performance.
+ *
+ * _Note: If you just want to add a segment that is managed by the user, use the
+ * SegmentEditor API._
+ *
+ * **Example**
+ *
+ * Piwik::addAction('Segments.getKnownSegmentsToArchiveAllSites', function (&$segments) {
+ * $segments[] = 'country=jp;city=Tokyo';
+ * });
+ *
+ * @param array &$segmentsToProcess List of segment definitions, eg,
+ *
+ * array(
+ * 'browserCode=ff;resolution=800x600',
+ * 'country=jp;city=Tokyo'
+ * )
+ *
+ * Add segments to this array in your event handler.
+ */
+ Piwik::postEvent('Segments.getKnownSegmentsToArchiveAllSites', array(&$segmentsToProcess));
+
+ $segmentsToProcess = array_unique($segmentsToProcess);
+
+ $cache->save($cacheId, $segmentsToProcess);
+ return $segmentsToProcess;
}
/**
@@ -104,8 +103,13 @@ class SettingsPiwik
*/
public static function getKnownSegmentsToArchiveForSite($idSite)
{
- $segments = array();
+ $cacheId = 'KnownSegmentsToArchiveForSite' . $idSite;
+ $cache = PiwikCache::getTransientCache();
+ if ($cache->contains($cacheId)) {
+ return $cache->fetch($cacheId);
+ }
+ $segments = array();
/**
* Triggered during the cron archiving process to collect segments that
* should be pre-processed for one specific site. The archiving process will be launched
@@ -133,6 +137,11 @@ class SettingsPiwik
* @param int $idSite The ID of the site to get segments for.
*/
Piwik::postEvent('Segments.getKnownSegmentsToArchiveForSite', array(&$segments, $idSite));
+
+ $segments = array_unique($segments);
+
+ $cache->save($cacheId, $segments);
+
return $segments;
}
@@ -319,7 +328,12 @@ class SettingsPiwik
// this will match when Piwik is installed and favicon has been customised
$expectedString = 'misc/user/';
- $expectedStringNotFound = strpos($fetched, $expectedString) === false && strpos($fetched, $expectedStringAlt) === false;
+ // see checkPiwikIsNotInstalled()
+ $expectedStringAlreadyInstalled = 'piwik-is-already-installed';
+
+ $expectedStringNotFound = strpos($fetched, $expectedString) === false
+ && strpos($fetched, $expectedStringAlt) === false
+ && strpos($fetched, $expectedStringAlreadyInstalled) === false;
$hasError = false !== strpos($fetched, PAGE_TITLE_WHEN_ERROR);
@@ -429,4 +443,14 @@ class SettingsPiwik
return Config::getInstance()->General['force_ssl'] == 1;
}
+ /**
+ * Note: this config settig is also checked in the InterSites plugin
+ *
+ * @return bool
+ */
+ public static function isSameFingerprintAcrossWebsites()
+ {
+ return (bool)Config::getInstance()->Tracker['enable_fingerprinting_across_websites'];
+ }
+
}
diff --git a/core/SettingsServer.php b/core/SettingsServer.php
index fc9d5f6bc1..3f6fbb8878 100644
--- a/core/SettingsServer.php
+++ b/core/SettingsServer.php
@@ -41,6 +41,22 @@ class SettingsServer
}
/**
+ * Mark the current request as a Tracker API request
+ */
+ public static function setIsTrackerApiRequest()
+ {
+ $GLOBALS['PIWIK_TRACKER_MODE'] = true;
+ }
+
+ /**
+ * Set the current request is not a tracker API request
+ */
+ public static function setIsNotTrackerApiRequest()
+ {
+ $GLOBALS['PIWIK_TRACKER_MODE'] = false;
+ }
+
+ /**
* Returns `true` if running on Microsoft IIS 7 (or above), `false` if otherwise.
*
* @return bool
diff --git a/core/Site.php b/core/Site.php
index deefbd4911..1a828fce47 100644
--- a/core/Site.php
+++ b/core/Site.php
@@ -133,7 +133,12 @@ class Site
public static function setSitesFromArray($sites)
{
foreach ($sites as $site) {
- self::setSite($site['idsite'], $site);
+ $idSite = null;
+ if (!empty($site['idsite'])) {
+ $idSite = $site['idsite'];
+ }
+
+ self::setSite($idSite, $site);
}
}
@@ -231,8 +236,17 @@ class Site
*/
protected function get($name)
{
+ if (!isset(self::$infoSites[$this->id])) {
+ $site = API::getInstance()->getSiteFromId($this->id);
+
+ if (empty($site)) {
+ throw new UnexpectedWebsiteFoundException('The requested website id = ' . (int)$this->id . ' couldn\'t be found');
+ }
+
+ self::setSite($this->id, $site);
+ }
if (!isset(self::$infoSites[$this->id][$name])) {
- throw new Exception('The requested website id = ' . (int)$this->id . ' (or its property ' . $name . ') couldn\'t be found');
+ throw new Exception("The property $name could not be found on the website ID " . (int)$this->id);
}
return self::$infoSites[$this->id][$name];
}
diff --git a/core/TaskScheduler.php b/core/TaskScheduler.php
index 8fd9069389..46968ad361 100644
--- a/core/TaskScheduler.php
+++ b/core/TaskScheduler.php
@@ -9,8 +9,9 @@
namespace Piwik;
-use Exception;
-use Piwik\Plugin\Manager as PluginManager;
+use Piwik\Container\StaticContainer;
+use Piwik\Scheduler\Scheduler;
+use Piwik\Scheduler\Task;
// When set to true, all scheduled tasks will be triggered in all requests (careful!)
//define('DEBUG_FORCE_SCHEDULED_TASKS', true);
@@ -19,25 +20,24 @@ use Piwik\Plugin\Manager as PluginManager;
* Manages scheduled task execution.
*
* A scheduled task is a callback that should be executed every so often (such as daily,
- * weekly, monthly, etc.). They are registered with **TaskScheduler** through the
- * {@hook TaskScheduler.getScheduledTasks} event.
+ * weekly, monthly, etc.). They are registered by extending {@link \Piwik\Plugin\Tasks}.
*
- * Tasks are executed when the cron core:archive command is executed.
+ * Tasks are executed when the `core:archive` command is executed.
*
* ### Examples
*
* **Scheduling a task**
*
- * // event handler for TaskScheduler.getScheduledTasks event
- * public function getScheduledTasks(&$tasks)
+ * class Tasks extends \Piwik\Plugin\Tasks
* {
- * $tasks[] = new ScheduledTask(
- * 'Piwik\Plugins\CorePluginsAdmin\MarketplaceApiClient',
- * 'clearAllCacheEntries',
- * null,
- * ScheduledTime::factory('daily'),
- * ScheduledTask::LOWEST_PRIORITY
- * );
+ * public function schedule()
+ * {
+ * $this->hourly('myTask'); // myTask() will be executed once every hour
+ * }
+ * public function myTask()
+ * {
+ * // do something
+ * }
* }
*
* **Executing all pending tasks**
@@ -49,21 +49,11 @@ use Piwik\Plugin\Manager as PluginManager;
*
* echo "Executed task '$task1Name'. Task output:\n$task1Output";
*
- * @method static \Piwik\TaskScheduler getInstance()
+ * @deprecated Use Piwik\Scheduler\Scheduler instead
+ * @see \Piwik\Scheduler\Scheduler
*/
-class TaskScheduler extends Singleton
+class TaskScheduler
{
- const GET_TASKS_EVENT = 'TaskScheduler.getScheduledTasks';
-
- private $isRunning = false;
-
- private $timetable = null;
-
- public function __construct()
- {
- $this->timetable = new ScheduledTaskTimetable();
- }
-
/**
* Executes tasks that are scheduled to run, then reschedules them.
*
@@ -79,79 +69,7 @@ class TaskScheduler extends Singleton
*/
public static function runTasks()
{
- return self::getInstance()->doRunTasks();
- }
-
- // for backwards compatibility
- private function collectTasksRegisteredViaEvent()
- {
- $tasks = array();
-
- /**
- * @ignore
- * @deprecated
- */
- Piwik::postEvent(self::GET_TASKS_EVENT, array(&$tasks));
-
- return $tasks;
- }
-
- private function getScheduledTasks()
- {
- /** @var \Piwik\ScheduledTask[] $tasks */
- $tasks = $this->collectTasksRegisteredViaEvent();
-
- /** @var \Piwik\Plugin\Tasks[] $pluginTasks */
- $pluginTasks = PluginManager::getInstance()->findComponents('Tasks', 'Piwik\\Plugin\\Tasks');
- foreach ($pluginTasks as $pluginTask) {
-
- $pluginTask->schedule();
-
- foreach ($pluginTask->getScheduledTasks() as $task) {
- $tasks[] = $task;
- }
- }
-
- return $tasks;
- }
-
- private function doRunTasks()
- {
- $tasks = $this->getScheduledTasks();
-
- // remove from timetable tasks that are not active anymore
- $this->timetable->removeInactiveTasks($tasks);
-
- // for every priority level, starting with the highest and concluding with the lowest
- $executionResults = array();
- for ($priority = ScheduledTask::HIGHEST_PRIORITY;
- $priority <= ScheduledTask::LOWEST_PRIORITY;
- ++$priority) {
- // loop through each task
- foreach ($tasks as $task) {
- // if the task does not have the current priority level, don't execute it yet
- if ($task->getPriority() != $priority) {
- continue;
- }
-
- $taskName = $task->getName();
- $shouldExecuteTask = $this->timetable->shouldExecuteTask($taskName);
-
- if ($this->timetable->taskShouldBeRescheduled($taskName)) {
- $this->timetable->rescheduleTask($task);
- }
-
- if ($shouldExecuteTask) {
- $this->isRunning = true;
- $message = self::executeTask($task);
- $this->isRunning = false;
-
- $executionResults[] = array('task' => $taskName, 'output' => $message);
- }
- }
- }
-
- return $executionResults;
+ return self::getInstance()->run();
}
/**
@@ -160,12 +78,12 @@ class TaskScheduler extends Singleton
* Call this method if your task's scheduled time has changed due to, for example, an option that
* was changed.
*
- * @param ScheduledTask $task Describes the scheduled task being rescheduled.
+ * @param Task $task Describes the scheduled task being rescheduled.
* @api
*/
- public static function rescheduleTask(ScheduledTask $task)
+ public static function rescheduleTask(Task $task)
{
- self::getInstance()->timetable->rescheduleTask($task);
+ self::getInstance()->rescheduleTask($task);
}
/**
@@ -175,7 +93,7 @@ class TaskScheduler extends Singleton
*/
public static function isTaskBeingExecuted()
{
- return self::getInstance()->isRunning;
+ return self::getInstance()->isRunningTask();
}
/**
@@ -189,25 +107,14 @@ class TaskScheduler extends Singleton
*/
public static function getScheduledTimeForMethod($className, $methodName, $methodParameter = null)
{
- return self::getInstance()->timetable->getScheduledTimeForMethod($className, $methodName, $methodParameter);
+ return self::getInstance()->getScheduledTimeForMethod($className, $methodName, $methodParameter);
}
/**
- * Executes the given taks
- *
- * @param ScheduledTask $task
- * @return string
+ * @return Scheduler
*/
- private static function executeTask($task)
+ private static function getInstance()
{
- try {
- $timer = new Timer();
- call_user_func(array($task->getObjectInstance(), $task->getMethodName()), $task->getMethodParameter());
- $message = $timer->__toString();
- } catch (Exception $e) {
- $message = 'ERROR: ' . $e->getMessage();
- }
-
- return $message;
+ return StaticContainer::getContainer()->get('Piwik\Scheduler\Scheduler');
}
}
diff --git a/core/Tracker.php b/core/Tracker.php
index d6b2d9236f..5bf6a9b4e1 100644
--- a/core/Tracker.php
+++ b/core/Tracker.php
@@ -9,17 +9,16 @@
namespace Piwik;
use Exception;
-use Piwik\Exception\InvalidRequestParameterException;
-use Piwik\Exception\UnexpectedWebsiteFoundException;
+use Piwik\Plugins\BulkTracking\Tracker\Requests;
use Piwik\Plugins\PrivacyManager\Config as PrivacyManagerConfig;
-use Piwik\Plugins\SitesManager\SiteUrls;
-use Piwik\Tracker\Cache;
+use Piwik\Tracker\Db as TrackerDb;
use Piwik\Tracker\Db\DbException;
-use Piwik\Tracker\Db\Mysqli;
-use Piwik\Tracker\Db\Pdo\Mysql;
+use Piwik\Tracker\Handler;
use Piwik\Tracker\Request;
+use Piwik\Tracker\RequestSet;
+use Piwik\Tracker\TrackerConfig;
use Piwik\Tracker\Visit;
-use Piwik\Tracker\VisitInterface;
+use Piwik\Plugin\Manager as PluginManager;
/**
* Class used by the logging script piwik.php called by the javascript tag.
@@ -27,363 +26,131 @@ use Piwik\Tracker\VisitInterface;
* saves information in the cookie, etc.
*
* We try to include as little files as possible (no dependency on 3rd party modules).
- *
*/
class Tracker
{
- protected $stateValid = self::STATE_NOTHING_TO_NOTICE;
/**
* @var Db
*/
- protected static $db = null;
-
- const STATE_NOTHING_TO_NOTICE = 1;
- const STATE_LOGGING_DISABLE = 10;
- const STATE_EMPTY_REQUEST = 11;
- const STATE_NOSCRIPT_REQUEST = 13;
+ private static $db = null;
// We use hex ID that are 16 chars in length, ie. 64 bits IDs
const LENGTH_HEX_ID_STRING = 16;
const LENGTH_BINARY_ID = 8;
- protected static $pluginsNotToLoad = array();
- protected static $pluginsToLoad = array();
-
- /**
- * The set of visits to track.
- *
- * @var array
- */
- private $requests = array();
-
- /**
- * The token auth supplied with a bulk visits POST.
- *
- * @var string
- */
- private $tokenAuth = null;
-
- /**
- * Whether we're currently using bulk tracking or not.
- *
- * @var bool
- */
- private $usingBulkTracking = false;
+ public static $initTrackerMode = false;
- /**
- * The number of requests that have been successfully logged.
- *
- * @var int
- */
private $countOfLoggedRequests = 0;
+ protected $isInstalled = null;
- protected function outputAccessControlHeaders()
- {
- $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
- if ($requestMethod !== 'GET') {
- $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*';
- Common::sendHeader('Access-Control-Allow-Origin: ' . $origin);
- Common::sendHeader('Access-Control-Allow-Credentials: true');
- }
- }
-
- public function clear()
+ public function isDebugModeEnabled()
{
- $this->stateValid = self::STATE_NOTHING_TO_NOTICE;
+ return array_key_exists('PIWIK_TRACKER_DEBUG', $GLOBALS) && $GLOBALS['PIWIK_TRACKER_DEBUG'] === true;
}
- /**
- * Do not load the specified plugins (used during testing, to disable Provider plugin)
- * @param array $plugins
- */
- public static function setPluginsNotToLoad($plugins)
+ public function shouldRecordStatistics()
{
- self::$pluginsNotToLoad = $plugins;
- }
+ $record = TrackerConfig::getConfigValue('record_statistics') != 0;
- /**
- * Get list of plugins to not load
- *
- * @return array
- */
- public static function getPluginsNotToLoad()
- {
- return self::$pluginsNotToLoad;
- }
+ if (!$record) {
+ Common::printDebug('Tracking is disabled in the config.ini.php via record_statistics=0');
+ }
- /**
- * Update Tracker config
- *
- * @param string $name Setting name
- * @param mixed $value Value
- */
- private static function updateTrackerConfig($name, $value)
- {
- $section = Config::getInstance()->Tracker;
- $section[$name] = $value;
- Config::getInstance()->Tracker = $section;
+ return $record && $this->isInstalled();
}
- protected function initRequests($args)
+ public static function loadTrackerEnvironment()
{
- $rawData = self::getRawBulkRequest();
- if (!empty($rawData)) {
- $this->usingBulkTracking = strpos($rawData, '"requests"') || strpos($rawData, "'requests'");
- if ($this->usingBulkTracking) {
- return $this->authenticateBulkTrackingRequests($rawData);
- }
+ SettingsServer::setIsTrackerApiRequest();
+ try {
+ $debug = (bool)TrackerConfig::getConfigValue('debug');
+ } catch(Exception $e) {
+ $debug = false;
}
-
- // Not using bulk tracking
- $this->requests = $args ? $args : (!empty($_GET) || !empty($_POST) ? array($_GET + $_POST) : array());
+ $GLOBALS['PIWIK_TRACKER_DEBUG'] = $debug;
+ PluginManager::getInstance()->loadTrackerPlugins();
}
- private static function getRequestsArrayFromBulkRequest($rawData)
+ private function init()
{
- $rawData = trim($rawData);
- $rawData = Common::sanitizeLineBreaks($rawData);
+ $this->handleFatalErrors();
- // POST data can be array of string URLs or array of arrays w/ visit info
- $jsonData = json_decode($rawData, $assoc = true);
+ \Piwik\FrontController::createConfigObject();
- $tokenAuth = Common::getRequestVar('token_auth', false, 'string', $jsonData);
+ if ($this->isDebugModeEnabled()) {
+ ErrorHandler::registerErrorHandler();
+ ExceptionHandler::setUp();
- $requests = array();
- if (isset($jsonData['requests'])) {
- $requests = $jsonData['requests'];
+ Common::printDebug("Debug enabled - Input parameters: ");
+ Common::printDebug(var_export($_GET, true));
}
-
- return array($requests, $tokenAuth);
- }
-
- private function isBulkTrackingRequireTokenAuth()
- {
- return !empty(Config::getInstance()->Tracker['bulk_requests_require_authentication']);
}
- private function authenticateBulkTrackingRequests($rawData)
+ public function isInstalled()
{
- list($this->requests, $tokenAuth) = $this->getRequestsArrayFromBulkRequest($rawData);
-
- $bulkTrackingRequireTokenAuth = $this->isBulkTrackingRequireTokenAuth();
- if ($bulkTrackingRequireTokenAuth) {
- if (empty($tokenAuth)) {
- throw new Exception("token_auth must be specified when using Bulk Tracking Import. "
- . " See <a href='http://developer.piwik.org/api-reference/tracking-api'>Tracking Doc</a>");
- }
+ if (is_null($this->isInstalled)) {
+ $this->isInstalled = SettingsPiwik::isPiwikInstalled();
}
- if (!empty($this->requests)) {
- foreach ($this->requests as &$request) {
- // if a string is sent, we assume its a URL and try to parse it
- if (is_string($request)) {
- $params = array();
-
- $url = @parse_url($request);
- if (!empty($url)) {
- @parse_str($url['query'], $params);
- $request = $params;
- }
- }
-
- $requestObj = new Request($request, $tokenAuth);
- $this->loadTrackerPlugins($requestObj);
-
- if ($bulkTrackingRequireTokenAuth
- && !$requestObj->isAuthenticated()
- ) {
- throw new Exception(sprintf("token_auth specified does not have Admin permission for idsite=%s", $requestObj->getIdSite()));
- }
- $request = $requestObj;
- }
- }
-
- return $tokenAuth;
+ return $this->isInstalled;
}
- /**
- * Main - tracks the visit/action
- *
- * @param array $args Optional Request Array
- */
- public function main($args = null)
+ public function main(Handler $handler, RequestSet $requestSet)
{
- if (!SettingsPiwik::isPiwikInstalled()) {
- return $this->handleEmptyRequest();
- }
try {
- $tokenAuth = $this->initRequests($args);
- } catch (Exception $ex) {
- $this->exitWithException($ex, true);
- }
-
- $this->initOutputBuffer();
-
- if (!empty($this->requests)) {
- $this->beginTransaction();
-
- try {
- foreach ($this->requests as $params) {
- $isAuthenticated = $this->trackRequest($params, $tokenAuth);
- }
- $this->runScheduledTasksIfAllowed($isAuthenticated);
- $this->commitTransaction();
- } catch (DbException $e) {
- Common::printDebug($e->getMessage());
- $this->rollbackTransaction();
- }
-
- } else {
- $this->handleEmptyRequest();
+ $this->init();
+ $handler->init($this, $requestSet);
+ $this->track($handler, $requestSet);
+ } catch (Exception $e) {
+ $handler->onException($this, $requestSet, $e);
}
Piwik::postEvent('Tracker.end');
+ $response = $handler->finish($this, $requestSet);
- $this->end();
+ $this->disconnectDatabase();
- $this->flushOutputBuffer();
-
- $this->performRedirectToUrlIfSet();
- }
-
- protected function initOutputBuffer()
- {
- ob_start();
- }
-
- protected function flushOutputBuffer()
- {
- ob_end_flush();
- }
-
- protected function getOutputBuffer()
- {
- return ob_get_contents();
+ return $response;
}
- protected function beginTransaction()
+ public function track(Handler $handler, RequestSet $requestSet)
{
- $this->transactionId = null;
- if (!$this->shouldUseTransactions()) {
+ if (!$this->shouldRecordStatistics()) {
return;
}
- $this->transactionId = self::getDatabase()->beginTransaction();
- }
- protected function commitTransaction()
- {
- if (empty($this->transactionId)) {
- return;
- }
- self::getDatabase()->commit($this->transactionId);
- }
+ $requestSet->initRequestsAndTokenAuth();
- protected function rollbackTransaction()
- {
- if (empty($this->transactionId)) {
- return;
+ if ($requestSet->hasRequests()) {
+ $handler->onStartTrackRequests($this, $requestSet);
+ $handler->process($this, $requestSet);
+ $handler->onAllRequestsTracked($this, $requestSet);
}
- self::getDatabase()->rollback($this->transactionId);
}
/**
- * @return bool
- */
- protected function shouldUseTransactions()
- {
- $isBulkRequest = count($this->requests) > 1;
- return $isBulkRequest && $this->isTransactionSupported();
- }
-
- /**
- * @return bool
- */
- protected function isTransactionSupported()
- {
- return (bool)Config::getInstance()->Tracker['bulk_requests_use_transaction'];
- }
-
- protected function shouldRunScheduledTasks()
- {
- // don't run scheduled tasks in CLI mode from Tracker, this is the case
- // where we bulk load logs & don't want to lose time with tasks
- return !Common::isPhpCliMode()
- && $this->getState() != self::STATE_LOGGING_DISABLE;
- }
-
- /**
- * Tracker requests will automatically trigger the Scheduled tasks.
- * This is useful for users who don't setup the cron,
- * but still want daily/weekly/monthly PDF reports emailed automatically.
- *
- * This is similar to calling the API CoreAdminHome.runScheduledTasks
+ * @param Request $request
+ * @return array
*/
- protected static function runScheduledTasks()
+ public function trackRequest(Request $request)
{
- $now = time();
-
- // Currently, there are no hourly tasks. When there are some,
- // this could be too aggressive minimum interval (some hours would be skipped in case of low traffic)
- $minimumInterval = Config::getInstance()->Tracker['scheduled_tasks_min_interval'];
+ if ($request->isEmptyRequest()) {
+ Common::printDebug("The request is empty");
+ } else {
+ $this->loadTrackerPlugins();
- // If the user disabled browser archiving, he has already setup a cron
- // To avoid parallel requests triggering the Scheduled Tasks,
- // Get last time tasks started executing
- $cache = Cache::getCacheGeneral();
+ Common::printDebug("Current datetime: " . date("Y-m-d H:i:s", $request->getCurrentTimestamp()));
- if ($minimumInterval <= 0
- || empty($cache['isBrowserTriggerEnabled'])
- ) {
- Common::printDebug("-> Scheduled tasks not running in Tracker: Browser archiving is disabled.");
- return;
+ $visit = Visit\Factory::make();
+ $visit->setRequest($request);
+ $visit->handle();
}
- $nextRunTime = $cache['lastTrackerCronRun'] + $minimumInterval;
-
- if ((defined('DEBUG_FORCE_SCHEDULED_TASKS') && DEBUG_FORCE_SCHEDULED_TASKS)
- || $cache['lastTrackerCronRun'] === false
- || $nextRunTime < $now
- ) {
- $cache['lastTrackerCronRun'] = $now;
- Cache::setCacheGeneral($cache);
- self::initCorePiwikInTrackerMode();
- Option::set('lastTrackerCronRun', $cache['lastTrackerCronRun']);
- Common::printDebug('-> Scheduled Tasks: Starting...');
-
- // save current user privilege and temporarily assume Super User privilege
- $isSuperUser = Piwik::hasUserSuperUserAccess();
-
- // Scheduled tasks assume Super User is running
- Piwik::setUserHasSuperUserAccess();
-
- // While each plugins should ensure that necessary languages are loaded,
- // we ensure English translations at least are loaded
- Translate::loadEnglishTranslation();
-
- ob_start();
- CronArchive::$url = SettingsPiwik::getPiwikUrl();
- $cronArchive = new CronArchive();
- $cronArchive->runScheduledTasksInTrackerMode();
-
- $resultTasks = ob_get_contents();
- ob_clean();
-
- // restore original user privilege
- Piwik::setUserHasSuperUserAccess($isSuperUser);
-
- foreach (explode('</pre>', $resultTasks) as $resultTask) {
- Common::printDebug(str_replace('<pre>', '', $resultTask));
- }
-
- Common::printDebug('Finished Scheduled Tasks.');
- } else {
- Common::printDebug("-> Scheduled tasks not triggered.");
- }
- Common::printDebug("Next run will be from: " . date('Y-m-d H:i:s', $nextRunTime) . ' UTC');
+ // increment successfully logged request count. make sure to do this after try-catch,
+ // since an excluded visit is considered 'successfully logged'
+ ++$this->countOfLoggedRequests;
}
- public static $initTrackerMode = false;
-
/**
* Used to initialize core Piwik components on a piwik.php request
* Eg. when cache is missed and we will be calling some APIs to generate cache
@@ -405,356 +172,67 @@ class Tracker
Db::createDatabaseObject();
}
- \Piwik\Plugin\Manager::getInstance()->loadCorePluginsDuringTracker();
+ PluginManager::getInstance()->loadCorePluginsDuringTracker();
}
}
- /**
- * Echos an error message & other information, then exits.
- *
- * @param Exception $e
- * @param bool $authenticated
- * @param int $statusCode eg 500
- */
- protected function exitWithException($e, $authenticated = false, $statusCode = 500)
+ public function getCountOfLoggedRequests()
{
- if ($this->hasRedirectUrl()) {
- $this->performRedirectToUrlIfSet();
- exit;
- }
-
- Common::sendResponseCode($statusCode);
- error_log(sprintf("Error in Piwik (tracker): %s", str_replace("\n", " ", $this->getMessageFromException($e))));
-
- if ($this->usingBulkTracking) {
- // when doing bulk tracking we return JSON so the caller will know how many succeeded
- $result = array(
- 'status' => 'error',
- 'tracked' => $this->countOfLoggedRequests
- );
- // send error when in debug mode or when authenticated (which happens when doing log importing,
- if ((isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG'])
- || $authenticated
- ) {
- $result['message'] = $this->getMessageFromException($e);
- }
- Common::sendHeader('Content-Type: application/json');
- echo json_encode($result);
- die(1);
- exit;
- }
-
- if (isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG']) {
- Common::sendHeader('Content-Type: text/html; charset=utf-8');
- $trailer = '<span style="color: #888888">Backtrace:<br /><pre>' . $e->getTraceAsString() . '</pre></span>';
- $headerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutHeader.tpl');
- $footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutFooter.tpl');
- $headerPage = str_replace('{$HTML_TITLE}', 'Piwik &rsaquo; Error', $headerPage);
-
- echo $headerPage . '<p>' . $this->getMessageFromException($e) . '</p>' . $trailer . $footerPage;
- } // If not debug, but running authenticated (eg. during log import) then we display raw errors
- elseif ($authenticated) {
- Common::sendHeader('Content-Type: text/html; charset=utf-8');
- echo $this->getMessageFromException($e);
- } else {
- $this->sendResponse();
- }
-
- die(1);
- exit;
+ return $this->countOfLoggedRequests;
}
- /**
- * Returns the date in the "Y-m-d H:i:s" PHP format
- *
- * @param int $timestamp
- * @return string
- */
- public static function getDatetimeFromTimestamp($timestamp)
+ public function setCountOfLoggedRequests($numLoggedRequests)
{
- return date("Y-m-d H:i:s", $timestamp);
+ $this->countOfLoggedRequests = $numLoggedRequests;
}
- /**
- * Initialization
- * @param Request $request
- */
- protected function init(Request $request)
+ public function hasLoggedRequests()
{
- $this->loadTrackerPlugins($request);
- $this->handleDisabledTracker();
- $this->handleEmptyRequest($request);
+ return 0 !== $this->countOfLoggedRequests;
}
/**
- * Cleanup
+ * @deprecated since 2.10.0 use {@link Date::getDatetimeFromTimestamp()} instead
*/
- protected function end()
- {
- if ($this->usingBulkTracking) {
- $result = array(
- 'status' => 'success',
- 'tracked' => $this->countOfLoggedRequests
- );
-
- $this->outputAccessControlHeaders();
-
- Common::sendHeader('Content-Type: application/json');
- echo json_encode($result);
- exit;
- }
- switch ($this->getState()) {
- case self::STATE_LOGGING_DISABLE:
- $this->sendResponse();
- Common::printDebug("Logging disabled, display transparent logo");
- break;
-
- case self::STATE_EMPTY_REQUEST:
- Common::printDebug("Empty request => Piwik page");
- echo "<a href='/'>Piwik</a> is a free/libre web <a href='http://piwik.org'>analytics</a> that lets you keep control of your data.";
- break;
-
- case self::STATE_NOSCRIPT_REQUEST:
- case self::STATE_NOTHING_TO_NOTICE:
- default:
- $this->sendResponse();
- Common::printDebug("Nothing to notice => default behaviour");
- break;
- }
- Common::printDebug("End of the page.");
-
- if ($GLOBALS['PIWIK_TRACKER_DEBUG'] === true) {
- if (isset(self::$db)) {
- self::$db->recordProfiling();
- Profiler::displayDbTrackerProfile(self::$db);
- }
- }
-
- self::disconnectDatabase();
- }
-
- /**
- * Factory to create database objects
- *
- * @param array $configDb Database configuration
- * @throws Exception
- * @return \Piwik\Tracker\Db\Mysqli|\Piwik\Tracker\Db\Pdo\Mysql
- */
- public static function factory($configDb)
+ public static function getDatetimeFromTimestamp($timestamp)
{
- /**
- * Triggered before a connection to the database is established by the Tracker.
- *
- * This event can be used to change the database connection settings used by the Tracker.
- *
- * @param array $dbInfos Reference to an array containing database connection info,
- * including:
- *
- * - **host**: The host name or IP address to the MySQL database.
- * - **username**: The username to use when connecting to the
- * database.
- * - **password**: The password to use when connecting to the
- * database.
- * - **dbname**: The name of the Piwik MySQL database.
- * - **port**: The MySQL database port to use.
- * - **adapter**: either `'PDO\MYSQL'` or `'MYSQLI'`
- * - **type**: The MySQL engine to use, for instance 'InnoDB'
- */
- Piwik::postEvent('Tracker.getDatabaseConfig', array(&$configDb));
-
- switch ($configDb['adapter']) {
- case 'PDO\MYSQL':
- case 'PDO_MYSQL': // old format pre Piwik 2
- require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Pdo/Mysql.php';
- return new Mysql($configDb);
-
- case 'MYSQLI':
- require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Mysqli.php';
- return new Mysqli($configDb);
- }
-
- throw new Exception('Unsupported database adapter ' . $configDb['adapter']);
+ return Date::getDatetimeFromTimestamp($timestamp);
}
- public static function connectPiwikTrackerDb()
+ public function isDatabaseConnected()
{
- $db = null;
- $configDb = Config::getInstance()->database;
-
- if (!isset($configDb['port'])) {
- // before 0.2.4 there is no port specified in config file
- $configDb['port'] = '3306';
- }
-
- $db = Tracker::factory($configDb);
- $db->connect();
-
- return $db;
+ return !is_null(self::$db);
}
- protected static function connectDatabaseIfNotConnected()
+ public static function getDatabase()
{
- if (!is_null(self::$db)) {
- return;
- }
-
- try {
- self::$db = self::connectPiwikTrackerDb();
- } catch (Exception $e) {
- throw new DbException($e->getMessage(), $e->getCode());
+ if (is_null(self::$db)) {
+ try {
+ self::$db = TrackerDb::connectPiwikTrackerDb();
+ } catch (Exception $e) {
+ throw new DbException($e->getMessage(), $e->getCode());
+ }
}
- }
- /**
- * @return Db
- */
- public static function getDatabase()
- {
- self::connectDatabaseIfNotConnected();
return self::$db;
}
- public static function disconnectDatabase()
+ protected function disconnectDatabase()
{
- if (isset(self::$db)) {
+ if ($this->isDatabaseConnected()) { // note: I think we do this only for the tests
self::$db->disconnect();
self::$db = null;
}
}
- /**
- * Returns the Tracker_Visit object.
- * This method can be overwritten to use a different Tracker_Visit object
- *
- * @throws Exception
- * @return \Piwik\Tracker\Visit
- */
- protected function getNewVisitObject()
- {
- $visit = null;
-
- /**
- * Triggered before a new **visit tracking object** is created. Subscribers to this
- * event can force the use of a custom visit tracking object that extends from
- * {@link Piwik\Tracker\VisitInterface}.
- *
- * @param \Piwik\Tracker\VisitInterface &$visit Initialized to null, but can be set to
- * a new visit object. If it isn't modified
- * Piwik uses the default class.
- */
- Piwik::postEvent('Tracker.makeNewVisitObject', array(&$visit));
-
- if (is_null($visit)) {
- $visit = new Visit();
- } elseif (!($visit instanceof VisitInterface)) {
- throw new Exception("The Visit object set in the plugin must implement VisitInterface");
- }
- return $visit;
- }
-
- private function sendResponse()
- {
- if (isset($GLOBALS['PIWIK_TRACKER_DEBUG'])
- && $GLOBALS['PIWIK_TRACKER_DEBUG']
- ) {
- return;
- }
-
- if (strlen($this->getOutputBuffer()) > 0) {
- // If there was an error during tracker, return so errors can be flushed
- return;
- }
-
- $this->outputAccessControlHeaders();
-
- $request = $_GET + $_POST;
-
- if (array_key_exists('send_image', $request) && $request['send_image'] === '0') {
- Common::sendResponseCode(204);
-
- return;
- }
-
- $this->outputTransparentGif();
- }
-
- protected function outputTransparentGif ()
- {
- $transGifBase64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
- Common::sendHeader('Content-Type: image/gif');
-
- print(base64_decode($transGifBase64));
- }
-
- protected function isVisitValid()
- {
- return $this->stateValid !== self::STATE_LOGGING_DISABLE
- && $this->stateValid !== self::STATE_EMPTY_REQUEST;
- }
-
- protected function getState()
- {
- return $this->stateValid;
- }
-
- protected function setState($value)
- {
- $this->stateValid = $value;
- }
-
- protected function loadTrackerPlugins(Request $request)
- {
- // Adding &dp=1 will disable the provider plugin, if token_auth is used (used to speed up bulk imports)
- $disableProvider = $request->getParam('dp');
- if (!empty($disableProvider)) {
- Tracker::setPluginsNotToLoad(array('Provider'));
- }
-
- try {
- $pluginsTracker = \Piwik\Plugin\Manager::getInstance()->loadTrackerPlugins();
- Common::printDebug("Loading plugins: { " . implode(", ", $pluginsTracker) . " }");
- } catch (Exception $e) {
- Common::printDebug("ERROR: " . $e->getMessage());
- }
- }
-
- protected function handleEmptyRequest(Request $request = null)
- {
- if (is_null($request)) {
- $request = new Request($_GET + $_POST);
- }
- $countParameters = $request->getParamsCount();
- if ($countParameters == 0) {
- $this->setState(self::STATE_EMPTY_REQUEST);
- }
- if ($countParameters == 1) {
- $this->setState(self::STATE_NOSCRIPT_REQUEST);
- }
- }
-
- protected function handleDisabledTracker()
- {
- $saveStats = Config::getInstance()->Tracker['record_statistics'];
- if ($saveStats == 0) {
- $this->setState(self::STATE_LOGGING_DISABLE);
- }
- }
-
- protected function getTokenAuth()
- {
- if (!is_null($this->tokenAuth)) {
- return $this->tokenAuth;
- }
-
- return Common::getRequestVar('token_auth', false);
- }
-
public static function setTestEnvironment($args = null, $requestMethod = null)
{
if (is_null($args)) {
- $postData = self::getRequestsArrayFromBulkRequest(self::getRawBulkRequest());
- $args = $_GET + $postData;
+ $requests = new Requests();
+ $args = $requests->getRequestsArrayFromBulkRequest($requests->getRawBulkRequest());
+ $args = $_GET + $args;
}
+
if (is_null($requestMethod) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
$requestMethod = $_SERVER['REQUEST_METHOD'];
} else if (is_null($requestMethod)) {
@@ -762,17 +240,22 @@ class Tracker
}
// Do not run scheduled tasks during tests
- self::updateTrackerConfig('scheduled_tasks_min_interval', 0);
+ TrackerConfig::setConfigValue('scheduled_tasks_min_interval', 0);
// if nothing found in _GET/_POST and we're doing a POST, assume bulk request. in which case,
// we have to bypass authentication
if (empty($args) && $requestMethod == 'POST') {
- self::updateTrackerConfig('tracking_requests_require_authentication', 0);
+ TrackerConfig::setConfigValue('tracking_requests_require_authentication', 0);
+ }
+
+ // Tests can force the use of 3rd party cookie for ID visitor
+ if (Common::getRequestVar('forceEnableFingerprintingAcrossWebsites', false, null, $args) == 1) {
+ TrackerConfig::setConfigValue('enable_fingerprinting_across_websites', 1);
}
// Tests can force the use of 3rd party cookie for ID visitor
if (Common::getRequestVar('forceUseThirdPartyCookie', false, null, $args) == 1) {
- self::updateTrackerConfig('use_third_party_id_cookie', 1);
+ TrackerConfig::setConfigValue('use_third_party_id_cookie', 1);
}
// Tests using window_look_back_for_visitor
@@ -780,13 +263,13 @@ class Tracker
// also look for this in bulk requests (see fake_logs_replay.log)
|| strpos(json_encode($args, true), '"forceLargeWindowLookBackForVisitor":"1"') !== false
) {
- self::updateTrackerConfig('window_look_back_for_visitor', 2678400);
+ TrackerConfig::setConfigValue('window_look_back_for_visitor', 2678400);
}
// Tests can force the enabling of IP anonymization
if (Common::getRequestVar('forceIpAnonymization', false, null, $args) == 1) {
- self::connectDatabaseIfNotConnected();
+ self::getDatabase(); // make sure db is initialized
$privacyConfig = new PrivacyManagerConfig();
$privacyConfig->ipAddressMaskLength = 2;
@@ -797,155 +280,27 @@ class Tracker
$pluginsDisabled = array('Provider');
// Disable provider plugin, because it is so slow to do many reverse ip lookups
- self::setPluginsNotToLoad($pluginsDisabled);
- }
-
- /**
- * Gets the error message to output when a tracking request fails.
- *
- * @param Exception $e
- * @return string
- */
- private function getMessageFromException($e)
- {
- // Note: duplicated from FormDatabaseSetup.isAccessDenied
- // Avoid leaking the username/db name when access denied
- if ($e->getCode() == 1044 || $e->getCode() == 42000) {
- return "Error while connecting to the Piwik database - please check your credentials in config/config.ini.php file";
- }
- if(Common::isPhpCliMode()) {
- return $e->getMessage() . "\n" . $e->getTraceAsString();
- }
- return $e->getMessage();
+ PluginManager::getInstance()->setTrackerPluginsNotToLoad($pluginsDisabled);
}
- /**
- * @param $params
- * @param $tokenAuth
- * @return array
- */
- protected function trackRequest($params, $tokenAuth)
+ protected function loadTrackerPlugins()
{
- if ($params instanceof Request) {
- $request = $params;
- } else {
- $request = new Request($params, $tokenAuth);
- }
-
- $this->init($request);
-
- $isAuthenticated = $request->isAuthenticated();
-
try {
- if ($this->isVisitValid()) {
- Common::printDebug("Current datetime: " . date("Y-m-d H:i:s", $request->getCurrentTimestamp()));
-
- $visit = $this->getNewVisitObject();
- $visit->setRequest($request);
- $visit->handle();
- } else {
- Common::printDebug("The request is invalid: empty request, or maybe tracking is disabled in the config.ini.php via record_statistics=0");
- }
- } catch (UnexpectedWebsiteFoundException $e) {
- Common::printDebug("Exception: " . $e->getMessage());
- $this->exitWithException($e, $isAuthenticated, 400);
- } catch (InvalidRequestParameterException $e) {
- Common::printDebug("Exception: " . $e->getMessage());
- $this->exitWithException($e, $isAuthenticated, 400);
- } catch (DbException $e) {
- Common::printDebug("Exception: " . $e->getMessage());
- $this->exitWithException($e, $isAuthenticated);
+ $pluginManager = PluginManager::getInstance();
+ $pluginsTracker = $pluginManager->loadTrackerPlugins();
+ Common::printDebug("Loading plugins: { " . implode(", ", $pluginsTracker) . " }");
} catch (Exception $e) {
- $this->exitWithException($e, $isAuthenticated);
+ Common::printDebug("ERROR: " . $e->getMessage());
}
- $this->clear();
-
- // increment successfully logged request count. make sure to do this after try-catch,
- // since an excluded visit is considered 'successfully logged'
- ++$this->countOfLoggedRequests;
- return $isAuthenticated;
}
- protected function runScheduledTasksIfAllowed($isAuthenticated)
+ private function handleFatalErrors()
{
- // Do not run schedule task if we are importing logs
- // or doing custom tracking (as it could slow down)
- try {
- if (!$isAuthenticated
- && $this->shouldRunScheduledTasks()
- ) {
- self::runScheduledTasks();
+ register_shutdown_function(function () {
+ $lastError = error_get_last();
+ if (!empty($lastError) && $lastError['type'] == E_ERROR) {
+ Common::sendResponseCode(500);
}
- } catch (Exception $e) {
- $this->exitWithException($e);
- }
+ });
}
-
- /**
- * @return string
- */
- protected static function getRawBulkRequest()
- {
- return file_get_contents("php://input");
- }
-
- private function getRedirectUrl()
- {
- return Common::getRequestVar('redirecturl', false, 'string');
- }
-
- private function hasRedirectUrl()
- {
- $redirectUrl = $this->getRedirectUrl();
-
- return !empty($redirectUrl);
- }
-
- private function performRedirectToUrlIfSet()
- {
- if (!$this->hasRedirectUrl()) {
- return;
- }
-
- if (empty($this->requests)) {
- return;
- }
-
- $redirectUrl = $this->getRedirectUrl();
- $host = Url::getHostFromUrl($redirectUrl);
-
- if (empty($host)) {
- return;
- }
-
- $urls = new SiteUrls();
- $siteUrls = $urls->getAllCachedSiteUrls();
- $siteIds = $this->getAllSiteIdsWithinRequest();
-
- foreach ($siteIds as $siteId) {
- if (empty($siteUrls[$siteId])) {
- continue;
- }
-
- if (Url::isHostInUrls($host, $siteUrls[$siteId])) {
- Url::redirectToUrl($redirectUrl);
- }
- }
- }
-
- private function getAllSiteIdsWithinRequest()
- {
- if (empty($this->requests)) {
- return array();
- }
-
- $siteIds = array();
-
- foreach ($this->requests as $request) {
- $siteIds[] = (int) $request['idsite'];
- }
-
- return array_unique($siteIds);
- }
-
}
diff --git a/core/Tracker/Cache.php b/core/Tracker/Cache.php
index b8f0413c63..22b57a9cc2 100644
--- a/core/Tracker/Cache.php
+++ b/core/Tracker/Cache.php
@@ -10,7 +10,7 @@ namespace Piwik\Tracker;
use Piwik\Access;
use Piwik\ArchiveProcessor\Rules;
-use Piwik\CacheFile;
+use Piwik\Cache as PiwikCache;
use Piwik\Common;
use Piwik\Config;
use Piwik\Option;
@@ -23,19 +23,29 @@ use Piwik\Tracker;
*/
class Cache
{
+ private static $cacheIdGeneral = 'general';
+
/**
* Public for tests only
- * @var CacheFile
+ * @var \Piwik\Cache\Lazy
*/
- public static $trackerCache = null;
+ public static $cache;
- protected static function getInstance()
+ /**
+ * @return \Piwik\Cache\Lazy
+ */
+ private static function getCache()
{
- if (is_null(self::$trackerCache)) {
- $ttl = Config::getInstance()->Tracker['tracker_cache_file_ttl'];
- self::$trackerCache = new CacheFile('tracker', $ttl);
+ if (is_null(self::$cache)) {
+ self::$cache = PiwikCache::getLazyCache();
}
- return self::$trackerCache;
+
+ return self::$cache;
+ }
+
+ private static function getTtl()
+ {
+ return Config::getInstance()->Tracker['tracker_cache_file_ttl'];
}
/**
@@ -50,13 +60,14 @@ class Cache
return array();
}
- $idSite = (int)$idSite;
+ $idSite = (int) $idSite;
if ($idSite <= 0) {
return array();
}
- $cache = self::getInstance();
- $cacheContent = $cache->get($idSite);
+ $cache = self::getCache();
+ $cacheId = $idSite;
+ $cacheContent = $cache->fetch($cacheId);
if (false !== $cacheContent) {
return $cacheContent;
@@ -91,7 +102,7 @@ class Cache
// if nothing is returned from the plugins, we don't save the content
// this is not expected: all websites are expected to have at least one URL
if (!empty($content)) {
- $cache->set($idSite, $content);
+ $cache->save($cacheId, $content, self::getTtl());
}
return $content;
@@ -102,7 +113,7 @@ class Cache
*/
public static function clearCacheGeneral()
{
- self::getInstance()->delete('general');
+ self::getCache()->delete(self::$cacheIdGeneral);
}
/**
@@ -113,10 +124,8 @@ class Cache
*/
public static function getCacheGeneral()
{
- $cache = self::getInstance();
- $cacheId = 'general';
-
- $cacheContent = $cache->get($cacheId);
+ $cache = self::getCache();
+ $cacheContent = $cache->fetch(self::$cacheIdGeneral);
if (false !== $cacheContent) {
return $cacheContent;
@@ -162,11 +171,9 @@ class Cache
*/
public static function setCacheGeneral($value)
{
- $cache = self::getInstance();
- $cacheId = 'general';
- $cache->set($cacheId, $value);
+ $cache = self::getCache();
- return true;
+ return $cache->save(self::$cacheIdGeneral, $value, self::getTtl());
}
/**
@@ -193,8 +200,7 @@ class Cache
*/
public static function deleteCacheWebsiteAttributes($idSite)
{
- $idSite = (int)$idSite;
- self::getInstance()->delete($idSite);
+ self::getCache()->delete((int) $idSite);
}
/**
@@ -202,6 +208,6 @@ class Cache
*/
public static function deleteTrackerCache()
{
- self::getInstance()->deleteAll();
+ self::getCache()->flushAll();
}
}
diff --git a/core/Tracker/Db.php b/core/Tracker/Db.php
index 0c419d8f6e..e5ec25f57f 100644
--- a/core/Tracker/Db.php
+++ b/core/Tracker/Db.php
@@ -11,8 +11,13 @@ namespace Piwik\Tracker;
use Exception;
use PDOStatement;
use Piwik\Common;
+use Piwik\Config;
+use Piwik\Piwik;
use Piwik\Timer;
+use Piwik\Tracker;
use Piwik\Tracker\Db\DbException;
+use Piwik\Tracker\Db\Mysqli;
+use Piwik\Tracker\Db\Pdo\Mysql;
/**
* Simple database wrapper.
@@ -226,4 +231,63 @@ abstract class Db
* @return bool True if error number matches; false otherwise
*/
abstract public function isErrNo($e, $errno);
+
+ /**
+ * Factory to create database objects
+ *
+ * @param array $configDb Database configuration
+ * @throws Exception
+ * @return \Piwik\Tracker\Db\Mysqli|\Piwik\Tracker\Db\Pdo\Mysql
+ */
+ public static function factory($configDb)
+ {
+ /**
+ * Triggered before a connection to the database is established by the Tracker.
+ *
+ * This event can be used to change the database connection settings used by the Tracker.
+ *
+ * @param array $dbInfos Reference to an array containing database connection info,
+ * including:
+ *
+ * - **host**: The host name or IP address to the MySQL database.
+ * - **username**: The username to use when connecting to the
+ * database.
+ * - **password**: The password to use when connecting to the
+ * database.
+ * - **dbname**: The name of the Piwik MySQL database.
+ * - **port**: The MySQL database port to use.
+ * - **adapter**: either `'PDO\MYSQL'` or `'MYSQLI'`
+ * - **type**: The MySQL engine to use, for instance 'InnoDB'
+ */
+ Piwik::postEvent('Tracker.getDatabaseConfig', array(&$configDb));
+
+ switch ($configDb['adapter']) {
+ case 'PDO\MYSQL':
+ case 'PDO_MYSQL': // old format pre Piwik 2
+ require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Pdo/Mysql.php';
+ return new Mysql($configDb);
+
+ case 'MYSQLI':
+ require_once PIWIK_INCLUDE_PATH . '/core/Tracker/Db/Mysqli.php';
+ return new Mysqli($configDb);
+ }
+
+ throw new Exception('Unsupported database adapter ' . $configDb['adapter']);
+ }
+
+ public static function connectPiwikTrackerDb()
+ {
+ $db = null;
+ $configDb = Config::getInstance()->database;
+
+ if (!isset($configDb['port'])) {
+ // before 0.2.4 there is no port specified in config file
+ $configDb['port'] = '3306';
+ }
+
+ $db = self::factory($configDb);
+ $db->connect();
+
+ return $db;
+ }
}
diff --git a/core/Tracker/Db/Mysqli.php b/core/Tracker/Db/Mysqli.php
index 9452551683..e1922e11e5 100644
--- a/core/Tracker/Db/Mysqli.php
+++ b/core/Tracker/Db/Mysqli.php
@@ -291,11 +291,11 @@ class Mysqli extends Db
*/
public function beginTransaction()
{
- if (!$this->activeTransaction === false ) {
+ if (!$this->activeTransaction === false) {
return;
}
- if ( $this->connection->autocommit(false) ) {
+ if ( $this->connection->autocommit(false)) {
$this->activeTransaction = uniqid();
return $this->activeTransaction;
}
@@ -309,15 +309,17 @@ class Mysqli extends Db
*/
public function commit($xid)
{
- if ($this->activeTransaction != $xid || $this->activeTransaction === false ) {
+ if ($this->activeTransaction != $xid || $this->activeTransaction === false) {
return;
}
+
$this->activeTransaction = false;
if (!$this->connection->commit() ) {
throw new DbException("Commit failed");
}
+
$this->connection->autocommit(true);
}
@@ -329,14 +331,16 @@ class Mysqli extends Db
*/
public function rollBack($xid)
{
- if ($this->activeTransaction != $xid || $this->activeTransaction === false ) {
+ if ($this->activeTransaction != $xid || $this->activeTransaction === false) {
return;
}
+
$this->activeTransaction = false;
if (!$this->connection->rollback() ) {
throw new DbException("Rollback failed");
}
+
$this->connection->autocommit(true);
}
}
diff --git a/core/Tracker/Db/Pdo/Mysql.php b/core/Tracker/Db/Pdo/Mysql.php
index 1cb72c11a6..4d6094b478 100644
--- a/core/Tracker/Db/Pdo/Mysql.php
+++ b/core/Tracker/Db/Pdo/Mysql.php
@@ -270,12 +270,13 @@ class Mysql extends Db
*/
public function commit($xid)
{
- if ($this->activeTransaction != $xid || $this->activeTransaction === false ) {
+ if ($this->activeTransaction != $xid || $this->activeTransaction === false) {
return;
}
+
$this->activeTransaction = false;
- if (!$this->connection->commit() ) {
+ if (!$this->connection->commit()) {
throw new DbException("Commit failed");
}
}
@@ -288,12 +289,13 @@ class Mysql extends Db
*/
public function rollBack($xid)
{
- if ($this->activeTransaction != $xid || $this->activeTransaction === false ) {
+ if ($this->activeTransaction != $xid || $this->activeTransaction === false) {
return;
}
+
$this->activeTransaction = false;
- if (!$this->connection->rollBack() ) {
+ if (!$this->connection->rollBack()) {
throw new DbException("Rollback failed");
}
}
diff --git a/core/Tracker/GoalManager.php b/core/Tracker/GoalManager.php
index 0d4bd2bfcb..913522696a 100644
--- a/core/Tracker/GoalManager.php
+++ b/core/Tracker/GoalManager.php
@@ -10,6 +10,7 @@ namespace Piwik\Tracker;
use Exception;
use Piwik\Common;
+use Piwik\Date;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\ConversionDimension;
use Piwik\Plugin\Dimension\VisitDimension;
@@ -837,7 +838,7 @@ class GoalManager
$goal = array(
'idvisit' => $visitorInformation['idvisit'],
'idvisitor' => $visitorInformation['idvisitor'],
- 'server_time' => Tracker::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time'])
+ 'server_time' => Date::getDatetimeFromTimestamp($visitorInformation['visit_last_action_time'])
);
$visitDimensions = VisitDimension::getAllDimensions();
diff --git a/core/Tracker/Handler.php b/core/Tracker/Handler.php
new file mode 100644
index 0000000000..3970b910d1
--- /dev/null
+++ b/core/Tracker/Handler.php
@@ -0,0 +1,118 @@
+<?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\Tracker;
+
+use Piwik\Common;
+use Piwik\Exception\InvalidRequestParameterException;
+use Piwik\Exception\UnexpectedWebsiteFoundException;
+use Piwik\Tracker;
+use Exception;
+use Piwik\Url;
+
+class Handler
+{
+ /**
+ * @var Response
+ */
+ private $response;
+
+ /**
+ * @var ScheduledTasksRunner
+ */
+ private $tasksRunner;
+
+ public function __construct()
+ {
+ $this->setResponse(new Response());
+ }
+
+ public function setResponse($response)
+ {
+ $this->response = $response;
+ }
+
+ public function init(Tracker $tracker, RequestSet $requestSet)
+ {
+ $this->response->init($tracker);
+ }
+
+ public function process(Tracker $tracker, RequestSet $requestSet)
+ {
+ foreach ($requestSet->getRequests() as $request) {
+ $tracker->trackRequest($request);
+ }
+ }
+
+ public function onStartTrackRequests(Tracker $tracker, RequestSet $requestSet)
+ {
+ }
+
+ public function onAllRequestsTracked(Tracker $tracker, RequestSet $requestSet)
+ {
+ $tasks = $this->getScheduledTasksRunner();
+ if ($tasks->shouldRun($tracker)) {
+ $tasks->runScheduledTasks();
+ }
+ }
+
+ private function getScheduledTasksRunner()
+ {
+ if (is_null($this->tasksRunner)) {
+ $this->tasksRunner = new ScheduledTasksRunner();
+ }
+
+ return $this->tasksRunner;
+ }
+
+ /**
+ * @internal
+ */
+ public function setScheduledTasksRunner(ScheduledTasksRunner $runner)
+ {
+ $this->tasksRunner = $runner;
+ }
+
+ public function onException(Tracker $tracker, RequestSet $requestSet, Exception $e)
+ {
+ Common::printDebug("Exception: " . $e->getMessage());
+
+ $statusCode = 500;
+ if ($e instanceof UnexpectedWebsiteFoundException) {
+ $statusCode = 400;
+ } elseif ($e instanceof InvalidRequestParameterException) {
+ $statusCode = 400;
+ }
+
+ $this->response->outputException($tracker, $e, $statusCode);
+ $this->redirectIfNeeded($requestSet);
+ }
+
+ public function finish(Tracker $tracker, RequestSet $requestSet)
+ {
+ $this->response->outputResponse($tracker);
+ $this->redirectIfNeeded($requestSet);
+ return $this->response->getOutput();
+ }
+
+ public function getResponse()
+ {
+ return $this->response;
+ }
+
+ protected function redirectIfNeeded(RequestSet $requestSet)
+ {
+ $redirectUrl = $requestSet->shouldPerformRedirectToUrl();
+
+ if (!empty($redirectUrl)) {
+ Url::redirectToUrl($redirectUrl);
+ }
+ }
+
+}
diff --git a/core/Tracker/Handler/Factory.php b/core/Tracker/Handler/Factory.php
new file mode 100644
index 0000000000..c15def0fe4
--- /dev/null
+++ b/core/Tracker/Handler/Factory.php
@@ -0,0 +1,43 @@
+<?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\Tracker\Handler;
+
+use Exception;
+use Piwik\Piwik;
+use Piwik\Tracker\Handler;
+
+class Factory
+{
+ public static function make()
+ {
+ $handler = null;
+
+ /**
+ * Triggered before a new **handler tracking object** is created. Subscribers to this
+ * event can force the use of a custom handler tracking object that extends from
+ * {@link Piwik\Tracker\Handler} and customize any tracking behavior.
+ *
+ * @param \Piwik\Tracker\Handler &$handler Initialized to null, but can be set to
+ * a new handler object. If it isn't modified
+ * Piwik uses the default class.
+ * @ignore This event is not public yet as the Handler API is not really stable yet
+ */
+ Piwik::postEvent('Tracker.newHandler', array(&$handler));
+
+ if (is_null($handler)) {
+ $handler = new Handler();
+ } elseif (!($handler instanceof Handler)) {
+ throw new Exception("The Handler object set in the plugin must be an instance of Piwik\\Tracker\\Handler");
+ }
+
+ return $handler;
+ }
+
+}
diff --git a/core/Tracker/Model.php b/core/Tracker/Model.php
index 090cd8be59..e40f845e00 100644
--- a/core/Tracker/Model.php
+++ b/core/Tracker/Model.php
@@ -9,10 +9,8 @@
namespace Piwik\Tracker;
use Exception;
-use PDOStatement;
use Piwik\Common;
use Piwik\Tracker;
-use Piwik\Tracker\Db\DbException;
class Model
{
@@ -142,8 +140,32 @@ class Model
Common::printDebug($bind);
}
+ /**
+ * Inserts a new action into the log_action table. If there is an existing action that was inserted
+ * due to another request pre-empting this one, the newly inserted action is deleted.
+ *
+ * @param string $name
+ * @param int $type
+ * @param int $urlPrefix
+ * @return int The ID of the action (can be for an existing action or new action).
+ */
public function createNewIdAction($name, $type, $urlPrefix)
{
+ $newActionId = $this->insertNewAction($name, $type, $urlPrefix);
+
+ $realFirstActionId = $this->getIdActionMatchingNameAndType($name, $type);
+
+ // if the inserted action ID is not the same as the queried action ID, then that means we inserted
+ // a duplicate, so remove it now
+ if ($realFirstActionId != $newActionId) {
+ $this->deleteDuplicateAction($newActionId);
+ }
+
+ return $realFirstActionId;
+ }
+
+ private function insertNewAction($name, $type, $urlPrefix)
+ {
$table = Common::prefixTable('log_action');
$sql = "INSERT INTO $table (name, hash, type, url_prefix) VALUES (?,CRC32(?),?,?)";
@@ -157,8 +179,11 @@ class Model
private function getSqlSelectActionId()
{
+ // it is possible for multiple actions to exist in the DB (due to rare concurrency issues), so the ORDER BY and
+ // LIMIT are important
$sql = "SELECT idaction, type, name FROM " . Common::prefixTable('log_action')
- . " WHERE ( hash = CRC32(?) AND name = ? AND type = ? ) ";
+ . " WHERE " . $this->getSqlConditionToMatchSingleAction() . " "
+ . "ORDER BY idaction ASC LIMIT 1";
return $sql;
}
@@ -173,9 +198,16 @@ class Model
return $idAction;
}
+ /**
+ * Returns the IDs for multiple actions based on name + type values.
+ *
+ * @param array $actionsNameAndType Array like `array( array('name' => '...', 'type' => 1), ... )`
+ * @return array|false Array of DB rows w/ columns: **idaction**, **type**, **name**.
+ */
public function getIdsAction($actionsNameAndType)
{
- $sql = $this->getSqlSelectActionId();
+ $sql = "SELECT MIN(idaction) as idaction, type, name FROM " . Common::prefixTable('log_action')
+ . " WHERE";
$bind = array();
$i = 0;
@@ -187,15 +219,19 @@ class Model
}
if ($i > 0) {
- $sql .= " OR ( hash = CRC32(?) AND name = ? AND type = ? ) ";
+ $sql .= " OR";
}
+ $sql .= " " . $this->getSqlConditionToMatchSingleAction() . " ";
+
$bind[] = $name;
$bind[] = $name;
$bind[] = $actionNameType['type'];
$i++;
}
+ $sql .= " GROUP BY type, hash, name";
+
// Case URL & Title are empty
if (empty($bind)) {
return false;
@@ -375,9 +411,21 @@ class Model
return array($updateParts, $sqlBind);
}
+ private function deleteDuplicateAction($newActionId)
+ {
+ $sql = "DELETE FROM " . Common::prefixTable('log_action') . " WHERE idaction = ?";
+
+ $db = $this->getDb();
+ $db->query($sql, array($newActionId));
+ }
+
private function getDb()
{
return Tracker::getDatabase();
}
+ private function getSqlConditionToMatchSingleAction()
+ {
+ return "( hash = CRC32(?) AND name = ? AND type = ? )";
+ }
}
diff --git a/core/Tracker/PageUrl.php b/core/Tracker/PageUrl.php
index ae55b48aac..bbd98882cd 100644
--- a/core/Tracker/PageUrl.php
+++ b/core/Tracker/PageUrl.php
@@ -11,6 +11,7 @@ namespace Piwik\Tracker;
use Piwik\Common;
use Piwik\Config;
+use Piwik\Piwik;
use Piwik\UrlHelper;
class PageUrl
@@ -86,14 +87,22 @@ class PageUrl
$website = Cache::getCacheWebsiteAttributes($idSite);
$excludedParameters = self::getExcludedParametersFromWebsite($website);
- if (!empty($excludedParameters)) {
- Common::printDebug('Excluding parameters "' . implode(',', $excludedParameters) . '" from URL');
- }
-
$parametersToExclude = array_merge($excludedParameters,
self::$queryParametersToExclude,
$campaignTrackingParameters);
+ /**
+ * Triggered before setting the action url in Piwik\Tracker\Action so plugins can register
+ * parameters to be excluded from the tracking URL (e.g. campaign parameters).
+ *
+ * @param array &$parametersToExclude An array of parameters to exclude from the tracking url.
+ */
+ Piwik::postEvent('Tracker.PageUrl.getQueryParametersToExclude', array(&$parametersToExclude));
+
+ if (!empty($parametersToExclude)) {
+ Common::printDebug('Excluding parameters "' . implode(',', $parametersToExclude) . '" from URL');
+ }
+
$parametersToExclude = array_map('strtolower', $parametersToExclude);
return $parametersToExclude;
}
@@ -110,7 +119,7 @@ class PageUrl
public static function shouldRemoveURLFragmentFor($idSite)
{
$websiteAttributes = Cache::getCacheWebsiteAttributes($idSite);
- return !$websiteAttributes['keep_url_fragment'];
+ return empty($websiteAttributes['keep_url_fragment']);
}
/**
@@ -157,7 +166,7 @@ class PageUrl
}
if (!empty($parsedUrl['host'])) {
- $parsedUrl['host'] = mb_strtolower($parsedUrl['host'], 'UTF-8');
+ $parsedUrl['host'] = Common::mb_strtolower($parsedUrl['host'], 'UTF-8');
}
if (!empty($parsedUrl['fragment'])) {
@@ -219,7 +228,8 @@ class PageUrl
{
if (is_string($value)) {
$decoded = urldecode($value);
- if (@mb_check_encoding($decoded, $encoding)) {
+ if (function_exists('mb_check_encoding')
+ && @mb_check_encoding($decoded, $encoding)) {
$value = urlencode(mb_convert_encoding($decoded, 'UTF-8', $encoding));
}
}
@@ -256,13 +266,18 @@ class PageUrl
*/
public static function reencodeParameters(&$queryParameters, $encoding = false)
{
- // if query params are encoded w/ non-utf8 characters (due to browser bug or whatever),
- // encode to UTF-8.
- if (false !== $encoding
- && 'utf-8' != strtolower($encoding)
- && function_exists('mb_check_encoding')
- ) {
- $queryParameters = PageUrl::reencodeParametersArray($queryParameters, $encoding);
+ if (function_exists('mb_check_encoding')) {
+ // if query params are encoded w/ non-utf8 characters (due to browser bug or whatever),
+ // encode to UTF-8.
+ if (strtolower($encoding) != 'utf-8'
+ && $encoding != false
+ ) {
+ Common::printDebug("Encoding page URL query parameters to $encoding.");
+
+ $queryParameters = PageUrl::reencodeParametersArray($queryParameters, $encoding);
+ }
+ } else {
+ Common::printDebug("Page charset supplied in tracking request, but mbstring extension is not available.");
}
return $queryParameters;
@@ -349,5 +364,15 @@ class PageUrl
return array();
}
-}
+ public static function urldecodeValidUtf8($value)
+ {
+ $value = urldecode($value);
+ if (function_exists('mb_check_encoding')
+ && !@mb_check_encoding($value, 'utf-8')
+ ) {
+ return urlencode($value);
+ }
+ return $value;
+ }
+} \ No newline at end of file
diff --git a/core/Tracker/Request.php b/core/Tracker/Request.php
index 8853800fa6..23f01a555b 100644
--- a/core/Tracker/Request.php
+++ b/core/Tracker/Request.php
@@ -11,6 +11,7 @@ namespace Piwik\Tracker;
use Exception;
use Piwik\Common;
use Piwik\Config;
+use Piwik\Container\StaticContainer;
use Piwik\Cookie;
use Piwik\Exception\InvalidRequestParameterException;
use Piwik\Exception\UnexpectedWebsiteFoundException;
@@ -18,7 +19,6 @@ use Piwik\IP;
use Piwik\Network\IPUtils;
use Piwik\Piwik;
use Piwik\Plugins\CustomVariables\CustomVariables;
-use Piwik\Registry;
use Piwik\Tracker;
/**
@@ -31,8 +31,10 @@ class Request
* @var array
*/
protected $params;
+ protected $rawParams;
protected $isAuthenticated = null;
+ private $isEmptyRequest = false;
protected $tokenAuth;
@@ -50,16 +52,19 @@ class Request
$params = array();
}
$this->params = $params;
+ $this->rawParams = $params;
$this->tokenAuth = $tokenAuth;
$this->timestamp = time();
+ $this->isEmptyRequest = empty($params);
// When the 'url' and referrer url parameter are not given, we might be in the 'Simple Image Tracker' mode.
// The URL can default to the Referrer, which will be in this case
// the URL of the page containing the Simple Image beacon
if (empty($this->params['urlref'])
&& empty($this->params['url'])
+ && array_key_exists('HTTP_REFERER', $_SERVER)
) {
- $url = @$_SERVER['HTTP_REFERER'];
+ $url = $_SERVER['HTTP_REFERER'];
if (!empty($url)) {
$this->params['url'] = $url;
}
@@ -67,6 +72,21 @@ class Request
}
/**
+ * Get the params that were originally passed to the instance. These params do not contain any params that were added
+ * within this object.
+ * @return array
+ */
+ public function getRawParams()
+ {
+ return $this->rawParams;
+ }
+
+ public function getTokenAuth()
+ {
+ return $this->tokenAuth;
+ }
+
+ /**
* @return bool
*/
public function isAuthenticated()
@@ -82,21 +102,27 @@ class Request
* This method allows to set custom IP + server time + visitor ID, when using Tracking API.
* These two attributes can be only set by the Super User (passing token_auth).
*/
- protected function authenticateTrackingApi($tokenAuthFromBulkRequest)
+ protected function authenticateTrackingApi($tokenAuth)
{
- $shouldAuthenticate = Config::getInstance()->Tracker['tracking_requests_require_authentication'];
+ $shouldAuthenticate = TrackerConfig::getConfigValue('tracking_requests_require_authentication');
+
if ($shouldAuthenticate) {
- $tokenAuth = $tokenAuthFromBulkRequest ? $tokenAuthFromBulkRequest : Common::getRequestVar('token_auth', false, 'string', $this->params);
+
+ if (empty($tokenAuth)) {
+ $tokenAuth = Common::getRequestVar('token_auth', false, 'string', $this->params);
+ }
+
try {
$idSite = $this->getIdSite();
- $this->isAuthenticated = $this->authenticateSuperUserOrAdmin($tokenAuth, $idSite);
+ $this->isAuthenticated = self::authenticateSuperUserOrAdmin($tokenAuth, $idSite);
} catch (Exception $e) {
$this->isAuthenticated = false;
}
- if (!$this->isAuthenticated) {
- return;
+
+ if ($this->isAuthenticated) {
+ Common::printDebug("token_auth is authenticated!");
}
- Common::printDebug("token_auth is authenticated!");
+
} else {
$this->isAuthenticated = true;
Common::printDebug("token_auth authentication not required");
@@ -112,7 +138,7 @@ class Request
Piwik::postEvent('Request.initAuthenticationObject');
/** @var \Piwik\Auth $auth */
- $auth = Registry::get('auth');
+ $auth = StaticContainer::get('Piwik\Auth');
$auth->setTokenAuth($tokenAuth);
$auth->setLogin(null);
$access = $auth->authenticate();
@@ -124,10 +150,12 @@ class Request
// Now checking the list of admin token_auth cached in the Tracker config file
if (!empty($idSite) && $idSite > 0) {
$website = Cache::getCacheWebsiteAttributes($idSite);
+
if (array_key_exists('admin_token_auth', $website) && in_array($tokenAuth, $website['admin_token_auth'])) {
return true;
}
}
+
Common::printDebug("WARNING! token_auth = $tokenAuth is not valid, Super User / Admin was NOT authenticated");
return false;
@@ -139,11 +167,13 @@ class Request
public function getDaysSinceFirstVisit()
{
$cookieFirstVisitTimestamp = $this->getParam('_idts');
+
if (!$this->isTimestampValid($cookieFirstVisitTimestamp)) {
$cookieFirstVisitTimestamp = $this->getCurrentTimestamp();
}
$daysSinceFirstVisit = round(($this->getCurrentTimestamp() - $cookieFirstVisitTimestamp) / 86400, $precision = 0);
+
if ($daysSinceFirstVisit < 0) {
$daysSinceFirstVisit = 0;
}
@@ -324,21 +354,31 @@ class Request
public function getCurrentTimestamp()
{
$cdt = $this->getCustomTimestamp();
- if(!empty($cdt)) {
+
+ if (!empty($cdt)) {
return $cdt;
}
+
return $this->timestamp;
}
+ public function setCurrentTimestamp($timestamp)
+ {
+ $this->timestamp = $timestamp;
+ }
+
protected function getCustomTimestamp()
{
$cdt = $this->getParam('cdt');
+
if (empty($cdt)) {
return false;
}
+
if (!is_numeric($cdt)) {
$cdt = strtotime($cdt);
}
+
if (!$this->isTimestampValid($cdt, $this->timestamp)) {
Common::printDebug(sprintf("Datetime %s is not valid", date("Y-m-d H:i:m", $cdt)));
return false;
@@ -347,6 +387,7 @@ class Request
// If timestamp in the past, token_auth is required
$timeFromNow = $this->timestamp - $cdt;
$isTimestampRecent = $timeFromNow < self::CUSTOM_TIMESTAMP_DOES_NOT_REQUIRE_TOKENAUTH_WHEN_NEWER_THAN;
+
if (!$isTimestampRecent) {
if(!$this->isAuthenticated()) {
Common::printDebug(sprintf("Custom timestamp is %s seconds old, requires &token_auth...", $timeFromNow));
@@ -354,6 +395,7 @@ class Request
return false;
}
}
+
return $cdt;
}
@@ -366,9 +408,10 @@ class Request
*/
protected function isTimestampValid($time, $now = null)
{
- if(empty($now)) {
+ if (empty($now)) {
$now = $this->getCurrentTimestamp();
}
+
return $time <= $now
&& $time > $now - 10 * 365 * 86400;
}
@@ -400,10 +443,29 @@ class Request
public function getUserAgent()
{
- $default = @$_SERVER['HTTP_USER_AGENT'];
- return Common::getRequestVar('ua', is_null($default) ? false : $default, 'string', $this->params);
+ $default = false;
+
+ if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) {
+ $default = $_SERVER['HTTP_USER_AGENT'];
+ }
+
+ return Common::getRequestVar('ua', $default, 'string', $this->params);
}
+ public function getCustomVariablesInVisitScope()
+ {
+ return $this->getCustomVariables('visit');
+ }
+
+ public function getCustomVariablesInPageScope()
+ {
+ return $this->getCustomVariables('page');
+ }
+
+ /**
+ * @deprecated since Piwik 2.10.0. Use Request::getCustomVariablesInPageScope() or Request::getCustomVariablesInVisitScope() instead.
+ * When we "remove" this method we will only set visibility to "private" and pass $parameter = _cvar|cvar as an argument instead of $scope
+ */
public function getCustomVariables($scope)
{
if ($scope == 'visit') {
@@ -412,16 +474,19 @@ class Request
$parameter = 'cvar';
}
- $customVar = Common::unsanitizeInputValues(Common::getRequestVar($parameter, '', 'json', $this->params));
+ $cvar = Common::getRequestVar($parameter, '', 'json', $this->params);
+ $customVar = Common::unsanitizeInputValues($cvar);
if (!is_array($customVar)) {
return array();
}
$customVariables = array();
- $maxCustomVars = CustomVariables::getMaxCustomVariables();
+ $maxCustomVars = CustomVariables::getMaxCustomVariables();
+
foreach ($customVar as $id => $keyValue) {
$id = (int)$id;
+
if ($id < 1
|| $id > $maxCustomVars
|| count($keyValue) != 2
@@ -437,10 +502,8 @@ class Request
// We keep in the URL when Custom Variable have empty names
// and values, as it means they can be deleted server side
- $key = self::truncateCustomVariable($keyValue[0]);
- $value = self::truncateCustomVariable($keyValue[1]);
- $customVariables['custom_var_k' . $id] = $key;
- $customVariables['custom_var_v' . $id] = $value;
+ $customVariables['custom_var_k' . $id] = self::truncateCustomVariable($keyValue[0]);
+ $customVariables['custom_var_v' . $id] = self::truncateCustomVariable($keyValue[1]);
}
return $customVariables;
@@ -485,17 +548,17 @@ class Request
protected function getCookieName()
{
- return Config::getInstance()->Tracker['cookie_name'];
+ return TrackerConfig::getConfigValue('cookie_name');
}
protected function getCookieExpire()
{
- return $this->getCurrentTimestamp() + Config::getInstance()->Tracker['cookie_expire'];
+ return $this->getCurrentTimestamp() + TrackerConfig::getConfigValue('cookie_expire');
}
protected function getCookiePath()
{
- return Config::getInstance()->Tracker['cookie_path'];
+ return TrackerConfig::getConfigValue('cookie_path');
}
/**
@@ -594,9 +657,9 @@ class Request
return $plugins;
}
- public function getParamsCount()
+ public function isEmptyRequest()
{
- return count($this->params);
+ return $this->isEmptyRequest;
}
const GENERATION_TIME_MS_MAXIMUM = 3600000; // 1 hour
@@ -637,18 +700,19 @@ class Request
* @return mixed|string
* @throws Exception
*/
- private function getIpString()
+ public function getIpString()
{
$cip = $this->getParam('cip');
- if(empty($cip)) {
+ if (empty($cip)) {
return IP::getIpFromHeader();
}
- if(!$this->isAuthenticated()) {
+ if (!$this->isAuthenticated()) {
Common::printDebug("WARN: Tracker API 'cip' was used with invalid token_auth");
return IP::getIpFromHeader();
}
+
return $cip;
}
}
diff --git a/core/Tracker/RequestSet.php b/core/Tracker/RequestSet.php
new file mode 100644
index 0000000000..4d8771dc79
--- /dev/null
+++ b/core/Tracker/RequestSet.php
@@ -0,0 +1,258 @@
+<?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\Tracker;
+
+use Piwik\Common;
+use Piwik\Piwik;
+use Piwik\Plugins\SitesManager\SiteUrls;
+use Piwik\Url;
+
+class RequestSet
+{
+ /**
+ * The set of visits to track.
+ *
+ * @var Request[]
+ */
+ private $requests = null;
+
+ /**
+ * The token auth supplied with a bulk visits POST.
+ *
+ * @var string
+ */
+ private $tokenAuth = null;
+
+ private $env = array();
+
+ public function setRequests($requests)
+ {
+ $this->requests = array();
+
+ foreach ($requests as $request) {
+
+ if (empty($request) && !is_array($request)) {
+ continue;
+ }
+
+ if (!$request instanceof Request) {
+ $request = new Request($request, $this->getTokenAuth());
+ }
+
+ $this->requests[] = $request;
+ }
+ }
+
+ public function setTokenAuth($tokenAuth)
+ {
+ $this->tokenAuth = $tokenAuth;
+ }
+
+ public function getNumberOfRequests()
+ {
+ if (is_array($this->requests)) {
+ return count($this->requests);
+ }
+
+ return 0;
+ }
+
+ public function getRequests()
+ {
+ if (!$this->areRequestsInitialized()) {
+ return array();
+ }
+
+ return $this->requests;
+ }
+
+ public function getTokenAuth()
+ {
+ if (!is_null($this->tokenAuth)) {
+ return $this->tokenAuth;
+ }
+
+ return Common::getRequestVar('token_auth', false);
+ }
+
+ private function areRequestsInitialized()
+ {
+ return !is_null($this->requests);
+ }
+
+ public function initRequestsAndTokenAuth()
+ {
+ if ($this->areRequestsInitialized()) {
+ return;
+ }
+
+ /**
+ * Triggered when detecting tracking requests. A plugin can use this event to set
+ * requests that should be tracked by calling the {@link RequestSet::setRequests()} method.
+ * For example the BulkTracking plugin uses this event to detect tracking requests and auth token based on
+ * a sent JSON instead of default $_GET+$_POST. It would allow you for example to track requests based on
+ * XML or you could import tracking requests stored in a file.
+ *
+ * @param \Piwik\Tracker\RequestSet &$requestSet Call {@link setRequests()} to initialize requests and
+ * {@link setTokenAuth()} to set a detected auth token.
+ *
+ * @ignore This event is not public yet as the RequestSet API is not really stable yet
+ */
+ Piwik::postEvent('Tracker.initRequestSet', array($this));
+
+ if (!$this->areRequestsInitialized()) {
+ $this->requests = array();
+
+ if (!empty($_GET) || !empty($_POST)) {
+ $this->setRequests(array($_GET + $_POST));
+ }
+ }
+ }
+
+ public function hasRequests()
+ {
+ return !empty($this->requests);
+ }
+
+ protected function getRedirectUrl()
+ {
+ return Common::getRequestVar('redirecturl', false, 'string');
+ }
+
+ protected function hasRedirectUrl()
+ {
+ $redirectUrl = $this->getRedirectUrl();
+
+ return !empty($redirectUrl);
+ }
+
+ protected function getAllSiteIdsWithinRequest()
+ {
+ if (empty($this->requests)) {
+ return array();
+ }
+
+ $siteIds = array();
+ foreach ($this->requests as $request) {
+ $siteIds[] = (int) $request->getIdSite();
+ }
+
+ return array_values(array_unique($siteIds));
+ }
+
+ // TODO maybe move to reponse? or somewhere else? not sure where!
+ public function shouldPerformRedirectToUrl()
+ {
+ if (!$this->hasRedirectUrl()) {
+ return false;
+ }
+
+ if (!$this->hasRequests()) {
+ return false;
+ }
+
+ $redirectUrl = $this->getRedirectUrl();
+ $host = Url::getHostFromUrl($redirectUrl);
+
+ if (empty($host)) {
+ return false;
+ }
+
+ $urls = new SiteUrls();
+ $siteUrls = $urls->getAllCachedSiteUrls();
+ $siteIds = $this->getAllSiteIdsWithinRequest();
+
+ foreach ($siteIds as $siteId) {
+ if (empty($siteUrls[$siteId])) {
+ $siteUrls[$siteId] = array();
+ }
+
+ if (Url::isHostInUrls($host, $siteUrls[$siteId])) {
+ return $redirectUrl;
+ }
+ }
+
+ return false;
+ }
+
+ public function getState()
+ {
+ $requests = array(
+ 'requests' => array(),
+ 'env' => $this->getEnvironment(),
+ 'tokenAuth' => $this->getTokenAuth(),
+ 'time' => time()
+ );
+
+ foreach ($this->getRequests() as $request) {
+ $requests['requests'][] = $request->getRawParams();
+ }
+
+ return $requests;
+ }
+
+ public function restoreState($state)
+ {
+ $backupEnv = $this->getCurrentEnvironment();
+
+ $this->setEnvironment($state['env']);
+ $this->setTokenAuth($state['tokenAuth']);
+
+ $this->restoreEnvironment();
+ $this->setRequests($state['requests']);
+
+ foreach ($this->getRequests() as $request) {
+ $request->setCurrentTimestamp($state['time']);
+ }
+
+ $this->resetEnvironment($backupEnv);
+ }
+
+ public function rememberEnvironment()
+ {
+ $this->setEnvironment($this->getEnvironment());
+ }
+
+ public function setEnvironment($env)
+ {
+ $this->env = $env;
+ }
+
+ protected function getEnvironment()
+ {
+ if (!empty($this->env)) {
+ return $this->env;
+ }
+
+ return $this->getCurrentEnvironment();
+ }
+
+ public function restoreEnvironment()
+ {
+ if (empty($this->env)) {
+ return;
+ }
+
+ $this->resetEnvironment($this->env);
+ }
+
+ private function resetEnvironment($env)
+ {
+ $_SERVER = $env['server'];
+ }
+
+ private function getCurrentEnvironment()
+ {
+ return array(
+ 'server' => $_SERVER
+ );
+ }
+
+
+}
diff --git a/core/Tracker/Response.php b/core/Tracker/Response.php
new file mode 100644
index 0000000000..5258d65cd2
--- /dev/null
+++ b/core/Tracker/Response.php
@@ -0,0 +1,175 @@
+<?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\Tracker;
+
+use Exception;
+use Piwik\Common;
+use Piwik\Profiler;
+use Piwik\Timer;
+use Piwik\Tracker;
+use Piwik\Tracker\Db as TrackerDb;
+
+class Response
+{
+ private $timer;
+
+ private $content;
+
+ public function init(Tracker $tracker)
+ {
+ ob_start(); // we use ob_start only because of Common::printDebug, we should actually not really use ob_start
+
+ if ($tracker->isDebugModeEnabled()) {
+ $this->timer = new Timer();
+
+ TrackerDb::enableProfiling();
+ }
+ }
+
+ public function getOutput()
+ {
+ $this->outputAccessControlHeaders();
+
+ if (is_null($this->content) && ob_get_level() > 0) {
+ $this->content = ob_get_clean();
+ }
+
+ return $this->content;
+ }
+
+ /**
+ * Echos an error message & other information, then exits.
+ *
+ * @param Tracker $tracker
+ * @param Exception $e
+ * @param int $statusCode eg 500
+ */
+ public function outputException(Tracker $tracker, Exception $e, $statusCode)
+ {
+ Common::sendResponseCode($statusCode);
+ $this->logExceptionToErrorLog($e);
+
+ if ($tracker->isDebugModeEnabled()) {
+ Common::sendHeader('Content-Type: text/html; charset=utf-8');
+ $trailer = '<span style="color: #888888">Backtrace:<br /><pre>' . $e->getTraceAsString() . '</pre></span>';
+ $headerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutHeader.tpl');
+ $footerPage = file_get_contents(PIWIK_INCLUDE_PATH . '/plugins/Morpheus/templates/simpleLayoutFooter.tpl');
+ $headerPage = str_replace('{$HTML_TITLE}', 'Piwik &rsaquo; Error', $headerPage);
+
+ echo $headerPage . '<p>' . $this->getMessageFromException($e) . '</p>' . $trailer . $footerPage;
+ } else {
+ $this->outputApiResponse($tracker);
+ }
+ }
+
+ public function outputResponse(Tracker $tracker)
+ {
+ if (!$tracker->shouldRecordStatistics()) {
+ $this->outputApiResponse($tracker);
+ Common::printDebug("Logging disabled, display transparent logo");
+ } elseif (!$tracker->hasLoggedRequests()) {
+ Common::printDebug("Empty request => Piwik page");
+ echo "<a href='/'>Piwik</a> is a free/libre web <a href='http://piwik.org'>analytics</a> that lets you keep control of your data.";
+ } else {
+ $this->outputApiResponse($tracker);
+ Common::printDebug("Nothing to notice => default behaviour");
+ }
+
+ Common::printDebug("End of the page.");
+
+ if ($tracker->isDebugModeEnabled()
+ && $tracker->isDatabaseConnected()
+ && TrackerDb::isProfilingEnabled()) {
+ $db = Tracker::getDatabase();
+ $db->recordProfiling();
+ Profiler::displayDbTrackerProfile($db);
+ }
+
+ if ($tracker->isDebugModeEnabled()) {
+ Common::printDebug($_COOKIE);
+ Common::printDebug((string)$this->timer);
+ }
+ }
+
+ private function outputAccessControlHeaders()
+ {
+ $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
+
+ if ($requestMethod !== 'GET') {
+ $origin = isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '*';
+ Common::sendHeader('Access-Control-Allow-Origin: ' . $origin);
+ Common::sendHeader('Access-Control-Allow-Credentials: true');
+ }
+ }
+
+ private function getOutputBuffer()
+ {
+ return ob_get_contents();
+ }
+
+ protected function hasAlreadyPrintedOutput()
+ {
+ return strlen($this->getOutputBuffer()) > 0;
+ }
+
+ private function outputApiResponse(Tracker $tracker)
+ {
+ if ($tracker->isDebugModeEnabled()) {
+ return;
+ }
+
+ if ($this->hasAlreadyPrintedOutput()) {
+ return;
+ }
+
+ $request = $_GET + $_POST;
+
+ if (array_key_exists('send_image', $request) && $request['send_image'] === '0') {
+ Common::sendResponseCode(204);
+ return;
+ }
+
+ $this->outputTransparentGif();
+ }
+
+ private function outputTransparentGif ()
+ {
+ $transGifBase64 = "R0lGODlhAQABAIAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
+ Common::sendHeader('Content-Type: image/gif');
+
+ echo base64_decode($transGifBase64);
+ }
+
+ /**
+ * Gets the error message to output when a tracking request fails.
+ *
+ * @param Exception $e
+ * @return string
+ */
+ protected function getMessageFromException($e)
+ {
+ // Note: duplicated from FormDatabaseSetup.isAccessDenied
+ // Avoid leaking the username/db name when access denied
+ if ($e->getCode() == 1044 || $e->getCode() == 42000) {
+ return "Error while connecting to the Piwik database - please check your credentials in config/config.ini.php file";
+ }
+
+ if (Common::isPhpCliMode()) {
+ return $e->getMessage() . "\n" . $e->getTraceAsString();
+ }
+
+ return $e->getMessage();
+ }
+
+ protected function logExceptionToErrorLog(Exception $e)
+ {
+ error_log(sprintf("Error in Piwik (tracker): %s", str_replace("\n", " ", $this->getMessageFromException($e))));
+ }
+
+}
diff --git a/core/Tracker/ScheduledTasksRunner.php b/core/Tracker/ScheduledTasksRunner.php
new file mode 100644
index 0000000000..2a4683a4cb
--- /dev/null
+++ b/core/Tracker/ScheduledTasksRunner.php
@@ -0,0 +1,103 @@
+<?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\Tracker;
+
+
+use Piwik\Common;
+use Piwik\Config;
+use Piwik\CronArchive;
+use Piwik\Option;
+use Piwik\Piwik;
+use Piwik\SettingsPiwik;
+use Piwik\Tracker;
+use Piwik\Translate;
+
+class ScheduledTasksRunner
+{
+
+ public function shouldRun(Tracker $tracker)
+ {
+ if (Common::isPhpCliMode()) {
+ // don't run scheduled tasks in CLI mode from Tracker, this is the case
+ // where we bulk load logs & don't want to lose time with tasks
+ return false;
+ }
+
+ return $tracker->shouldRecordStatistics();
+ }
+
+ /**
+ * Tracker requests will automatically trigger the Scheduled tasks.
+ * This is useful for users who don't setup the cron,
+ * but still want daily/weekly/monthly PDF reports emailed automatically.
+ *
+ * This is similar to calling the API CoreAdminHome.runScheduledTasks
+ */
+ public function runScheduledTasks()
+ {
+ $now = time();
+
+ // Currently, there are no hourly tasks. When there are some,
+ // this could be too aggressive minimum interval (some hours would be skipped in case of low traffic)
+ $minimumInterval = TrackerConfig::getConfigValue('scheduled_tasks_min_interval');
+
+ // If the user disabled browser archiving, he has already setup a cron
+ // To avoid parallel requests triggering the Scheduled Tasks,
+ // Get last time tasks started executing
+ $cache = Cache::getCacheGeneral();
+
+ if ($minimumInterval <= 0
+ || empty($cache['isBrowserTriggerEnabled'])
+ ) {
+ Common::printDebug("-> Scheduled tasks not running in Tracker: Browser archiving is disabled.");
+ return;
+ }
+
+ $nextRunTime = $cache['lastTrackerCronRun'] + $minimumInterval;
+
+ if ((defined('DEBUG_FORCE_SCHEDULED_TASKS') && DEBUG_FORCE_SCHEDULED_TASKS)
+ || $cache['lastTrackerCronRun'] === false
+ || $nextRunTime < $now
+ ) {
+ $cache['lastTrackerCronRun'] = $now;
+ Cache::setCacheGeneral($cache);
+ Tracker::initCorePiwikInTrackerMode();
+ Option::set('lastTrackerCronRun', $cache['lastTrackerCronRun']);
+ Common::printDebug('-> Scheduled Tasks: Starting...');
+
+ // save current user privilege and temporarily assume Super User privilege
+ $isSuperUser = Piwik::hasUserSuperUserAccess();
+
+ // Scheduled tasks assume Super User is running
+ Piwik::setUserHasSuperUserAccess();
+
+ ob_start();
+ CronArchive::$url = SettingsPiwik::getPiwikUrl();
+ $cronArchive = new CronArchive();
+ $cronArchive->runScheduledTasksInTrackerMode();
+
+ $resultTasks = ob_get_contents();
+ ob_clean();
+
+ // restore original user privilege
+ Piwik::setUserHasSuperUserAccess($isSuperUser);
+
+ foreach (explode('</pre>', $resultTasks) as $resultTask) {
+ Common::printDebug(str_replace('<pre>', '', $resultTask));
+ }
+
+ Common::printDebug('Finished Scheduled Tasks.');
+ } else {
+ Common::printDebug("-> Scheduled tasks not triggered.");
+ }
+
+ Common::printDebug("Next run will be from: " . date('Y-m-d H:i:s', $nextRunTime) . ' UTC');
+ }
+}
diff --git a/core/Tracker/Settings.php b/core/Tracker/Settings.php
index 836d03650a..9b3fd7b2e5 100644
--- a/core/Tracker/Settings.php
+++ b/core/Tracker/Settings.php
@@ -8,6 +8,7 @@
*/
namespace Piwik\Tracker;
+use Piwik\Config;
use Piwik\Tracker;
use Piwik\DeviceDetectorFactory;
use Piwik\SettingsPiwik;
@@ -16,10 +17,11 @@ class Settings
{
const OS_BOT = 'BOT';
- function __construct(Request $request, $ip)
+ function __construct(Request $request, $ip, $isSameFingerprintsAcrossWebsites)
{
$this->request = $request;
$this->ipAddress = $ip;
+ $this->isSameFingerprintsAcrossWebsites = $isSameFingerprintsAcrossWebsites;
$this->configId = null;
}
@@ -78,7 +80,9 @@ class Settings
}
/**
- * Returns a 64-bit hash of all the configuration settings
+ * Returns a 64-bit hash that attemps to identify a user.
+ * Maintaining some privacy by default, eg. prevents the merging of several Piwik serve together for matching across instances..
+ *
* @param $os
* @param $browserName
* @param $browserVersion
@@ -110,8 +114,13 @@ class Settings
. $browserLang
. $salt;
+ if(!$this->isSameFingerprintsAcrossWebsites) {
+ $configString .= $this->request->getIdSite();
+ }
+
$hash = md5($configString, $raw_output = true);
return substr($hash, 0, Tracker::LENGTH_BINARY_ID);
}
+
} \ No newline at end of file
diff --git a/core/Tracker/SettingsStorage.php b/core/Tracker/SettingsStorage.php
index 6c54b1b993..7ffb3de872 100644
--- a/core/Tracker/SettingsStorage.php
+++ b/core/Tracker/SettingsStorage.php
@@ -11,33 +11,24 @@ namespace Piwik\Tracker;
use Piwik\Settings\Storage;
use Piwik\Tracker;
+use Piwik\Cache as PiwikCache;
/**
* Loads settings from tracker cache instead of database. If not yet present in tracker cache will cache it.
*/
class SettingsStorage extends Storage
{
-
protected function loadSettings()
{
- $trackerCache = Cache::getCacheGeneral();
- $settings = null;
-
- if (array_key_exists('settingsStorage', $trackerCache)) {
- $allSettings = $trackerCache['settingsStorage'];
+ $cacheId = $this->getOptionKey();
+ $cache = $this->getCache();
- if (is_array($allSettings) && array_key_exists($this->getOptionKey(), $allSettings)) {
- $settings = $allSettings[$this->getOptionKey()];
- }
+ if ($cache->contains($cacheId)) {
+ $settings = $cache->fetch($cacheId);
} else {
- $trackerCache['settingsStorage'] = array();
- }
-
- if (is_null($settings)) {
$settings = parent::loadSettings();
- $trackerCache['settingsStorage'][$this->getOptionKey()] = $settings;
- Cache::setCacheGeneral($trackerCache);
+ $cache->save($cacheId, $settings);
}
return $settings;
@@ -49,9 +40,20 @@ class SettingsStorage extends Storage
self::clearCache();
}
+ private function getCache()
+ {
+ return self::buildCache($this->getOptionKey());
+ }
+
public static function clearCache()
{
- Cache::clearCacheGeneral();
+ Cache::deleteTrackerCache();
+ self::buildCache()->flushAll();
+ }
+
+ private static function buildCache()
+ {
+ return PiwikCache::getEagerCache();
}
}
diff --git a/core/Tracker/TableLogAction.php b/core/Tracker/TableLogAction.php
index 709936f2a5..fe620035d7 100644
--- a/core/Tracker/TableLogAction.php
+++ b/core/Tracker/TableLogAction.php
@@ -10,7 +10,7 @@
namespace Piwik\Tracker;
use Piwik\Common;
-use Piwik\SegmentExpression;
+use Piwik\Segment\SegmentExpression;
use Piwik\Tracker;
/**
diff --git a/core/Tracker/TrackerConfig.php b/core/Tracker/TrackerConfig.php
new file mode 100644
index 0000000000..537dc8f0c9
--- /dev/null
+++ b/core/Tracker/TrackerConfig.php
@@ -0,0 +1,39 @@
+<?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\Tracker;
+
+use Piwik\Config;
+use Piwik\Tracker;
+
+class TrackerConfig
+{
+ /**
+ * Update Tracker config
+ *
+ * @param string $name Setting name
+ * @param mixed $value Value
+ */
+ public static function setConfigValue($name, $value)
+ {
+ $section = self::getConfig();
+ $section[$name] = $value;
+ Config::getInstance()->Tracker = $section;
+ }
+
+ public static function getConfigValue($name)
+ {
+ $config = self::getConfig();
+ return $config[$name];
+ }
+
+ private static function getConfig()
+ {
+ return Config::getInstance()->Tracker;
+ }
+}
diff --git a/core/Tracker/Visit.php b/core/Tracker/Visit.php
index 9dee4196b7..e5dbc69ef6 100644
--- a/core/Tracker/Visit.php
+++ b/core/Tracker/Visit.php
@@ -11,9 +11,13 @@ namespace Piwik\Tracker;
use Piwik\Common;
use Piwik\Config;
+use Piwik\DataAccess\ArchiveInvalidator;
+use Piwik\Date;
+use Piwik\Exception\UnexpectedWebsiteFoundException;
use Piwik\Network\IPUtils;
use Piwik\Piwik;
use Piwik\Plugin\Dimension\VisitDimension;
+use Piwik\SettingsPiwik;
use Piwik\Tracker;
/**
@@ -152,11 +156,7 @@ class Visit implements VisitInterface
$this->visitorInfo = $visitor->getVisitorInfo();
- $isLastActionInTheSameVisit = $this->isLastActionInTheSameVisit($visitor);
-
- if (!$isLastActionInTheSameVisit) {
- Common::printDebug("Visitor detected, but last action was more than 30 minutes ago...");
- }
+ $isNewVisit = $this->isVisitNew($visitor, $action);
// Known visit when:
// ( - the visitor has the Piwik cookie with the idcookie ID used by Piwik to match the visitor
@@ -165,9 +165,7 @@ class Visit implements VisitInterface
// )
// AND
// - the last page view for this visitor was less than 30 minutes ago @see isLastActionInTheSameVisit()
- if ($visitor->isVisitorKnown()
- && $isLastActionInTheSameVisit
- ) {
+ if (!$isNewVisit) {
$idReferrerActionUrl = $this->visitorInfo['visit_exit_idaction_url'];
$idReferrerActionName = $this->visitorInfo['visit_exit_idaction_name'];
@@ -203,9 +201,7 @@ class Visit implements VisitInterface
// - the visitor has the Piwik cookie but the last action was performed more than 30 min ago @see isLastActionInTheSameVisit()
// - the visitor doesn't have the Piwik cookie, and couldn't be matched in @see recognizeTheVisitor()
// - the visitor does have the Piwik cookie but the idcookie and idvisit found in the cookie didn't match to any existing visit in the DB
- if (!$visitor->isVisitorKnown()
- || !$isLastActionInTheSameVisit
- ) {
+ if ($isNewVisit) {
$this->handleNewVisit($visitor, $action, $visitIsConverted);
if (!is_null($action)) {
$action->record($visitor, 0, 0);
@@ -226,6 +222,8 @@ class Visit implements VisitInterface
}
unset($this->goalManager);
unset($action);
+
+ $this->markArchivedReportsAsInvalidIfArchiveAlreadyFinished();
}
/**
@@ -386,7 +384,7 @@ class Visit implements VisitInterface
protected function getSettingsObject()
{
if (is_null($this->userSettings)) {
- $this->userSettings = new Settings( $this->request, $this->getVisitorIp() );
+ $this->userSettings = new Settings( $this->request, $this->getVisitorIp(), SettingsPiwik::isSameFingerprintAcrossWebsites());
}
return $this->userSettings;
@@ -405,6 +403,33 @@ class Visit implements VisitInterface
&& ($lastActionTime > ($this->request->getCurrentTimestamp() - Config::getInstance()->Tracker['visit_standard_length']));
}
+ /**
+ * Returns true if the last action was not today.
+ * @param Visitor $visitor
+ * @return bool
+ */
+ private function wasLastActionNotToday(Visitor $visitor)
+ {
+ $lastActionTime = $visitor->getVisitorColumn('visit_last_action_time');
+
+ if (empty($lastActionTime)) {
+ return false;
+ }
+
+ $idSite = $this->request->getIdSite();
+ $timezone = $this->getTimezoneForSite($idSite);
+
+ if (empty($timezone)) {
+ throw new UnexpectedWebsiteFoundException('An unexpected website was found, check idSite in the request');
+ }
+
+ $date = Date::factory((int) $lastActionTime, $timezone);
+ $now = $this->request->getCurrentTimestamp();
+ $now = Date::factory((int) $now, $timezone);
+
+ return $date->toString() !== $now->toString();
+ }
+
// is the referrer host any of the registered URLs for this website?
public static function isHostKnownAliasHost($urlHost, $idSite)
{
@@ -427,7 +452,7 @@ class Visit implements VisitInterface
private static function toCanonicalHost($host)
{
- $hostLower = mb_strtolower($host, 'UTF-8');
+ $hostLower = Common::mb_strtolower($host, 'UTF-8');
return str_replace('www.', '', $hostLower);
}
@@ -547,6 +572,16 @@ class Visit implements VisitInterface
return $valuesToUpdate;
}
+ private function triggerPredicateHookOnDimensions($dimensions, $hook, Visitor $visitor, Action $action = null)
+ {
+ foreach ($dimensions as $dimension) {
+ if ($dimension->$hook($this->request, $visitor, $action)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
protected function getAllVisitDimensions()
{
$dimensions = VisitDimension::getAllDimensions();
@@ -595,4 +630,73 @@ class Visit implements VisitInterface
{
return $this->getModel()->createVisit($visit);
}
+
+ /**
+ * Determines if the tracker if the current action should be treated as the start of a new visit or
+ * an action in an existing visit.
+ *
+ * @param Visitor $visitor The current visit/visitor information.
+ * @param Action|null $action The current action being tracked.
+ * @return bool
+ */
+ public function isVisitNew(Visitor $visitor, Action $action = null)
+ {
+ if (!$visitor->isVisitorKnown()) {
+ return true;
+ }
+
+ $isLastActionInTheSameVisit = $this->isLastActionInTheSameVisit($visitor);
+
+ if (!$isLastActionInTheSameVisit) {
+ Common::printDebug("Visitor detected, but last action was more than 30 minutes ago...");
+
+ return true;
+ }
+
+ $wasLastActionYesterday = $this->wasLastActionNotToday($visitor);
+ if ($wasLastActionYesterday) {
+ Common::printDebug("Visitor detected, but last action was yesterday...");
+
+ return true;
+ }
+
+ $shouldForceNewVisit = $this->triggerPredicateHookOnDimensions($this->getAllVisitDimensions(), 'shouldForceNewVisit', $visitor, $action);
+ if ($shouldForceNewVisit) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function markArchivedReportsAsInvalidIfArchiveAlreadyFinished()
+ {
+ $idSite = (int) $this->request->getIdSite();
+ $time = $this->request->getCurrentTimestamp();
+
+ $timezone = $this->getTimezoneForSite($idSite);
+
+ if (!isset($timezone)) {
+ return;
+ }
+
+ $date = Date::factory((int) $time, $timezone);
+
+ if (!$date->isToday()) { // we don't have to handle in case date is in future as it is not allowed by tracker
+ $invalidReport = new ArchiveInvalidator();
+ $invalidReport->rememberToInvalidateArchivedReportsLater($idSite, $date);
+ }
+ }
+
+ private function getTimezoneForSite($idSite)
+ {
+ try {
+ $site = Cache::getCacheWebsiteAttributes($idSite);
+ } catch (UnexpectedWebsiteFoundException $e) {
+ return;
+ }
+
+ if (!empty($site['timezone'])) {
+ return $site['timezone'];
+ }
+ }
}
diff --git a/core/Tracker/Visit/Factory.php b/core/Tracker/Visit/Factory.php
new file mode 100644
index 0000000000..71362dddea
--- /dev/null
+++ b/core/Tracker/Visit/Factory.php
@@ -0,0 +1,48 @@
+<?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\Tracker\Visit;
+use Piwik\Piwik;
+use Piwik\Tracker\Visit;
+use Piwik\Tracker\VisitInterface;
+use Exception;
+
+class Factory
+{
+ /**
+ * Returns the Tracker_Visit object.
+ * This method can be overwritten to use a different Tracker_Visit object
+ *
+ * @throws Exception
+ * @return \Piwik\Tracker\Visit
+ */
+ public static function make()
+ {
+ $visit = null;
+
+ /**
+ * Triggered before a new **visit tracking object** is created. Subscribers to this
+ * event can force the use of a custom visit tracking object that extends from
+ * {@link Piwik\Tracker\VisitInterface}.
+ *
+ * @param \Piwik\Tracker\VisitInterface &$visit Initialized to null, but can be set to
+ * a new visit object. If it isn't modified
+ * Piwik uses the default class.
+ */
+ Piwik::postEvent('Tracker.makeNewVisitObject', array(&$visit));
+
+ if (is_null($visit)) {
+ $visit = new Visit();
+ } elseif (!($visit instanceof VisitInterface)) {
+ throw new Exception("The Visit object set in the plugin must implement VisitInterface");
+ }
+
+ return $visit;
+ }
+}
diff --git a/core/Tracker/VisitExcluded.php b/core/Tracker/VisitExcluded.php
index ca481218cb..f3d8d0cab2 100644
--- a/core/Tracker/VisitExcluded.php
+++ b/core/Tracker/VisitExcluded.php
@@ -167,8 +167,17 @@ class VisitExcluded
{
return array(
// Google
- '66.249.0.0/16',
- '64.233.172.0/24',
+ '216.239.32.0/19',
+ '64.233.160.0/19',
+ '66.249.80.0/20',
+ '72.14.192.0/18',
+ '209.85.128.0/17',
+ '66.102.0.0/20',
+ '74.125.0.0/16',
+ '64.18.0.0/20',
+ '207.126.144.0/20',
+ '173.194.0.0/16',
+
// Live/Bing/MSN
'64.4.0.0/18',
'65.52.0.0/14',
@@ -180,6 +189,7 @@ class VisitExcluded
'207.68.192.0/20',
'131.253.26.0/20',
'131.253.24.0/20',
+
// Yahoo
'72.30.198.0/20',
'72.30.196.0/20',
@@ -261,6 +271,7 @@ class VisitExcluded
$referrerUrl = $this->request->getParam('urlref');
foreach($spamHosts as $spamHost) {
+ $spamHost = trim($spamHost);
if ( strpos($referrerUrl, $spamHost) !== false) {
Common::printDebug('Referrer URL is a known spam: ' . $spamHost);
return true;
diff --git a/core/Translate.php b/core/Translate.php
index c29d0f0339..5b46a2e71a 100644
--- a/core/Translate.php
+++ b/core/Translate.php
@@ -9,14 +9,16 @@
namespace Piwik;
use Exception;
+use Piwik\Container\StaticContainer;
+use Piwik\Plugin\Manager;
+use Piwik\Translation\Translator;
/**
+ * @deprecated Use Piwik\Translation\Translator instead.
+ * @see \Piwik\Translation\Translator
*/
class Translate
{
- private static $languageToLoad = null;
- private static $loadedLanguage = false;
-
/**
* Clean a string that may contain HTML special chars, single/double quotes, HTML entities, leading/trailing whitespace
*
@@ -28,25 +30,27 @@ class Translate
return html_entity_decode(trim($s), ENT_QUOTES, 'UTF-8');
}
+ /**
+ * @deprecated
+ */
public static function loadEnglishTranslation()
{
- self::loadCoreTranslationFile('en');
+ self::loadAllTranslations();
}
+ /**
+ * @deprecated
+ */
public static function unloadEnglishTranslation()
{
- $GLOBALS['Piwik_translations'] = array();
+ self::reset();
}
+ /**
+ * @deprecated
+ */
public static function reloadLanguage($language = false)
{
- if (empty($language)) {
- $language = self::getLanguageToLoad();
- }
- self::unloadEnglishTranslation();
- self::loadEnglishTranslation();
- self::loadCoreTranslation($language);
- \Piwik\Plugin\Manager::getInstance()->loadPluginTranslations($language);
}
/**
@@ -57,41 +61,14 @@ class Translate
*/
public static function loadCoreTranslation($language = false)
{
- if (empty($language)) {
- $language = self::getLanguageToLoad();
- }
- if (self::$loadedLanguage == $language) {
- return;
- }
- self::loadCoreTranslationFile($language);
- }
-
- private static function loadCoreTranslationFile($language)
- {
- if (empty($language)) {
- return;
- }
- $path = PIWIK_INCLUDE_PATH . '/lang/' . $language . '.json';
- if (!Filesystem::isValidFilename($language) || !is_readable($path)) {
- throw new Exception(Piwik::translate('General_ExceptionLanguageFileNotFound', array($language)));
- }
- $data = file_get_contents($path);
- $translations = json_decode($data, true);
- self::mergeTranslationArray($translations);
- self::setLocale();
- self::$loadedLanguage = $language;
+ self::getTranslator()->addDirectory(PIWIK_INCLUDE_PATH . '/lang');
}
+ /**
+ * @deprecated
+ */
public static function mergeTranslationArray($translation)
{
- if (!isset($GLOBALS['Piwik_translations'])) {
- $GLOBALS['Piwik_translations'] = array();
- }
- if (empty($translation)) {
- return;
- }
- // we could check that no string overlap here
- $GLOBALS['Piwik_translations'] = array_replace_recursive($GLOBALS['Piwik_translations'], $translation);
}
/**
@@ -100,46 +77,13 @@ class Translate
*/
public static function getLanguageToLoad()
{
- if (is_null(self::$languageToLoad)) {
- $lang = Common::getRequestVar('language', '', 'string');
-
- /**
- * Triggered when the current user's language is requested.
- *
- * By default the current language is determined by the **language** query
- * parameter. Plugins can override this logic by subscribing to this event.
- *
- * **Example**
- *
- * public function getLanguage(&$lang)
- * {
- * $client = new My3rdPartyAPIClient();
- * $thirdPartyLang = $client->getLanguageForUser(Piwik::getCurrentUserLogin());
- *
- * if (!empty($thirdPartyLang)) {
- * $lang = $thirdPartyLang;
- * }
- * }
- *
- * @param string &$lang The language that should be used for the current user. Will be
- * initialized to the value of the **language** query parameter.
- */
- Piwik::postEvent('User.getLanguage', array(&$lang));
-
- self::$languageToLoad = $lang;
- }
-
- return self::$languageToLoad;
+ return self::getTranslator()->getCurrentLanguage();
}
/** Reset the cached language to load. Used in tests. */
public static function reset()
{
- self::$languageToLoad = null;
- }
-
- private static function isALanguageLoaded() {
- return !empty($GLOBALS['Piwik_translations']);
+ self::getTranslator()->reset();
}
/**
@@ -148,16 +92,12 @@ class Translate
*/
public static function getLanguageLoaded()
{
- if (!self::isALanguageLoaded()) {
- return null;
- }
-
- return self::$loadedLanguage;
+ return self::getTranslator()->getCurrentLanguage();
}
public static function getLanguageDefault()
{
- return Config::getInstance()->General['default_language'];
+ return self::getTranslator()->getDefaultLanguage();
}
/**
@@ -165,77 +105,25 @@ class Translate
*/
public static function getJavascriptTranslations()
{
- $translations = & $GLOBALS['Piwik_translations'];
-
- $clientSideTranslations = array();
- foreach (self::getClientSideTranslationKeys() as $key) {
- list($plugin, $stringName) = explode("_", $key, 2);
- $clientSideTranslations[$key] = $translations[$plugin][$stringName];
- }
-
- $js = 'var translations = ' . json_encode($clientSideTranslations) . ';';
- $js .= "\n" . 'if (typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }' .
- 'for(var i in translations) { piwik_translations[i] = translations[i];} ';
- return $js;
+ return self::getTranslator()->getJavascriptTranslations();
}
- /**
- * Returns the list of client side translations by key. These translations will be outputted
- * to the translation JavaScript.
- */
- private static function getClientSideTranslationKeys()
+ public static function findTranslationKeyForTranslation($translation)
{
- $result = array();
-
- /**
- * Triggered before generating the JavaScript code that allows i18n strings to be used
- * in the browser.
- *
- * Plugins should subscribe to this event to specify which translations
- * should be available to JavaScript.
- *
- * Event handlers should add whole translation keys, ie, keys that include the plugin name.
- *
- * **Example**
- *
- * public function getClientSideTranslationKeys(&$result)
- * {
- * $result[] = "MyPlugin_MyTranslation";
- * }
- *
- * @param array &$result The whole list of client side translation keys.
- */
- Piwik::postEvent('Translate.getClientSideTranslationKeys', array(&$result));
-
- $result = array_unique($result);
-
- return $result;
+ return self::getTranslator()->findTranslationKeyForTranslation($translation);
}
/**
- * Set locale
- *
- * @see http://php.net/setlocale
+ * @return Translator
*/
- private static function setLocale()
+ private static function getTranslator()
{
- $locale = $GLOBALS['Piwik_translations']['General']['Locale'];
- $locale_variant = str_replace('UTF-8', 'UTF8', $locale);
- setlocale(LC_ALL, $locale, $locale_variant);
- setlocale(LC_CTYPE, '');
+ return StaticContainer::get('Piwik\Translation\Translator');
}
- public static function findTranslationKeyForTranslation($translation)
+ public static function loadAllTranslations()
{
- if (empty($GLOBALS['Piwik_translations'])) {
- return;
- }
-
- foreach ($GLOBALS['Piwik_translations'] as $key => $translations) {
- $possibleKey = array_search($translation, $translations);
- if (!empty($possibleKey)) {
- return $key . '_' . $possibleKey;
- }
- }
+ self::loadCoreTranslation();
+ Manager::getInstance()->loadPluginTranslations();
}
}
diff --git a/core/Translate/Filter/ByBaseTranslations.php b/core/Translate/Filter/ByBaseTranslations.php
deleted file mode 100644
index 8a2e095d95..0000000000
--- a/core/Translate/Filter/ByBaseTranslations.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?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\Translate\Filter;
-
-/**
- */
-class ByBaseTranslations extends FilterAbstract
-{
- protected $baseTranslations = array();
-
- /**
- * Sets base translations
- *
- * @param array $baseTranslations
- */
- public function __construct($baseTranslations = array())
- {
- $this->baseTranslations = $baseTranslations;
- }
-
- /**
- * Removes all translations that aren't present in the base translations set in constructor
- *
- * @param array $translations
- *
- * @return array filtered translations
- */
- public function filter($translations)
- {
- $cleanedTranslations = array();
-
- foreach ($translations as $pluginName => $pluginTranslations) {
-
- if (empty($this->baseTranslations[$pluginName])) {
- $this->filteredData[$pluginName] = $pluginTranslations;
- continue;
- }
-
- foreach ($pluginTranslations as $key => $translation) {
- if (isset($this->baseTranslations[$pluginName][$key])) {
- $cleanedTranslations[$pluginName][$key] = $translation;
- }
- }
-
- if (!empty($cleanedTranslations[$pluginName])) {
- $diff = array_diff($translations[$pluginName], $cleanedTranslations[$pluginName]);
- } else {
- $diff = $translations[$pluginName];
- }
- if (!empty($diff)) {
- $this->filteredData[$pluginName] = $diff;
- }
- }
-
- return $cleanedTranslations;
- }
-}
diff --git a/core/Translate/Filter/ByParameterCount.php b/core/Translate/Filter/ByParameterCount.php
deleted file mode 100644
index 357ab5ba33..0000000000
--- a/core/Translate/Filter/ByParameterCount.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?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\Translate\Filter;
-
-/**
- */
-class ByParameterCount extends FilterAbstract
-{
- protected $baseTranslations = array();
-
- /**
- * Sets base translations
- *
- * @param array $baseTranslations
- */
- public function __construct($baseTranslations = array())
- {
- $this->baseTranslations = $baseTranslations;
- }
-
- /**
- * Removes all translations where the placeholder parameter count differs to base translation
- *
- * @param array $translations
- *
- * @return array filtered translations
- */
- public function filter($translations)
- {
- $cleanedTranslations = array();
-
- foreach ($translations as $pluginName => $pluginTranslations) {
-
- foreach ($pluginTranslations as $key => $translation) {
-
- if (isset($this->baseTranslations[$pluginName][$key])) {
- $baseTranslation = $this->baseTranslations[$pluginName][$key];
- } else {
- // english string was deleted, do not error
- continue;
- }
-
- // ensure that translated strings have the same number of %s as the english source strings
- $baseCount = $this->_getParametersCountToReplace($baseTranslation);
- $translationCount = $this->_getParametersCountToReplace($translation);
-
- if ($baseCount != $translationCount) {
-
- $this->filteredData[$pluginName][$key] = $translation;
- continue;
- }
-
- $cleanedTranslations[$pluginName][$key] = $translation;
- }
- }
-
- return $cleanedTranslations;
- }
-
- /**
- * Counts the placeholder parameters n given string
- *
- * @param string $string
- * @return array
- */
- protected function _getParametersCountToReplace($string)
- {
- $sprintfParameters = array('%s', '%1$s', '%2$s', '%3$s', '%4$s', '%5$s', '%6$s', '%7$s', '%8$s', '%9$s');
- $count = array();
- foreach ($sprintfParameters as $parameter) {
-
- $placeholderCount = substr_count($string, $parameter);
- if ($placeholderCount > 0) {
-
- $count[$parameter] = $placeholderCount;
- }
- }
- return $count;
- }
-}
diff --git a/core/Translate/Filter/EmptyTranslations.php b/core/Translate/Filter/EmptyTranslations.php
deleted file mode 100644
index 75e3e6536f..0000000000
--- a/core/Translate/Filter/EmptyTranslations.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?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\Translate\Filter;
-
-/**
- */
-class EmptyTranslations extends FilterAbstract
-{
- /**
- * Removes all empty translations
- *
- * @param array $translations
- *
- * @return array filtered translations
- */
- public function filter($translations)
- {
- $translationsBefore = $translations;
-
- foreach ($translations as $plugin => &$pluginTranslations) {
-
- $pluginTranslations = array_filter($pluginTranslations, function ($value) {
- return !empty($value) && '' != trim($value);
- });
-
- $diff = array_diff($translationsBefore[$plugin], $pluginTranslations);
- if (!empty($diff)) {
- $this->filteredData[$plugin] = $diff;
- }
- }
-
- // remove plugins without translations
- $translations = array_filter($translations, function ($value) {
- return !empty($value) && count($value);
- });
-
- return $translations;
- }
-}
diff --git a/core/Translate/Filter/EncodedEntities.php b/core/Translate/Filter/EncodedEntities.php
deleted file mode 100644
index b7e3d6a54e..0000000000
--- a/core/Translate/Filter/EncodedEntities.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?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\Translate\Filter;
-
-use Piwik\Translate;
-
-/**
- */
-class EncodedEntities extends FilterAbstract
-{
- /**
- * Decodes all encoded entities in the given translations
- *
- * @param array $translations
- *
- * @return array filtered translations
- */
- public function filter($translations)
- {
- foreach ($translations as $pluginName => $pluginTranslations) {
- foreach ($pluginTranslations as $key => $translation) {
-
- // remove encoded entities
- $decoded = Translate::clean($translation);
- if ($translation != $decoded) {
- $this->filteredData[$pluginName][$key] = $translation;
- $translations[$pluginName][$key] = $decoded;
- continue;
- }
-
- }
- }
-
- return $translations;
- }
-}
diff --git a/core/Translate/Filter/FilterAbstract.php b/core/Translate/Filter/FilterAbstract.php
deleted file mode 100644
index 4e7ecc064d..0000000000
--- a/core/Translate/Filter/FilterAbstract.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?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\Translate\Filter;
-
-/**
- */
-abstract class FilterAbstract
-{
- protected $filteredData = array();
-
- /**
- * Filter the given translations
- *
- * @param array $translations
- *
- * @return array filtered translations
- */
- abstract public function filter($translations);
-
- /**
- * Returnes the data filtered out by the filter
- *
- * @return array
- */
- public function getFilteredData()
- {
- return $this->filteredData;
- }
-}
diff --git a/core/Translate/Filter/UnnecassaryWhitespaces.php b/core/Translate/Filter/UnnecassaryWhitespaces.php
deleted file mode 100644
index 61211fc8ab..0000000000
--- a/core/Translate/Filter/UnnecassaryWhitespaces.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?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\Translate\Filter;
-
-/**
- */
-class UnnecassaryWhitespaces extends FilterAbstract
-{
- protected $baseTranslations = array();
-
- /**
- * Sets base translations
- *
- * @param array $baseTranslations
- */
- public function __construct($baseTranslations = array())
- {
- $this->baseTranslations = $baseTranslations;
- }
-
- /**
- * Removes all unnecassary whitespaces and newlines from the given translations
- *
- * @param array $translations
- *
- * @return array filtered translations
- */
- public function filter($translations)
- {
- foreach ($translations as $pluginName => $pluginTranslations) {
- foreach ($pluginTranslations as $key => $translation) {
-
- $baseTranslation = '';
- if (isset($this->baseTranslations[$pluginName][$key])) {
- $baseTranslation = $this->baseTranslations[$pluginName][$key];
- }
-
- // remove excessive line breaks (and leading/trailing whitespace) from translations
- $stringNoLineBreak = trim($translation);
- $stringNoLineBreak = str_replace("\r", "", $stringNoLineBreak); # remove useless carrige renturns
- $stringNoLineBreak = preg_replace('/(\n[ ]+)/', "\n", $stringNoLineBreak); # remove useless white spaces after line breaks
- $stringNoLineBreak = preg_replace('/([\n]{2,})/', "\n\n", $stringNoLineBreak); # remove excessive line breaks
- if (empty($baseTranslation) || !substr_count($baseTranslation, "\n")) {
- $stringNoLineBreak = preg_replace("/[\n]+/", " ", $stringNoLineBreak); # remove all line breaks if english string doesn't contain any
- }
- $stringNoLineBreak = preg_replace('/([ ]{2,})/', " ", $stringNoLineBreak); # remove excessive white spaces again as there might be any now, after removing line breaks
- if ($translation !== $stringNoLineBreak) {
- $this->filteredData[$pluginName][$key] = $translation;
- $translations[$pluginName][$key] = $stringNoLineBreak;
- continue;
- }
- }
- }
-
- return $translations;
- }
-}
diff --git a/core/Translate/Validate/CoreTranslations.php b/core/Translate/Validate/CoreTranslations.php
deleted file mode 100644
index bb52dc1ec8..0000000000
--- a/core/Translate/Validate/CoreTranslations.php
+++ /dev/null
@@ -1,94 +0,0 @@
-<?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\Translate\Validate;
-
-use Piwik\Common;
-
-/**
- */
-class CoreTranslations extends ValidateAbstract
-{
- /**
- * Error States
- */
- const ERRORSTATE_LOCALEREQUIRED = 'Locale required';
- const ERRORSTATE_TRANSLATORINFOREQUIRED = 'Translator info required';
- const ERRORSTATE_TRANSLATOREMAILREQUIRED = 'Translator email required';
- const ERRORSTATE_LAYOUTDIRECTIONINVALID = 'Layout direction must be rtl or ltr';
- const ERRORSTATE_LOCALEINVALID = 'Locale is invalid';
- const ERRORSTATE_LOCALEINVALIDLANGUAGE = 'Locale is invalid - invalid language code';
- const ERRORSTATE_LOCALEINVALIDCOUNTRY = 'Locale is invalid - invalid country code';
-
- protected $baseTranslations = array();
-
- /**
- * Sets base translations
- *
- * @param array $baseTranslations
- */
- public function __construct($baseTranslations = array())
- {
- $this->baseTranslations = $baseTranslations;
- }
-
- /**
- * Validates the given translations
- * * There need to be more than 250 translations presen
- * * Locale, TranslatorName and TranslatorEmail needs to be set in plugin General
- * * LayoutDirection needs to be ltr or rtl if present
- * * Locale must be valid (format, language & country)
- *
- * @param array $translations
- *
- * @return boolean
- */
- public function isValid($translations)
- {
- $this->message = null;
-
- if (empty($translations['General']['Locale'])) {
- $this->message = self::ERRORSTATE_LOCALEREQUIRED;
- return false;
- }
-
- if (empty($translations['General']['TranslatorName'])) {
- $this->message = self::ERRORSTATE_TRANSLATORINFOREQUIRED;
- return false;
- }
-
- if (empty($translations['General']['TranslatorEmail'])) {
- $this->message = self::ERRORSTATE_TRANSLATOREMAILREQUIRED;
- return false;
- }
-
- if (!empty($translations['General']['LayoutDirection']) &&
- !in_array($translations['General']['LayoutDirection'], array('ltr', 'rtl'))
- ) {
- $this->message = self::ERRORSTATE_LAYOUTDIRECTIONINVALID;
- return false;
- }
-
- $allLanguages = Common::getLanguagesList();
- $allCountries = Common::getCountriesList();
-
- if (!preg_match('/^([a-z]{2})_([A-Z]{2})\.UTF-8$/', $translations['General']['Locale'], $matches)) {
- $this->message = self::ERRORSTATE_LOCALEINVALID;
- return false;
- } else if (!array_key_exists($matches[1], $allLanguages)) {
- $this->message = self::ERRORSTATE_LOCALEINVALIDLANGUAGE;
- return false;
- } else if (!array_key_exists(strtolower($matches[2]), $allCountries)) {
- $this->message = self::ERRORSTATE_LOCALEINVALIDCOUNTRY;
- return false;
- }
-
- return true;
- }
-}
diff --git a/core/Translate/Validate/NoScripts.php b/core/Translate/Validate/NoScripts.php
deleted file mode 100644
index e7f032ff55..0000000000
--- a/core/Translate/Validate/NoScripts.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?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\Translate\Validate;
-
-/**
- */
-class NoScripts extends ValidateAbstract
-{
- /**
- * Validates the given translations
- * * No script like parts should be present in any part of the translations
- *
- * @param array $translations
- *
- * @return boolean
- */
- public function isValid($translations)
- {
- $this->message = null;
-
- // check if any translation contains restricted script tags
- $serializedStrings = serialize($translations);
- $invalids = array("<script", 'document.', 'javascript:', 'src=', 'background=', 'onload=');
-
- foreach ($invalids as $invalid) {
- if (stripos($serializedStrings, $invalid) !== false) {
- $this->message = 'script tags restricted for language files';
- return false;
- }
- }
-
- return true;
- }
-}
diff --git a/core/Translate/Validate/ValidateAbstract.php b/core/Translate/Validate/ValidateAbstract.php
deleted file mode 100644
index c732d31d25..0000000000
--- a/core/Translate/Validate/ValidateAbstract.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?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\Translate\Validate;
-
-/**
- */
-abstract class ValidateAbstract
-{
- protected $message = null;
-
- /**
- * Returns if the given translations are valid
- *
- * @param array $translations
- *
- * @return boolean
- */
- abstract public function isValid($translations);
-
- /**
- * Returns an array of messages that explain why the most recent isValid()
- * call returned false.
- *
- * @return array
- */
- public function getMessage()
- {
- return $this->message;
- }
-}
diff --git a/core/Translate/Writer.php b/core/Translate/Writer.php
deleted file mode 100644
index 9ace61b74b..0000000000
--- a/core/Translate/Writer.php
+++ /dev/null
@@ -1,385 +0,0 @@
-<?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\Translate;
-
-use Exception;
-use Piwik\Container\StaticContainer;
-use Piwik\Filesystem;
-use Piwik\Piwik;
-use Piwik\Translate\Filter\FilterAbstract;
-use Piwik\Translate\Validate\ValidateAbstract;
-
-/**
- * Writes clean translations to file
- *
- */
-class Writer
-{
- /**
- * current language to write files for
- *
- * @var string
- */
- protected $language = '';
-
- /**
- * Name of a plugin (if set in contructor)
- *
- * @var string|null
- */
- protected $pluginName = null;
-
- /**
- * translations to write to file
- *
- * @var array
- */
- protected $translations = array();
-
- /**
- * Validators to check translations with
- *
- * @var ValidateAbstract[]
- */
- protected $validators = array();
-
- /**
- * Message why validation failed
- *
- * @var string|null
- */
- protected $validationMessage = null;
-
- /**
- * Filters to to apply to translations
- *
- * @var FilterAbstract[]
- */
- protected $filters = array();
-
- /**
- * Messages which filter changed the data
- *
- * @var array
- */
- protected $filterMessages = array();
-
- const UNFILTERED = 'unfiltered';
- const FILTERED = 'filtered';
-
- protected $currentState = self::UNFILTERED;
-
- /**
- * If $pluginName is given, Writer will be initialized for the given plugin if it exists
- * Otherwise it will be initialized for core translations
- *
- * @param string $language ISO 639-1 alpha-2 language code
- * @param string $pluginName optional plugin name
- * @throws \Exception
- */
- public function __construct($language, $pluginName = null)
- {
- $this->setLanguage($language);
-
- if (!empty($pluginName)) {
- $installedPlugins = \Piwik\Plugin\Manager::getInstance()->readPluginsDirectory();
-
- if (!in_array($pluginName, $installedPlugins)) {
-
- throw new Exception(Piwik::translate('General_ExceptionLanguageFileNotFound', array($pluginName)));
- }
-
- $this->pluginName = $pluginName;
- }
- }
-
- /**
- * @param string $language ISO 639-1 alpha-2 language code
- *
- * @throws \Exception
- */
- public function setLanguage($language)
- {
- if (!preg_match('/^([a-z]{2,3}(-[a-z]{2,3})?)$/i', $language)) {
- throw new Exception(Piwik::translate('General_ExceptionLanguageFileNotFound', array($language)));
- }
-
- $this->language = strtolower($language);
- }
-
- /**
- * @return string ISO 639-1 alpha-2 language code
- */
- public function getLanguage()
- {
- return $this->language;
- }
-
- /**
- * Returns if there are translations available or not
- * @return bool
- */
- public function hasTranslations()
- {
- return !empty($this->translations);
- }
-
- /**
- * Set the translations to write (and cleans them)
- *
- * @param $translations
- */
- public function setTranslations($translations)
- {
- $this->currentState = self::UNFILTERED;
- $this->translations = $translations;
- $this->applyFilters();
- }
-
- /**
- * Get translations from file
- *
- * @param string $lang ISO 639-1 alpha-2 language code
- * @throws Exception
- * @return array Array of translations ( plugin => ( key => translated string ) )
- */
- public function getTranslations($lang)
- {
- $path = $this->getTranslationPathBaseDirectory('lang', $lang);
-
- if (!is_readable($path)) {
- return array();
- }
-
- $data = file_get_contents($path);
- $translations = json_decode($data, true);
-
- return $translations;
- }
-
- /**
- * Returns the temporary path for translations
- *
- * @return string
- */
- public function getTemporaryTranslationPath()
- {
- return $this->getTranslationPathBaseDirectory('tmp');
- }
-
- /**
- * Returns the path to translation files
- *
- * @return string
- */
- public function getTranslationPath()
- {
- return $this->getTranslationPathBaseDirectory('lang');
- }
-
- /**
- * Get translation file path based on given params
- *
- * @param string $base Optional base directory (either 'lang' or 'tmp')
- * @param string|null $lang forced language
- * @throws \Exception
- * @return string path
- */
- protected function getTranslationPathBaseDirectory($base, $lang = null)
- {
- if (empty($lang)) {
- $lang = $this->getLanguage();
- }
-
- if (!empty($this->pluginName)) {
-
- if ($base == 'tmp') {
- return sprintf('%s/plugins/%s/lang/%s.json', StaticContainer::getContainer()->get('path.tmp'), $this->pluginName, $lang);
- } else {
- return sprintf('%s/plugins/%s/lang/%s.json', PIWIK_INCLUDE_PATH, $this->pluginName, $lang);
- }
- }
-
- return sprintf('%s/%s/%s.json', PIWIK_INCLUDE_PATH, $base, $lang);
- }
-
- /**
- * Converts translations to a string that can be written to a file
- *
- * @return string
- */
- public function __toString()
- {
- /*
- * Use JSON_UNESCAPED_UNICODE and JSON_PRETTY_PRINT for PHP >= 5.4
- */
- $options = 0;
- if (defined('JSON_UNESCAPED_UNICODE')) {
- $options |= JSON_UNESCAPED_UNICODE;
- }
- if (defined('JSON_PRETTY_PRINT')) {
- $options |= JSON_PRETTY_PRINT;
- }
-
- return json_encode($this->translations, $options);
- }
-
- /**
- * Save translations to file; translations should already be cleaned.
- *
- * @throws \Exception
- * @return bool|int False if failure, or number of bytes written
- */
- public function save()
- {
- $this->applyFilters();
-
- if (!$this->hasTranslations() || !$this->isValid()) {
- throw new Exception('unable to save empty or invalid translations');
- }
-
- $path = $this->getTranslationPath();
-
- Filesystem::mkdir(dirname($path));
-
- return file_put_contents($path, $this->__toString());
- }
-
- /**
- * Save translations to temporary file; translations should already be cleansed.
- *
- * @throws \Exception
- * @return bool|int False if failure, or number of bytes written
- */
- public function saveTemporary()
- {
- $this->applyFilters();
-
- if (!$this->hasTranslations() || !$this->isValid()) {
- throw new Exception('unable to save empty or invalid translations');
- }
-
- $path = $this->getTemporaryTranslationPath();
-
- Filesystem::mkdir(dirname($path));
-
- return file_put_contents($path, $this->__toString());
- }
-
- /**
- * Adds an validator to check before saving
- *
- * @param ValidateAbstract $validator
- */
- public function addValidator(ValidateAbstract $validator)
- {
- $this->validators[] = $validator;
- }
-
- /**
- * Returns if translations are valid to save or not
- *
- * @return bool
- */
- public function isValid()
- {
- $this->applyFilters();
-
- $this->validationMessage = null;
-
- foreach ($this->validators as $validator) {
- if (!$validator->isValid($this->translations)) {
- $this->validationMessage = $validator->getMessage();
- return false;
- }
- }
-
- return true;
- }
-
- /**
- * Returns last validation message
- *
- * @return null|string
- */
- public function getValidationMessage()
- {
- return $this->validationMessage;
- }
-
- /**
- * Returns if the were translations removed while cleaning
- *
- * @return bool
- */
- public function wasFiltered()
- {
- return !empty($this->filterMessages);
- }
-
- /**
- * Returns the cleaning errors
- *
- * @return array
- */
- public function getFilterMessages()
- {
- return $this->filterMessages;
- }
-
- /**
- * @param FilterAbstract $filter
- */
- public function addFilter(FilterAbstract $filter)
- {
- $this->filters[] = $filter;
- }
-
- /**
- * @throws \Exception
- *
- * @return bool error state
- */
- protected function applyFilters()
- {
- // skip if already cleaned
- if ($this->currentState == self::FILTERED) {
- return $this->wasFiltered();
- }
-
- $this->filterMessages = array();
-
- // skip if not translations available
- if (!$this->hasTranslations()) {
- $this->currentState = self::FILTERED;
- return false;
- }
-
- $cleanedTranslations = $this->translations;
-
- foreach ($this->filters as $filter) {
-
- $cleanedTranslations = $filter->filter($cleanedTranslations);
- $filteredData = $filter->getFilteredData();
- if (!empty($filteredData)) {
- $this->filterMessages[] = get_class($filter) . " changed: " . var_export($filteredData, 1);
- }
- }
-
- $this->currentState = self::FILTERED;
-
- if ($cleanedTranslations != $this->translations) {
- $this->filterMessages[] = 'translations have been cleaned';
- }
-
- $this->translations = $cleanedTranslations;
- return $this->wasFiltered();
- }
-}
diff --git a/core/Translation/Loader/DevelopmentLoader.php b/core/Translation/Loader/DevelopmentLoader.php
new file mode 100644
index 0000000000..a75af4bd2e
--- /dev/null
+++ b/core/Translation/Loader/DevelopmentLoader.php
@@ -0,0 +1,72 @@
+<?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\Translation\Loader;
+
+/**
+ * Loads a pseudo-language for developers where translation are equal to translation ids.
+ */
+class DevelopmentLoader implements LoaderInterface
+{
+ const LANGUAGE_ID = 'dev';
+
+ /**
+ * Decorated loader.
+ *
+ * @var LoaderInterface
+ */
+ private $loader;
+
+ /**
+ * @var string
+ */
+ private $fallbackLanguage = 'en';
+
+ /**
+ * @param LoaderInterface $loader Decorate another loader to add the pseudo-language.
+ */
+ public function __construct(LoaderInterface $loader)
+ {
+ $this->loader = $loader;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function load($language, array $directories)
+ {
+ if ($language !== self::LANGUAGE_ID) {
+ return $this->loader->load($language, $directories);
+ }
+
+ return $this->getDevelopmentTranslations($directories);
+ }
+
+ private function getDevelopmentTranslations(array $directories)
+ {
+ $fallbackTranslations = $this->loader->load($this->fallbackLanguage, $directories);
+
+ $translations = array();
+
+ foreach ($fallbackTranslations as $section => $sectionFallbackTranslations) {
+ $translationIds = array_keys($sectionFallbackTranslations);
+ $sectionTranslations = $this->prefixTranslationsWithSection($section, $translationIds);
+
+ $translations[$section] = array_combine($translationIds, $sectionTranslations);
+ }
+
+ return $translations;
+ }
+
+ private function prefixTranslationsWithSection($section, $translationIds)
+ {
+ return array_map(function ($translation) use ($section) {
+ return $section . '_' . $translation;
+ }, $translationIds);
+ }
+}
diff --git a/core/Translation/Loader/JsonFileLoader.php b/core/Translation/Loader/JsonFileLoader.php
new file mode 100644
index 0000000000..f80b940528
--- /dev/null
+++ b/core/Translation/Loader/JsonFileLoader.php
@@ -0,0 +1,65 @@
+<?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\Translation\Loader;
+
+use Exception;
+use Piwik\Common;
+
+/**
+ * Loads translations from JSON files.
+ */
+class JsonFileLoader implements LoaderInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function load($language, array $directories)
+ {
+ if (empty($language)) {
+ return array();
+ }
+
+ $translations = array();
+
+ foreach ($directories as $directory) {
+ $filename = $directory . '/' . $language . '.json';
+
+ if (! file_exists($filename)) {
+ continue;
+ }
+
+ $translations = array_replace_recursive(
+ $translations,
+ $this->loadFile($filename)
+ );
+ }
+
+ return $translations;
+ }
+
+ private function loadFile($filename)
+ {
+ $data = file_get_contents($filename);
+ $translations = json_decode($data, true);
+
+ if (is_null($translations) && Common::hasJsonErrorOccurred()) {
+ throw new \Exception(sprintf(
+ 'Not able to load translation file %s: %s',
+ $filename,
+ Common::getLastJsonError()
+ ));
+ }
+
+ if (!is_array($translations)) {
+ return array();
+ }
+
+ return $translations;
+ }
+}
diff --git a/core/Translation/Loader/LoaderCache.php b/core/Translation/Loader/LoaderCache.php
new file mode 100644
index 0000000000..5448e1aef4
--- /dev/null
+++ b/core/Translation/Loader/LoaderCache.php
@@ -0,0 +1,65 @@
+<?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\Translation\Loader;
+
+use Piwik\Cache;
+
+/**
+ * Caches the translations loaded by another loader.
+ */
+class LoaderCache implements LoaderInterface
+{
+ /**
+ * @var LoaderInterface
+ */
+ private $loader;
+
+ /**
+ * @var Cache\Lazy
+ */
+ private $cache;
+
+ public function __construct(LoaderInterface $loader, Cache\Lazy $cache)
+ {
+ $this->loader = $loader;
+ $this->cache = $cache;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function load($language, array $directories)
+ {
+ if (empty($language)) {
+ return array();
+ }
+
+ $cacheKey = $this->getCacheKey($language, $directories);
+
+ $translations = $this->cache->fetch($cacheKey);
+
+ if (empty($translations) || !is_array($translations)) {
+ $translations = $this->loader->load($language, $directories);
+
+ $this->cache->save($cacheKey, $translations, 43200); // ttl=12hours
+ }
+
+ return $translations;
+ }
+
+ private function getCacheKey($language, array $directories)
+ {
+ $cacheKey = 'Translations-' . $language . '-';
+
+ // in case loaded plugins change (ie Tests vs Tracker vs UI etc)
+ $cacheKey .= sha1(implode('', $directories));
+
+ return $cacheKey;
+ }
+}
diff --git a/core/Translation/Loader/LoaderInterface.php b/core/Translation/Loader/LoaderInterface.php
new file mode 100644
index 0000000000..9aae71d45b
--- /dev/null
+++ b/core/Translation/Loader/LoaderInterface.php
@@ -0,0 +1,23 @@
+<?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\Translation\Loader;
+
+/**
+ * Loads translations.
+ */
+interface LoaderInterface
+{
+ /**
+ * @param string $language
+ * @param mixed[] $directories Directories containing translation files.
+ * @throws \Exception The translation file was not found
+ * @return string[] Translations.
+ */
+ public function load($language, array $directories);
+}
diff --git a/core/Translation/Translator.php b/core/Translation/Translator.php
new file mode 100644
index 0000000000..dda946ccd9
--- /dev/null
+++ b/core/Translation/Translator.php
@@ -0,0 +1,255 @@
+<?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\Translation;
+
+use Piwik\Config;
+use Piwik\Piwik;
+use Piwik\Plugin;
+use Piwik\Translation\Loader\LoaderInterface;
+
+/**
+ * Translates messages.
+ *
+ * @api
+ */
+class Translator
+{
+ /**
+ * Contains the translated messages, indexed by the language name.
+ *
+ * @var array
+ */
+ private $translations = array();
+
+ /**
+ * @var string
+ */
+ private $currentLanguage;
+
+ /**
+ * @var string
+ */
+ private $fallback = 'en';
+
+ /**
+ * Directories containing the translations to load.
+ *
+ * @var string[]
+ */
+ private $directories = array();
+
+ /**
+ * @var LoaderInterface
+ */
+ private $loader;
+
+ public function __construct(LoaderInterface $loader, array $directories = null)
+ {
+ $this->loader = $loader;
+ $this->currentLanguage = $this->getDefaultLanguage();
+
+ if ($directories === null) {
+ // TODO should be moved out of this class
+ $directories = array(PIWIK_INCLUDE_PATH . '/lang');
+ }
+ $this->directories = $directories;
+ }
+
+ /**
+ * Returns an internationalized string using a translation ID. If a translation
+ * cannot be found for the ID, the ID is returned.
+ *
+ * @param string $translationId Translation ID, eg, `General_Date`.
+ * @param array|string|int $args `sprintf` arguments to be applied to the internationalized
+ * string.
+ * @param string|null $language Optionally force the language.
+ * @return string The translated string or `$translationId`.
+ * @api
+ */
+ public function translate($translationId, $args = array(), $language = null)
+ {
+ $args = is_array($args) ? $args : array($args);
+
+ if (strpos($translationId, "_") !== false) {
+ list($plugin, $key) = explode("_", $translationId, 2);
+ $language = is_string($language) ? $language : $this->currentLanguage;
+
+ $translationId = $this->getTranslation($translationId, $language, $plugin, $key);
+ }
+
+ if (count($args) == 0) {
+ return $translationId;
+ }
+ return vsprintf($translationId, $args);
+ }
+
+ /**
+ * @return string
+ */
+ public function getCurrentLanguage()
+ {
+ return $this->currentLanguage;
+ }
+
+ /**
+ * @param string $language
+ */
+ public function setCurrentLanguage($language)
+ {
+ if (!$language) {
+ $language = $this->getDefaultLanguage();
+ }
+
+ $this->currentLanguage = $language;
+ }
+
+ /**
+ * @return string The default configured language.
+ */
+ public function getDefaultLanguage()
+ {
+ return Config::getInstance()->General['default_language'];
+ }
+
+ /**
+ * Generate javascript translations array
+ */
+ public function getJavascriptTranslations()
+ {
+ $clientSideTranslations = array();
+ foreach ($this->getClientSideTranslationKeys() as $id) {
+ list($plugin, $key) = explode('_', $id, 2);
+ $clientSideTranslations[$id] = $this->getTranslation($id, $this->currentLanguage, $plugin, $key);
+ }
+
+ $js = 'var translations = ' . json_encode($clientSideTranslations) . ';';
+ $js .= "\n" . 'if (typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }' .
+ 'for(var i in translations) { piwik_translations[i] = translations[i];} ';
+ return $js;
+ }
+
+ /**
+ * Returns the list of client side translations by key. These translations will be outputted
+ * to the translation JavaScript.
+ */
+ private function getClientSideTranslationKeys()
+ {
+ $result = array();
+
+ /**
+ * Triggered before generating the JavaScript code that allows i18n strings to be used
+ * in the browser.
+ *
+ * Plugins should subscribe to this event to specify which translations
+ * should be available to JavaScript.
+ *
+ * Event handlers should add whole translation keys, ie, keys that include the plugin name.
+ *
+ * **Example**
+ *
+ * public function getClientSideTranslationKeys(&$result)
+ * {
+ * $result[] = "MyPlugin_MyTranslation";
+ * }
+ *
+ * @param array &$result The whole list of client side translation keys.
+ */
+ Piwik::postEvent('Translate.getClientSideTranslationKeys', array(&$result));
+
+ $result = array_unique($result);
+
+ return $result;
+ }
+
+ /**
+ * Add a directory containing translations.
+ *
+ * @param string $directory
+ */
+ public function addDirectory($directory)
+ {
+ if (isset($this->directories[$directory])) {
+ return;
+ }
+ // index by name to avoid duplicates
+ $this->directories[$directory] = $directory;
+
+ // clear currently loaded translations to force reloading them
+ $this->translations = array();
+ }
+
+ /**
+ * Should be used by tests only, and this method should eventually be removed.
+ */
+ public function reset()
+ {
+ $this->currentLanguage = $this->getDefaultLanguage();
+ $this->directories = array();
+ $this->translations = array();
+ }
+
+ /**
+ * @param string $translation
+ * @return null|string
+ */
+ public function findTranslationKeyForTranslation($translation)
+ {
+ foreach ($this->getAllTranslations() as $key => $translations) {
+ $possibleKey = array_search($translation, $translations);
+ if (!empty($possibleKey)) {
+ return $key . '_' . $possibleKey;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns all the translation messages loaded.
+ *
+ * @return array
+ */
+ public function getAllTranslations()
+ {
+ $this->loadTranslations($this->currentLanguage);
+
+ if (!isset($this->translations[$this->currentLanguage])) {
+ return array();
+ }
+
+ return $this->translations[$this->currentLanguage];
+ }
+
+ private function getTranslation($id, $lang, $plugin, $key)
+ {
+ $this->loadTranslations($lang);
+
+ if (isset($this->translations[$lang][$plugin])
+ && isset($this->translations[$lang][$plugin][$key])
+ ) {
+ return $this->translations[$lang][$plugin][$key];
+ }
+
+ // fallback
+ if ($lang !== $this->fallback) {
+ return $this->getTranslation($id, $this->fallback, $plugin, $key);
+ }
+
+ return $id;
+ }
+
+ private function loadTranslations($language)
+ {
+ if (empty($language) || isset($this->translations[$language])) {
+ return;
+ }
+
+ $this->translations[$language] = $this->loader->load($language, $this->directories);
+ }
+}
diff --git a/core/Twig.php b/core/Twig.php
index 1127d47a60..c60b430d50 100755
--- a/core/Twig.php
+++ b/core/Twig.php
@@ -12,7 +12,6 @@ use Exception;
use Piwik\Container\StaticContainer;
use Piwik\DataTable\Filter\SafeDecodeLabel;
use Piwik\Metrics\Formatter;
-use Piwik\Translate;
use Piwik\View\RenderTokenParser;
use Piwik\Visualization\Sparkline;
use Twig_Environment;
@@ -21,6 +20,7 @@ use Twig_Loader_Chain;
use Twig_Loader_Filesystem;
use Twig_SimpleFilter;
use Twig_SimpleFunction;
+use Twig_SimpleTest;
/**
* Twig class
@@ -65,7 +65,7 @@ class Twig
$chainLoader = new Twig_Loader_Chain($loaders);
// Create new Twig Environment and set cache dir
- $templatesCompiledPath = StaticContainer::getContainer()->get('path.tmp') . '/templates_c';
+ $templatesCompiledPath = StaticContainer::get('path.tmp') . '/templates_c';
$this->twig = new Twig_Environment($chainLoader,
array(
@@ -97,6 +97,19 @@ class Twig
$this->addFunction_getJavascriptTranslations();
$this->twig->addTokenParser(new RenderTokenParser());
+
+ $this->addTest_false();
+ }
+
+ private function addTest_false()
+ {
+ $test = new Twig_SimpleTest(
+ 'false',
+ function ($value) {
+ return false === $value;
+ }
+ );
+ $this->twig->addTest($test);
}
protected function addFunction_getJavascriptTranslations()
diff --git a/core/Updater.php b/core/Updater.php
index 692034d5e9..c9872d3481 100644
--- a/core/Updater.php
+++ b/core/Updater.php
@@ -8,6 +8,7 @@
*/
namespace Piwik;
use Piwik\Columns\Updater as ColumnUpdater;
+use Piwik\Exception\DatabaseSchemaIsNewerThanCodebaseException;
/**
* Load and execute all relevant, incremental update scripts for Piwik core and plugins, and bump the component version numbers for completed updates.
@@ -95,6 +96,26 @@ class Updater
return 'version_' . $name;
}
+
+ /**
+ * This method ensures that Piwik Platform cannot be running when using a NEWER database
+ */
+ public static function throwIfPiwikVersionIsOlderThanDBSchema()
+ {
+ $dbSchemaVersion = self::getCurrentRecordedComponentVersion('core');
+ $current = Version::VERSION;
+ if(-1 === version_compare($current, $dbSchemaVersion)) {
+ $messages = array(
+ Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebase', array($current, $dbSchemaVersion)),
+ Piwik::translate('General_ExceptionDatabaseVersionNewerThanCodebaseWait'),
+ // we cannot fill in the Super User emails as we are failing before Authentication was ready
+ Piwik::translate('General_ExceptionContactSupportGeneric', array('', ''))
+ );
+ throw new DatabaseSchemaIsNewerThanCodebaseException(implode(" ", $messages));
+ }
+ }
+
+
/**
* Returns a list of components (core | plugin) that need to run through the upgrade process.
*
diff --git a/core/Updates.php b/core/Updates.php
index b410effd21..85e7e589ab 100644
--- a/core/Updates.php
+++ b/core/Updates.php
@@ -56,7 +56,6 @@ abstract class Updates
static function enableMaintenanceMode()
{
$config = Config::getInstance();
- $config->init();
$tracker = $config->Tracker;
$tracker['record_statistics'] = 0;
@@ -75,7 +74,6 @@ abstract class Updates
static function disableMaintenanceMode()
{
$config = Config::getInstance();
- $config->init();
$tracker = $config->Tracker;
$tracker['record_statistics'] = 1;
@@ -91,7 +89,6 @@ abstract class Updates
public static function deletePluginFromConfigFile($pluginToDelete)
{
$config = Config::getInstance();
- $config->init();
if (isset($config->Plugins['Plugins'])) {
$plugins = $config->Plugins['Plugins'];
if (($key = array_search($pluginToDelete, $plugins)) !== false) {
diff --git a/core/Updates/2.10.0-b10.php b/core/Updates/2.10.0-b10.php
new file mode 100644
index 0000000000..3628719ccd
--- /dev/null
+++ b/core/Updates/2.10.0-b10.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\Updates;
+
+use Piwik\DataAccess\ArchiveTableCreator;
+use Piwik\Updater;
+use Piwik\Updates;
+
+class Updates_2_10_0_b10 extends Updates
+{
+
+ static function getSql()
+ {
+ $sqls = array();
+
+ $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
+
+ $archiveBlobTables = array_filter($archiveTables, function($name) {
+ return ArchiveTableCreator::getTypeFromTableName($name) == ArchiveTableCreator::BLOB_TABLE;
+ });
+
+ foreach ($archiveBlobTables as $table) {
+
+ $sqls["UPDATE " . $table . " SET name = 'DevicePlugins_plugin' WHERE name = 'UserSettings_plugin'"] = false;
+ }
+
+ return $sqls;
+ }
+
+ static function update()
+ {
+ $pluginManager = \Piwik\Plugin\Manager::getInstance();
+
+ try {
+ $pluginManager->activatePlugin('DevicePlugins');
+ } catch(\Exception $e) {
+ }
+
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+
+}
diff --git a/core/Updates/2.10.0-b4.php b/core/Updates/2.10.0-b4.php
new file mode 100644
index 0000000000..88cbaf4df4
--- /dev/null
+++ b/core/Updates/2.10.0-b4.php
@@ -0,0 +1,29 @@
+<?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\Updates;
+
+use Piwik\Updates;
+
+/**
+ * Update for version 2.10.0-b4.
+ */
+class Updates_2_10_0_b4 extends Updates
+{
+
+ static function update()
+ {
+ $pluginManager = \Piwik\Plugin\Manager::getInstance();
+
+ try {
+ $pluginManager->activatePlugin('BulkTracking');
+ } catch(\Exception $e) {
+ }
+ }
+}
diff --git a/core/Updates/2.10.0-b1.php b/core/Updates/2.10.0-b5.php
index e342788b09..1cbcb0e353 100644
--- a/core/Updates/2.10.0-b1.php
+++ b/core/Updates/2.10.0-b5.php
@@ -31,8 +31,7 @@ use Piwik\Plugins\Dashboard\Model AS DashboardModel;
* For big archives (month/year) that ment that some of the data was truncated, due to the datatable entry limit.
* To avoid that data loss / inaccuracy in the future, DevicesDetection plugin will also store archives without the version.
* For data archived after DevicesDetection plugin was enabled, those archive already exist. As we are removing the
- * UserSettings reports, we need to move the existing old data to the new archives, which means we need to build up
- * those archives, where they do not exist.
+ * UserSettings reports, there is a fallback in DevicesDetection API to build the report out of the datatable with versions.
*
* NOTE: Some archives might not contain "all" data.
* That might have happened directly after the day DevicesDetection plugin was enabled. For the days before, there were
@@ -41,7 +40,7 @@ use Piwik\Plugins\Dashboard\Model AS DashboardModel;
* contains DevicesDetection data. Day archives will always contain full data, but week/month/year archives may not.
* So we need to recreate those week/month/year archives.
*/
-class Updates_2_10_0_b1 extends Updates
+class Updates_2_10_0_b5 extends Updates
{
static function getSql()
@@ -106,12 +105,12 @@ class Updates_2_10_0_b1 extends Updates
Updater::updateDatabase(__FILE__, self::getSql());
// DeviceDetection upgrade in beta1 timed out on demo #6750
-// $archiveBlobTables = self::getAllArchiveBlobTables();
-//
-// foreach ($archiveBlobTables as $table) {
-// self::updateBrowserArchives($table);
-// self::updateOsArchives($table);
-// }
+ $archiveBlobTables = self::getAllArchiveBlobTables();
+
+ foreach ($archiveBlobTables as $table) {
+ self::updateBrowserArchives($table);
+ self::updateOsArchives($table);
+ }
}
/**
@@ -193,12 +192,6 @@ class Updates_2_10_0_b1 extends Updates
Db::get()->query(sprintf("UPDATE %s SET name = ? WHERE idarchive = ? AND name = ?", $table), array('DevicesDetection_browserVersions', $blob['idarchive'], 'UserSettings_browser'));
}
}
-
- // rebuild archives without versions
- $browserBlobs = Db::get()->fetchAll(sprintf("SELECT * FROM %s WHERE name = 'DevicesDetection_browserVersions'", $table));
- foreach ($browserBlobs as $blob) {
- self::createArchiveBlobWithoutVersions($blob, 'DevicesDetection_browsers', $table);
- }
}
public static function updateOsArchives($table) {
@@ -218,40 +211,5 @@ class Updates_2_10_0_b1 extends Updates
Db::get()->query(sprintf("UPDATE %s SET name = ? WHERE idarchive = ? AND name = ?", $table), array('DevicesDetection_osVersions', $blob['idarchive'], 'UserSettings_os'));
}
}
-
- // rebuild archives without versions
- $osBlobs = Db::get()->fetchAll(sprintf("SELECT * FROM %s WHERE name = 'DevicesDetection_osVersions'", $table));
- foreach ($osBlobs as $blob) {
- self::createArchiveBlobWithoutVersions($blob, 'DevicesDetection_os', $table);
- }
- }
-
- protected static function createArchiveBlobWithoutVersions($blob, $newName, $table)
- {
- $blob['value'] = @gzuncompress($blob['value']);
-
- try {
- $datatable = DataTable::fromSerializedArray($blob['value']);
- $datatable->filter('GroupBy', array('label', function ($label) {
- if (preg_match("/(.+) [0-9]+(?:\.[0-9]+)?$/", $label, $matches)) {
- return $matches[1]; // should match for browsers
- }
-
- if (strpos($label, ';')) {
- return substr($label, 0, 3); // should match for os
- }
-
- return $label;
- }));
-
- $newData = $datatable->getSerialized();
-
- $blob['value'] = @gzcompress($newData[0]);
- $blob['name'] = $newName;
-
- Db::get()->query(sprintf('REPLACE INTO %s (`idarchive`, `name`, `idsite`, `date1`, `date2`, `period`, `ts_archived`, `value`) VALUES (?, ? , ?, ?, ?, ?, ?, ?)', $table), array_values($blob));
- } catch (\Exception $e) {
- // fail silently and simply skip the current record
- }
}
}
diff --git a/core/Updates/2.10.0-b7.php b/core/Updates/2.10.0-b7.php
new file mode 100644
index 0000000000..a44117fc81
--- /dev/null
+++ b/core/Updates/2.10.0-b7.php
@@ -0,0 +1,43 @@
+<?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\Updates;
+
+use Piwik\DataAccess\ArchiveTableCreator;
+use Piwik\Updater;
+use Piwik\Updates;
+
+class Updates_2_10_0_b7 extends Updates
+{
+
+ static function getSql()
+ {
+ $sqls = array();
+
+ $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
+
+ $archiveBlobTables = array_filter($archiveTables, function($name) {
+ return ArchiveTableCreator::getTypeFromTableName($name) == ArchiveTableCreator::BLOB_TABLE;
+ });
+
+ foreach ($archiveBlobTables as $table) {
+
+ $sqls["UPDATE " . $table . " SET name = 'Resolution_resolution' WHERE name = 'UserSettings_resolution'"] = false;
+ $sqls["UPDATE " . $table . " SET name = 'Resolution_configuration' WHERE name = 'UserSettings_configuration'"] = false;
+ }
+
+ return $sqls;
+ }
+
+ static function update()
+ {
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+
+}
diff --git a/core/Updates/2.10.0-b8.php b/core/Updates/2.10.0-b8.php
new file mode 100644
index 0000000000..17705f8143
--- /dev/null
+++ b/core/Updates/2.10.0-b8.php
@@ -0,0 +1,26 @@
+<?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\Updates;
+
+use Piwik\Updates;
+
+class Updates_2_10_0_b8 extends Updates
+{
+ static function update()
+ {
+ $pluginManager = \Piwik\Plugin\Manager::getInstance();
+
+ try {
+ $pluginManager->activatePlugin('Resolution');
+ } catch(\Exception $e) {
+ }
+ }
+
+}
diff --git a/core/Updates/2.11.0-b2.php b/core/Updates/2.11.0-b2.php
new file mode 100644
index 0000000000..ce0c7b2533
--- /dev/null
+++ b/core/Updates/2.11.0-b2.php
@@ -0,0 +1,66 @@
+<?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\Updates;
+
+use Piwik\Common;
+use Piwik\Db;
+use Piwik\Piwik;
+use Piwik\Updater;
+use Piwik\Updates;
+use Piwik\Plugins\Dashboard\Model AS DashboardModel;
+
+/**
+ * Update for version 2.11.0-b2.
+ */
+class Updates_2_11_0_b2 extends Updates
+{
+
+ static function getSql()
+ {
+ $sqls = array();
+
+ // update dashboard to use new ecommerce widgets, they were moved from goals plugin to ecommerce
+ $oldWidgets = array(
+ array('module' => 'Goals', 'action' => 'getEcommerceLog', 'params' => array()),
+ array('module' => 'Goals', 'action' => 'widgetGoalReport', 'params' => array('idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER)),
+ );
+
+ $newWidgets = array(
+ array('module' => 'Ecommerce', 'action' => 'getEcommerceLog', 'params' => array()),
+ array('module' => 'Ecommerce', 'action' => 'widgetGoalReport', 'params' => array('idGoal' => Piwik::LABEL_ID_GOAL_IS_ECOMMERCE_ORDER)),
+ );
+
+ $allDashboards = Db::get()->fetchAll(sprintf("SELECT * FROM %s", Common::prefixTable('user_dashboard')));
+
+ foreach($allDashboards AS $dashboard) {
+
+ $dashboardLayout = json_decode($dashboard['layout']);
+ $dashboardLayout = DashboardModel::replaceDashboardWidgets($dashboardLayout, $oldWidgets, $newWidgets);
+
+ $newLayout = json_encode($dashboardLayout);
+ if ($newLayout != $dashboard['layout']) {
+ $sqls["UPDATE " . Common::prefixTable('user_dashboard') . " SET layout = '".addslashes($newLayout)."' WHERE iddashboard = ".$dashboard['iddashboard']] = false;
+ }
+ }
+
+ return $sqls;
+ }
+
+ static function update()
+ {
+ $pluginManager = \Piwik\Plugin\Manager::getInstance();
+
+ try {
+ $pluginManager->activatePlugin('Ecommerce');
+ } catch(\Exception $e) {
+ }
+
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+}
diff --git a/core/Updates/2.11.0-b4.php b/core/Updates/2.11.0-b4.php
new file mode 100644
index 0000000000..00dc1b23ee
--- /dev/null
+++ b/core/Updates/2.11.0-b4.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\Updates;
+
+use Piwik\DataAccess\ArchiveTableCreator;
+use Piwik\Updater;
+use Piwik\Updates;
+
+class Updates_2_11_0_b4 extends Updates
+{
+
+ static function getSql()
+ {
+ $sqls = array();
+
+ $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled();
+
+ $archiveBlobTables = array_filter($archiveTables, function($name) {
+ return ArchiveTableCreator::getTypeFromTableName($name) == ArchiveTableCreator::BLOB_TABLE;
+ });
+
+ foreach ($archiveBlobTables as $table) {
+
+ $sqls["UPDATE " . $table . " SET name = 'UserLanguage_language' WHERE name = 'UserSettings_language'"] = false;
+ }
+
+ return $sqls;
+ }
+
+ static function update()
+ {
+ $pluginManager = \Piwik\Plugin\Manager::getInstance();
+
+ try {
+ $pluginManager->activatePlugin('UserLanguage');
+ } catch(\Exception $e) {
+ }
+
+ Updater::updateDatabase(__FILE__, self::getSql());
+ }
+
+}
diff --git a/core/Updates/2.11.0-b5.php b/core/Updates/2.11.0-b5.php
new file mode 100644
index 0000000000..1a75fd9f88
--- /dev/null
+++ b/core/Updates/2.11.0-b5.php
@@ -0,0 +1,23 @@
+<?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\Updates;
+
+use Piwik\Plugin\Manager;
+use Piwik\Updates;
+
+class Updates_2_11_0_b5 extends Updates
+{
+ static function update()
+ {
+ try {
+ Manager::getInstance()->activatePlugin('Monolog');
+ } catch (\Exception $e) {
+ }
+ }
+}
diff --git a/core/Updates/2.11.1-b4.php b/core/Updates/2.11.1-b4.php
new file mode 100644
index 0000000000..7777f10f89
--- /dev/null
+++ b/core/Updates/2.11.1-b4.php
@@ -0,0 +1,39 @@
+<?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\Updates;
+
+use Piwik\Config;
+use Piwik\Development;
+use Piwik\Updates;
+
+class Updates_2_11_1_b4 extends Updates
+{
+ /**
+ * Here you can define any action that should be performed during the update. For instance executing SQL statements,
+ * renaming config entries, updating files, etc.
+ */
+ static function update()
+ {
+ if (!Development::isEnabled()) {
+ return;
+ }
+
+ $config = Config::getInstance();
+ $dbTests = $config->database_tests;
+
+ if ($dbTests['username'] === '@USERNAME@') {
+ $dbTests['username'] = 'root';
+ }
+
+ $config->database_tests = $dbTests;
+
+ $config->forceSave();
+ }
+}
diff --git a/core/Url.php b/core/Url.php
index d57276d5a5..04131c9827 100644
--- a/core/Url.php
+++ b/core/Url.php
@@ -28,7 +28,7 @@ use Piwik\Session;
* public function myControllerAction()
* {
* $url = Url::getCurrentQueryStringWithParametersModified(array(
- * 'module' => 'UserSettings',
+ * 'module' => 'DevicesDetection',
* 'action' => 'index'
* ));
* Url::redirectToUrl($url);
@@ -466,6 +466,22 @@ class Url
self::redirectToUrl(self::getCurrentUrlWithoutQueryString());
}
+ private static function redirectToUrlNoExit($url)
+ {
+ if (UrlHelper::isLookLikeUrl($url)
+ || strpos($url, 'index.php') === 0
+ ) {
+ Common::sendResponseCode(302);
+ Common::sendHeader("Location: $url");
+ } else {
+ echo "Invalid URL to redirect to.";
+ }
+
+ if (Common::isPhpCliMode()) {
+ throw new Exception("If you were using a browser, Piwik would redirect you to this URL: $url \n\n");
+ }
+ }
+
/**
* Redirects the user to the specified URL.
*
@@ -480,17 +496,8 @@ class Url
// but it is not always called fast enough
Session::close();
- if (UrlHelper::isLookLikeUrl($url)
- || strpos($url, 'index.php') === 0
- ) {
- Common::sendHeader("Location: $url");
- } else {
- echo "Invalid URL to redirect to.";
- }
+ self::redirectToUrlNoExit($url);
- if (Common::isPhpCliMode()) {
- throw new Exception("If you were using a browser, Piwik would redirect you to this URL: $url \n\n");
- }
exit;
}
diff --git a/core/UrlHelper.php b/core/UrlHelper.php
index 5efba99996..a0bc340bbd 100644
--- a/core/UrlHelper.php
+++ b/core/UrlHelper.php
@@ -8,6 +8,9 @@
*/
namespace Piwik;
+use Piwik\Container\StaticContainer;
+use Piwik\Intl\Data\Provider\RegionDataProvider;
+
/**
* Contains less commonly needed URL helper methods.
*
@@ -72,7 +75,9 @@ class UrlHelper
{
static $countries;
if (!isset($countries)) {
- $countries = implode('|', array_keys(Common::getCountriesList(true)));
+ /** @var RegionDataProvider $regionDataProvider */
+ $regionDataProvider = StaticContainer::get('Piwik\Intl\Data\Provider\RegionDataProvider');
+ $countries = implode('|', array_keys($regionDataProvider->getCountryList(true)));
}
return preg_replace(
@@ -411,6 +416,7 @@ class UrlHelper
|| strpos($query, sprintf('?%s=', $variableName)) !== false
// search engines with no keyword
+ || $searchEngineName == 'Ixquick'
|| $searchEngineName == 'Google Images'
|| $searchEngineName == 'DuckDuckGo')
) {
diff --git a/core/Version.php b/core/Version.php
index 0600fe25f3..5831690419 100644
--- a/core/Version.php
+++ b/core/Version.php
@@ -20,5 +20,21 @@ final class Version
* The current Piwik version.
* @var string
*/
- const VERSION = '2.10.0-b2';
+ const VERSION = '2.11.2';
+
+ public function isStableVersion($version)
+ {
+ return (bool) preg_match('/^(\d+)\.(\d+)\.(\d+)$/', $version);
+ }
+
+ public function isVersionNumber($version)
+ {
+ return $this->isStableVersion($version) || $this->isNonStableVersion($version);
+ }
+
+ private function isNonStableVersion($version)
+ {
+ return (bool) preg_match('/^(\d+)\.(\d+)\.(\d+)-.{1,4}(\d+)$/', $version);
+ }
+
}
diff --git a/core/View.php b/core/View.php
index 6200a23113..423aaeb278 100644
--- a/core/View.php
+++ b/core/View.php
@@ -221,6 +221,7 @@ class View implements ViewInterface
$this->url = Common::sanitizeInputValue(Url::getCurrentUrl());
$this->token_auth = Piwik::getCurrentUserTokenAuth();
$this->userHasSomeAdminAccess = Piwik::isUserHasSomeAdminAccess();
+ $this->userIsAnonymous = Piwik::isUserIsAnonymous();
$this->userIsSuperUser = Piwik::hasUserSuperUserAccess();
$this->latest_version_available = UpdateCheck::isNewestVersionAvailable();
$this->disableLink = Common::getRequestVar('disableLink', 0, 'int');
@@ -232,18 +233,11 @@ class View implements ViewInterface
$user = APIUsersManager::getInstance()->getUser($this->userLogin);
$this->userAlias = $user['alias'];
} catch (Exception $e) {
- Log::verbose($e);
+ Log::debug($e);
// can fail, for example at installation (no plugin loaded yet)
}
- try {
- $this->totalTimeGeneration = Registry::get('timer')->getTime();
- $this->totalNumberOfQueries = Profiler::getQueryCount();
- } catch (Exception $e) {
- $this->totalNumberOfQueries = 0;
- }
-
ProxyHttp::overrideCacheControlHeaders('no-store');
Common::sendHeader('Content-Type: ' . $this->contentType);
@@ -371,9 +365,17 @@ class View implements ViewInterface
* @ignore
*/
public static function clearCompiledTemplates()
- {
+ {
$twig = new Twig();
- $twig->getTwigEnvironment()->clearTemplateCache();
+ $environment = $twig->getTwigEnvironment();
+ $environment->clearTemplateCache();
+
+ $cacheDirectory = $environment->getCache();
+ if (!empty($cacheDirectory)
+ && is_dir($cacheDirectory)
+ ) {
+ $environment->clearCacheFiles();
+ }
}
/**
diff --git a/core/ViewDataTable/Config.php b/core/ViewDataTable/Config.php
index 1706e4b15f..220a253518 100644
--- a/core/ViewDataTable/Config.php
+++ b/core/ViewDataTable/Config.php
@@ -586,7 +586,7 @@ class Config
* references the one that is currently being displayed, it will not be added to the related
* report list.
*
- * @param string $relatedReport The plugin and method of the report, eg, `'UserSettings.getBrowser'`.
+ * @param string $relatedReport The plugin and method of the report, eg, `'DevicesDetection.getBrowsers'`.
* @param string $title The report's display name, eg, `'Browsers'`.
* @param array $queryParams Any extra query parameters to set in releated report's URL, eg,
* `array('idGoal' => 'ecommerceOrder')`.
@@ -620,8 +620,8 @@ class Config
* titles, eg,
* ```
* array(
- * 'UserSettings.getBrowser' => 'Browsers',
- * 'UserSettings.getConfiguration' => 'Configurations'
+ * 'DevicesDetection.getBrowsers' => 'Browsers',
+ * 'Resolution.getConfiguration' => 'Configurations'
* )
* ```
*/
diff --git a/core/ViewDataTable/Factory.php b/core/ViewDataTable/Factory.php
index 9ac90a1723..a5de6b3f06 100644
--- a/core/ViewDataTable/Factory.php
+++ b/core/ViewDataTable/Factory.php
@@ -80,7 +80,7 @@ class Factory
* If nothing is configured for the report and `null` is supplied for this
* argument, **table** is used.
* @param bool|false|string $apiAction The API method for the report that will be displayed, eg,
- * `'UserSettings.getBrowser'`.
+ * `'DevicesDetection.getBrowsers'`.
* @param bool|false|string $controllerAction The controller name and action dedicated to displaying the report. This
* action is used when reloading reports or changing the report visualization.
* Defaulted to `$apiAction` if `false` is supplied.
@@ -95,11 +95,9 @@ class Factory
$controllerAction = $apiAction;
}
- $defaultViewType = self::getDefaultViewTypeForReport($apiAction);
+ $report = self::getReport($apiAction);
- if (!$forceDefault && !empty($defaultViewType)) {
- $defaultType = $defaultViewType;
- }
+ $defaultViewType = self::getDefaultViewTypeForReport($report, $apiAction);
$isWidget = Common::getRequestVar('widget', '0', 'string');
@@ -110,18 +108,34 @@ class Factory
$params = Manager::getViewDataTableParameters($login, $controllerAction);
}
- $savedViewDataTable = false;
- if (!empty($params['viewDataTable'])) {
- $savedViewDataTable = $params['viewDataTable'];
+ if (!self::isDefaultViewTypeForReportFixed($report)) {
+ $savedViewDataTable = false;
+ if (!empty($params['viewDataTable'])) {
+ $savedViewDataTable = $params['viewDataTable'];
+ }
+
+ // order of default viewDataTables' priority is: function specified default, saved default, configured default for report
+ // function specified default is preferred
+ // -> force default == true : defaultType ?: saved ?: defaultView
+ // -> force default == false : saved ?: defaultType ?: defaultView
+ if ($forceDefault) {
+ $defaultType = $defaultType ?: $savedViewDataTable ?: $defaultViewType;
+ } else {
+ $defaultType = $savedViewDataTable ?: $defaultType ?: $defaultViewType;
+ }
+
+ $type = Common::getRequestVar('viewDataTable', $defaultType, 'string');
+
+ // Common::getRequestVar removes backslashes from the defaultValue in case magic quotes are enabled.
+ // therefore do not pass this as a default value to getRequestVar()
+ if ('' === $type) {
+ $type = $defaultType ?: HtmlTable::ID;
+ }
+ } else {
+ $type = $defaultViewType;
}
- $type = Common::getRequestVar('viewDataTable', $savedViewDataTable, 'string');
-
- // Common::getRequestVar removes backslashes from the defaultValue in case magic quotes are enabled.
- // therefore do not pass this as a default value to getRequestVar()
- if ('' === $type) {
- $type = $defaultType ? : HtmlTable::ID;
- }
+ $params['viewDataTable'] = $type;
$visualizations = Manager::getAvailableViewDataTables();
@@ -145,13 +159,27 @@ class Factory
}
/**
- * Returns the default viewDataTable ID to use when determining which visualization to use.
+ * Return the report object for the given apiAction
+ * @param $apiAction
+ * @return null|Report
*/
- private static function getDefaultViewTypeForReport($apiAction)
+ private static function getReport($apiAction)
{
list($module, $action) = explode('.', $apiAction);
$report = Report::factory($module, $action);
+ return $report;
+ }
+ /**
+ * Returns the default viewDataTable ID to use when determining which visualization to use.
+ *
+ * @param Report $report
+ * @param string $apiAction
+ *
+ * @return bool|string
+ */
+ private static function getDefaultViewTypeForReport($report, $apiAction)
+ {
if (!empty($report) && $report->isEnabled()) {
return $report->getDefaultTypeViewDataTable();
}
@@ -161,6 +189,21 @@ class Factory
}
/**
+ * Returns if the default viewDataTable ID to use is fixed.
+ *
+ * @param Report $report
+ * @return bool
+ */
+ private static function isDefaultViewTypeForReportFixed($report)
+ {
+ if (!empty($report) && $report->isEnabled()) {
+ return $report->alwaysUseDefaultViewDataTable();
+ }
+
+ return false;
+ }
+
+ /**
* Returns a list of default viewDataTables ID to use when determining which visualization to use for multiple
* reports.
*/
diff --git a/core/ViewDataTable/Manager.php b/core/ViewDataTable/Manager.php
index 5d05650657..aff1774368 100644
--- a/core/ViewDataTable/Manager.php
+++ b/core/ViewDataTable/Manager.php
@@ -8,6 +8,7 @@
*/
namespace Piwik\ViewDataTable;
+use Piwik\Cache;
use Piwik\Common;
use Piwik\Option;
use Piwik\Piwik;
@@ -63,6 +64,14 @@ class Manager
*/
public static function getAvailableViewDataTables()
{
+ $cache = Cache::getTransientCache();
+ $cacheId = 'ViewDataTable.getAvailableViewDataTables';
+ $dataTables = $cache->fetch($cacheId);
+
+ if (!empty($dataTables)) {
+ return $dataTables;
+ }
+
$klassToExtend = '\\Piwik\\Plugin\\ViewDataTable';
/** @var string[] $visualizations */
@@ -107,6 +116,8 @@ class Manager
$result[$vizId] = $viz;
}
+ $cache->save($cacheId, $result);
+
return $result;
}
diff --git a/core/WidgetsList.php b/core/WidgetsList.php
index 943dfceb68..e91a0f2b38 100644
--- a/core/WidgetsList.php
+++ b/core/WidgetsList.php
@@ -8,7 +8,7 @@
*/
namespace Piwik;
-use Piwik\Cache\PluginAwareStaticCache;
+use Piwik\Cache as PiwikCache;
use Piwik\Plugin\Report;
use Piwik\Plugin\Widgets;
@@ -63,9 +63,11 @@ class WidgetsList extends Singleton
*/
public static function get()
{
- $cache = self::getCacheForCompleteList();
- if (!self::$listCacheToBeInvalidated && $cache->has()) {
- return $cache->get();
+ $cache = self::getCacheForCompleteList();
+ $cacheId = self::getCacheId();
+
+ if (!self::$listCacheToBeInvalidated && $cache->contains($cacheId)) {
+ return $cache->fetch($cacheId);
}
self::addWidgets();
@@ -83,7 +85,7 @@ class WidgetsList extends Singleton
$widgets[$category] = $v;
}
- $cache->set($widgets);
+ $cache->save($cacheId, $widgets);
self::$listCacheToBeInvalidated = false;
return $widgets;
@@ -136,7 +138,7 @@ class WidgetsList extends Singleton
'VisitsSummary_VisitsSummary',
'Live!',
'General_Visitors',
- 'UserSettings_VisitorSettings',
+ 'General_VisitorSettings',
'DevicesDetection_DevicesDetection',
'General_Actions',
'Events_Events',
@@ -270,11 +272,16 @@ class WidgetsList extends Singleton
{
self::$widgets = array();
self::$hookCalled = false;
- self::getCacheForCompleteList()->clear();
+ self::getCacheForCompleteList()->delete(self::getCacheId());
+ }
+
+ private static function getCacheId()
+ {
+ return CacheId::pluginAware('WidgetsList');
}
private static function getCacheForCompleteList()
{
- return new PluginAwareStaticCache('WidgetsList');
+ return PiwikCache::getTransientCache();
}
}
diff --git a/core/bootstrap.php b/core/bootstrap.php
new file mode 100644
index 0000000000..ddb23c6016
--- /dev/null
+++ b/core/bootstrap.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Bootstraps the Piwik application.
+ *
+ * This file cannot be a class because it needs to be compatible with PHP 4.
+ */
+
+if (!defined('PIWIK_USER_PATH')) {
+ define('PIWIK_USER_PATH', PIWIK_DOCUMENT_ROOT);
+}
+
+error_reporting(E_ALL | E_NOTICE);
+@ini_set('display_errors', defined('PIWIK_DISPLAY_ERRORS') ? PIWIK_DISPLAY_ERRORS : @ini_get('display_errors'));
+@ini_set('xdebug.show_exception_trace', 0);
+@ini_set('magic_quotes_runtime', 0);
+
+// NOTE: the code above must be PHP 4 compatible
+require_once PIWIK_INCLUDE_PATH . '/core/testMinimumPhpVersion.php';
+
+session_cache_limiter('nocache');
+@date_default_timezone_set('UTC');
+
+disableEaccelerator();
+
+require_once PIWIK_INCLUDE_PATH . '/libs/upgradephp/upgrade.php';
+
+// Composer autoloader
+if (file_exists(PIWIK_INCLUDE_PATH . '/vendor/autoload.php')) {
+ $path = PIWIK_INCLUDE_PATH . '/vendor/autoload.php'; // Piwik is the main project
+} else {
+ $path = PIWIK_INCLUDE_PATH . '/../../autoload.php'; // Piwik is installed as a dependency
+}
+require_once $path;
+
+/**
+ * Eaccelerator does not support closures and is known to be not comptabile with Piwik. Therefore we are disabling
+ * it automatically. At this point it looks like Eaccelerator is no longer under development and the bug has not
+ * been fixed within a year.
+ *
+ * @link https://github.com/piwik/piwik/issues/4439#comment:8
+ * @link https://github.com/eaccelerator/eaccelerator/issues/12
+ */
+function disableEaccelerator()
+{
+ $isEacceleratorUsed = ini_get('eaccelerator.enable');
+ if (!empty($isEacceleratorUsed)) {
+ @ini_set('eaccelerator.enable', 0);
+ }
+}
diff --git a/core/dispatch.php b/core/dispatch.php
index 7f04a15664..c63f2f76b4 100644
--- a/core/dispatch.php
+++ b/core/dispatch.php
@@ -8,17 +8,12 @@
* @package Piwik
*/
-use Piwik\Error;
+use Piwik\ErrorHandler;
use Piwik\ExceptionHandler;
use Piwik\FrontController;
-use Piwik\Plugin\ControllerAdmin as PluginControllerAdmin;
-
-PluginControllerAdmin::disableEacceleratorIfEnabled();
if (!defined('PIWIK_ENABLE_ERROR_HANDLER') || PIWIK_ENABLE_ERROR_HANDLER) {
- require_once PIWIK_INCLUDE_PATH . '/core/Error.php';
- Error::setErrorHandler();
- require_once PIWIK_INCLUDE_PATH . '/core/ExceptionHandler.php';
+ ErrorHandler::registerErrorHandler();
ExceptionHandler::setUp();
}
@@ -35,16 +30,10 @@ if (PIWIK_ENABLE_DISPATCH) {
$controller->init();
$response = $controller->dispatch();
- if (is_array($response)) {
- var_export($response);
- } elseif (!is_null($response)) {
+ if (!is_null($response)) {
echo $response;
}
} catch (Exception $ex) {
- $response = $controller->getErrorResponse($ex);
-
- echo $response;
-
- exit(1);
+ ExceptionHandler::dieWithHtmlErrorPage($ex);
}
} \ No newline at end of file