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
diff options
context:
space:
mode:
authordiosmosis <benaka@piwik.pro>2015-03-18 08:30:33 +0300
committerdiosmosis <benaka@piwik.pro>2015-03-18 08:30:33 +0300
commit82865a9f6e132ecf780a9726b7562118dd80276d (patch)
tree13d174229d1f7e00d99c6763229573440b73b210 /plugins
parentacd924de32bb40be3305027b812f83ba7310bf6f (diff)
parent558877dc93ab4ca99528e1fc8cab7ed7c8b6257a (diff)
Merge branch 'master' into segment_initial_process_date
Diffstat (limited to 'plugins')
-rw-r--r--plugins/Actions/Archiver.php6
-rw-r--r--plugins/Contents/API.php2
-rw-r--r--plugins/Contents/Archiver.php10
-rw-r--r--plugins/Contents/tests/System/expected/test_Contents_Contents.getContentPieces_lastN__API.getProcessedReport_day.xml6
-rw-r--r--plugins/Contents/tests/System/expected/test_Contents__Contents.getContentPieces_day.xml6
-rw-r--r--plugins/Contents/tests/System/expected/test_Contents__Contents.getContentPieces_month.xml6
-rw-r--r--plugins/CoreUpdater/Commands/Update.php261
-rw-r--r--plugins/CoreUpdater/Commands/Update/CliUpdateObserver.php54
-rw-r--r--plugins/CoreUpdater/Controller.php28
-rw-r--r--plugins/CoreUpdater/CoreUpdater.php99
-rw-r--r--plugins/CoreUpdater/lang/en.json7
-rw-r--r--plugins/CoreUpdater/templates/oneClickResults.twig93
-rw-r--r--plugins/CoreUpdater/templates/runUpdaterAndExit_done_cli.twig51
-rw-r--r--plugins/CoreUpdater/templates/runUpdaterAndExit_welcome_cli.twig53
-rw-r--r--plugins/CoreUpdater/tests/Integration/Commands/UpdateTest.php126
-rw-r--r--plugins/CustomVariables/API.php1
-rw-r--r--plugins/CustomVariables/Archiver.php11
-rw-r--r--plugins/CustomVariables/Reports/Base.php2
-rw-r--r--plugins/DBStats/API.php13
-rw-r--r--plugins/Dashboard/API.php4
-rw-r--r--plugins/DevicePlugins/API.php23
-rw-r--r--plugins/DevicePlugins/Archiver.php11
-rw-r--r--plugins/DevicesDetection/API.php7
-rw-r--r--plugins/DevicesDetection/Archiver.php11
-rw-r--r--plugins/DevicesDetection/Reports/Base.php2
-rw-r--r--plugins/Ecommerce/Reports/BaseItem.php2
-rw-r--r--plugins/Events/Archiver.php11
-rw-r--r--plugins/Goals/API.php7
-rw-r--r--plugins/Goals/Archiver.php37
-rw-r--r--plugins/Goals/Reports/GetDaysToConversion.php2
-rw-r--r--plugins/Goals/Reports/GetVisitsUntilConversion.php2
-rw-r--r--plugins/Insights/API.php6
-rw-r--r--plugins/Installation/Controller.php4
-rw-r--r--plugins/Live/Reports/GetLastVisits.php22
-rw-r--r--plugins/Live/Reports/GetLastVisitsDetails.php2
-rw-r--r--plugins/Provider/API.php1
-rw-r--r--plugins/Provider/Archiver.php12
-rw-r--r--plugins/Referrers/API.php1
-rw-r--r--plugins/Referrers/Archiver.php11
-rw-r--r--plugins/Referrers/DataTable/Filter/UrlsForSocial.php1
-rw-r--r--plugins/Resolution/API.php1
-rw-r--r--plugins/Resolution/Archiver.php10
-rw-r--r--plugins/UserCountry/API.php1
-rw-r--r--plugins/UserCountry/Archiver.php13
-rw-r--r--plugins/UserCountry/Columns/Base.php111
-rw-r--r--plugins/UserCountry/Columns/Country.php1
-rw-r--r--plugins/UserCountry/Commands/AttributeHistoricalDataWithLocations.php218
-rw-r--r--plugins/UserCountry/VisitorGeolocator.php277
-rw-r--r--plugins/UserCountry/tests/Integration/VisitorGeolocatorTest.php378
-rw-r--r--plugins/UserCountry/tests/System/AttributeHistoricalDataWithLocationsTest.php148
-rw-r--r--plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getCity_month.xml166
-rw-r--r--plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getContinent_month.xml78
-rw-r--r--plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getCountry_month.xml206
-rw-r--r--plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getRegion_month.xml152
-rw-r--r--plugins/UserCountry/tests/Unit/UserCountryTest.php2
-rw-r--r--plugins/UserLanguage/API.php1
-rw-r--r--plugins/UserLanguage/Archiver.php10
-rw-r--r--plugins/UserLanguage/tests/System/expected/test___UserLanguage.getLanguageCode_day.xml20
-rw-r--r--plugins/UserLanguage/tests/System/expected/test___UserLanguage.getLanguage_day.xml38
-rw-r--r--plugins/UsersManager/API.php6
-rw-r--r--plugins/VisitTime/API.php2
-rw-r--r--plugins/VisitTime/Archiver.php10
-rw-r--r--plugins/VisitTime/Reports/GetByDayOfWeek.php2
-rw-r--r--plugins/VisitTime/Reports/GetVisitInformationPerLocalTime.php3
-rw-r--r--plugins/VisitTime/Reports/GetVisitInformationPerServerTime.php3
-rw-r--r--plugins/VisitorInterest/API.php4
-rw-r--r--plugins/VisitorInterest/Archiver.php10
-rw-r--r--plugins/VisitorInterest/Reports/GetNumberOfVisitsByDaysSinceLast.php2
-rw-r--r--plugins/VisitorInterest/Reports/GetNumberOfVisitsByVisitCount.php2
-rw-r--r--plugins/VisitorInterest/Reports/GetNumberOfVisitsPerPage.php2
-rw-r--r--plugins/VisitorInterest/Reports/GetNumberOfVisitsPerVisitDuration.php2
71 files changed, 2426 insertions, 467 deletions
diff --git a/plugins/Actions/Archiver.php b/plugins/Actions/Archiver.php
index 17fc4f058a..5110130b84 100644
--- a/plugins/Actions/Archiver.php
+++ b/plugins/Actions/Archiver.php
@@ -497,7 +497,8 @@ class Archiver extends \Piwik\Plugin\Archiver
ArchivingHelper::$maximumRowsInSubDataTable,
ArchivingHelper::$columnToSortByBeforeTruncation,
Metrics::$columnsAggregationOperation,
- Metrics::$columnsToRenameAfterAggregation
+ Metrics::$columnsToRenameAfterAggregation,
+ $countRowsRecursive = array()
);
$dataTableToSum = array(
@@ -511,7 +512,8 @@ class Archiver extends \Piwik\Plugin\Archiver
ArchivingHelper::$maximumRowsInSubDataTable,
ArchivingHelper::$columnToSortByBeforeTruncation,
$aggregation,
- Metrics::$columnsToRenameAfterAggregation
+ Metrics::$columnsToRenameAfterAggregation,
+ $countRowsRecursive = array()
);
$this->getProcessor()->aggregateNumericMetrics($this->getMetricNames());
diff --git a/plugins/Contents/API.php b/plugins/Contents/API.php
index 968231a702..574a24e9a3 100644
--- a/plugins/Contents/API.php
+++ b/plugins/Contents/API.php
@@ -57,8 +57,6 @@ class API extends \Piwik\Plugin\API
*/
private function filterDataTable($dataTable)
{
- $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS));
-
$dataTable->queueFilter('ReplaceColumnNames');
$dataTable->queueFilter('ReplaceSummaryRowLabel');
$dataTable->filter(function (DataTable $table) {
diff --git a/plugins/Contents/Archiver.php b/plugins/Contents/Archiver.php
index 6d3a2e05ce..ba8df12d79 100644
--- a/plugins/Contents/Archiver.php
+++ b/plugins/Contents/Archiver.php
@@ -48,7 +48,15 @@ class Archiver extends \Piwik\Plugin\Archiver
public function aggregateMultipleReports()
{
$dataTableToSum = $this->getRecordNames();
- $this->getProcessor()->aggregateDataTableRecords($dataTableToSum, $this->maximumRowsInDataTable, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation);
+ $columnsAggregationOperation = null;
+ $this->getProcessor()->aggregateDataTableRecords(
+ $dataTableToSum,
+ $this->maximumRowsInDataTable,
+ $this->maximumRowsInSubDataTable,
+ $this->columnToSortByBeforeTruncation,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
}
private function getRecordNames()
diff --git a/plugins/Contents/tests/System/expected/test_Contents_Contents.getContentPieces_lastN__API.getProcessedReport_day.xml b/plugins/Contents/tests/System/expected/test_Contents_Contents.getContentPieces_lastN__API.getProcessedReport_day.xml
index 2f19e727d9..7b1f18f074 100644
--- a/plugins/Contents/tests/System/expected/test_Contents_Contents.getContentPieces_lastN__API.getProcessedReport_day.xml
+++ b/plugins/Contents/tests/System/expected/test_Contents_Contents.getContentPieces_lastN__API.getProcessedReport_day.xml
@@ -53,13 +53,13 @@
<interaction_rate>100%</interaction_rate>
</row>
<row>
- <label>movie.mov</label>
+ <label>Content Piece not defined</label>
<nb_impressions>2</nb_impressions>
<nb_interactions>0</nb_interactions>
<interaction_rate>0%</interaction_rate>
</row>
<row>
- <label>Content Piece not defined</label>
+ <label>movie.mov</label>
<nb_impressions>2</nb_impressions>
<nb_interactions>0</nb_interactions>
<interaction_rate>0%</interaction_rate>
@@ -102,11 +102,11 @@
</row>
<row>
<contentTarget />
- <segment>contentPiece==movie.mov</segment>
</row>
<row>
<contentTarget />
+ <segment>contentPiece==movie.mov</segment>
</row>
<row>
diff --git a/plugins/Contents/tests/System/expected/test_Contents__Contents.getContentPieces_day.xml b/plugins/Contents/tests/System/expected/test_Contents__Contents.getContentPieces_day.xml
index a7e56b692a..fb7e0486ad 100644
--- a/plugins/Contents/tests/System/expected/test_Contents__Contents.getContentPieces_day.xml
+++ b/plugins/Contents/tests/System/expected/test_Contents__Contents.getContentPieces_day.xml
@@ -41,23 +41,23 @@
<segment>contentPiece==Click+NOW</segment>
</row>
<row>
- <label>movie.mov</label>
+ <label>Content Piece not defined</label>
<nb_uniq_visitors>2</nb_uniq_visitors>
<nb_visits>2</nb_visits>
<nb_impressions>2</nb_impressions>
<nb_interactions>0</nb_interactions>
<interaction_rate>0%</interaction_rate>
<contentTarget />
- <segment>contentPiece==movie.mov</segment>
</row>
<row>
- <label>Content Piece not defined</label>
+ <label>movie.mov</label>
<nb_uniq_visitors>2</nb_uniq_visitors>
<nb_visits>2</nb_visits>
<nb_impressions>2</nb_impressions>
<nb_interactions>0</nb_interactions>
<interaction_rate>0%</interaction_rate>
<contentTarget />
+ <segment>contentPiece==movie.mov</segment>
</row>
<row>
<label>Unknown</label>
diff --git a/plugins/Contents/tests/System/expected/test_Contents__Contents.getContentPieces_month.xml b/plugins/Contents/tests/System/expected/test_Contents__Contents.getContentPieces_month.xml
index 56da535eeb..512fcdb391 100644
--- a/plugins/Contents/tests/System/expected/test_Contents__Contents.getContentPieces_month.xml
+++ b/plugins/Contents/tests/System/expected/test_Contents__Contents.getContentPieces_month.xml
@@ -41,23 +41,23 @@
<segment>contentPiece==Click+NOW</segment>
</row>
<row>
- <label>movie.mov</label>
+ <label>Content Piece not defined</label>
<nb_visits>2</nb_visits>
<nb_impressions>2</nb_impressions>
<nb_interactions>0</nb_interactions>
<sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors>
<interaction_rate>0%</interaction_rate>
<contentTarget />
- <segment>contentPiece==movie.mov</segment>
</row>
<row>
- <label>Content Piece not defined</label>
+ <label>movie.mov</label>
<nb_visits>2</nb_visits>
<nb_impressions>2</nb_impressions>
<nb_interactions>0</nb_interactions>
<sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors>
<interaction_rate>0%</interaction_rate>
<contentTarget />
+ <segment>contentPiece==movie.mov</segment>
</row>
<row>
<label>Unknown</label>
diff --git a/plugins/CoreUpdater/Commands/Update.php b/plugins/CoreUpdater/Commands/Update.php
index 9eb8309e45..0f5566e394 100644
--- a/plugins/CoreUpdater/Commands/Update.php
+++ b/plugins/CoreUpdater/Commands/Update.php
@@ -8,22 +8,31 @@
*/
namespace Piwik\Plugins\CoreUpdater\Commands;
-use Piwik\Container\StaticContainer;
+use Piwik\Version;
+use Piwik\Config;
+use Piwik\DbHelper;
use Piwik\Filesystem;
+use Piwik\Piwik;
use Piwik\Plugin\ConsoleCommand;
-use Piwik\Plugins\CoreUpdater\Controller;
+use Piwik\Plugins\CoreUpdater\Commands\Update\CliUpdateObserver;
use Piwik\Plugins\CoreUpdater\NoUpdatesFoundException;
use Piwik\Plugins\UserCountry\LocationProvider;
+use Piwik\Updater;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
/**
- * @package CloudAdmin
+ * @package CoreUpdater
*/
class Update extends ConsoleCommand
{
+ /**
+ * @var string[]
+ */
+ private $migrationQueries;
+
protected function configure()
{
$this->setName('core:update');
@@ -62,10 +71,6 @@ class Update extends ConsoleCommand
} catch(NoUpdatesFoundException $e) {
// Do not fail if no updates were found
$this->writeSuccessMessage($output, array($e->getMessage()));
- } catch (\Exception $e) {
- // Fail in case of any other error during upgrade
- $output->writeln("<error>" . $e->getMessage() . "</error>");
- throw $e;
}
}
@@ -86,9 +91,245 @@ class Update extends ConsoleCommand
{
$this->checkAllRequiredOptionsAreNotEmpty($input);
- $updateController = StaticContainer::get('Piwik\Plugins\CoreUpdater\Controller');
- $content = $updateController->runUpdaterAndExit($doDryRun);
+ $updater = $this->makeUpdaterInstance($output);
+
+ $componentsWithUpdateFile = $updater->getComponentUpdates();
+ if (empty($componentsWithUpdateFile)) {
+ throw new NoUpdatesFoundException("Everything is already up to date.");
+ }
+
+ $output->writeln(array(
+ "",
+ " *** " . Piwik::translate('CoreUpdater_UpdateTitle') . " ***"
+ ));
+
+ // handle case of existing database with no tables
+ if (!DbHelper::isInstalled()) {
+ $this->handleCoreError($output, Piwik::translate('CoreUpdater_EmptyDatabaseError', Config::getInstance()->database['dbname']));
+ return;
+ }
+
+ $output->writeln(array(
+ "",
+ " " . Piwik::translate('CoreUpdater_DatabaseUpgradeRequired'),
+ "",
+ " " . Piwik::translate('CoreUpdater_YourDatabaseIsOutOfDate')
+ ));
+
+ if ($this->isUpdatingCore($componentsWithUpdateFile)) {
+ $currentVersion = $this->getCurrentVersionForCore($updater);
+ $output->writeln(array(
+ "",
+ " " . Piwik::translate('CoreUpdater_PiwikWillBeUpgradedFromVersionXToVersionY', array($currentVersion, Version::VERSION))
+ ));
+ }
+
+ $pluginsToUpdate = $this->getPluginsToUpdate($componentsWithUpdateFile);
+ if (!empty($pluginsToUpdate)) {
+ $output->writeln(array(
+ "",
+ " " . Piwik::translate('CoreUpdater_TheFollowingPluginsWillBeUpgradedX', implode(', ', $pluginsToUpdate))
+ ));
+ }
+
+ $dimensionsToUpdate = $this->getDimensionsToUpdate($componentsWithUpdateFile);
+ if (!empty($dimensionsToUpdate)) {
+ $output->writeln(array(
+ "",
+ " " . Piwik::translate('CoreUpdater_TheFollowingDimensionsWillBeUpgradedX', implode(', ', $dimensionsToUpdate))
+ ));
+ }
+
+ $output->writeln("");
+
+ if ($doDryRun) {
+ $this->doDryRun($updater, $output);
+ } else {
+ $this->doRealUpdate($updater, $componentsWithUpdateFile, $output);
+ }
+ }
+
+ private function doDryRun(Updater $updater, OutputInterface $output)
+ {
+ $migrationQueries = $this->getMigrationQueriesToExecute($updater);
+
+ $output->writeln(array(" *** Note: this is a Dry Run ***", ""));
+
+ foreach ($migrationQueries as $query) {
+ $output->writeln(" " . $query);
+ }
+
+ $output->writeln(array("", " *** End of Dry Run ***", ""));
+ }
+
+ private function doRealUpdate(Updater $updater, $componentsWithUpdateFile, OutputInterface $output)
+ {
+ $output->writeln(array(" " . Piwik::translate('CoreUpdater_TheUpgradeProcessMayTakeAWhilePleaseBePatient'), ""));
+
+ $updaterResult = $updater->updateComponents($componentsWithUpdateFile);
+
+ if (@$updaterResult['coreError']) {
+ $this->handleCoreError($output, $updaterResult['errors'], $includeDiyHelp = true);
+ return;
+ }
+
+ if (!empty($updaterResult['warnings'])) {
+ $this->outputUpdaterWarnings($output, $updaterResult['warnings']);
+ }
+
+ if (!empty($updaterResult['errors'])) {
+ $this->outputUpdaterErrors($output, $updaterResult['errors'], $updaterResult['deactivatedPlugins']);
+ }
+
+ if (!empty($updaterResult['warnings'])
+ || !empty($updaterResult['errors'])
+ ) {
+ $output->writeln(array(
+ " " . Piwik::translate('CoreUpdater_HelpMessageIntroductionWhenWarning'),
+ "",
+ " * " . $this->getUpdateHelpMessage()
+ ));
+ }
+ }
+
+ private function handleCoreError(OutputInterface $output, $errors, $includeDiyHelp = false)
+ {
+ if (!is_array($errors)) {
+ $errors = array($errors);
+ }
+
+ $output->writeln(array(
+ "",
+ " [X] " . Piwik::translate('CoreUpdater_CriticalErrorDuringTheUpgradeProcess'),
+ "",
+ ));
+
+ foreach ($errors as $errorMessage) {
+ $errorMessage = trim($errorMessage);
+ $errorMessage = str_replace("\n", "\n ", $errorMessage);
+
+ $output->writeln(" * $errorMessage");
+ }
+
+ $output->writeln(array(
+ "",
+ " " . Piwik::translate('CoreUpdater_HelpMessageIntroductionWhenError'),
+ "",
+ " * " . $this->getUpdateHelpMessage()
+ ));
+
+ if ($includeDiyHelp) {
+ $output->writeln(array(
+ "",
+ " " . Piwik::translate('CoreUpdater_ErrorDIYHelp'),
+ "",
+ " * " . Piwik::translate('CoreUpdater_ErrorDIYHelp_1'),
+ " * " . Piwik::translate('CoreUpdater_ErrorDIYHelp_2'),
+ " * " . Piwik::translate('CoreUpdater_ErrorDIYHelp_3'),
+ " * " . Piwik::translate('CoreUpdater_ErrorDIYHelp_4'),
+ " * " . Piwik::translate('CoreUpdater_ErrorDIYHelp_5')
+ ));
+ }
+
+ throw new \RuntimeException("Piwik could not be updated! See above for more information.");
+ }
+
+ private function outputUpdaterWarnings(OutputInterface $output, $warnings)
+ {
+ $output->writeln(array(
+ "",
+ " [!] " . Piwik::translate('CoreUpdater_WarningMessages'),
+ ""
+ ));
+
+ foreach ($warnings as $message) {
+ $output->writeln(" * $message");
+ }
+ }
+
+ private function outputUpdaterErrors(OutputInterface $output, $errors, $deactivatedPlugins)
+ {
+ $output->writeln(array(
+ "",
+ " [X] " . Piwik::translate('CoreUpdater_ErrorDuringPluginsUpdates'),
+ ""
+ ));
+
+ foreach ($errors as $message) {
+ $output->writeln(" * $message");
+ }
+
+ if (!empty($deactivatedPlugins)) {
+ $output->writeln(array(
+ "",
+ " [!] " . Piwik::translate('CoreUpdater_WeAutomaticallyDeactivatedTheFollowingPlugins', implode(', ', $deactivatedPlugins))
+ ));
+ }
+ }
+
+ private function getUpdateHelpMessage()
+ {
+ return Piwik::translate('CoreUpdater_HelpMessageContent', array('[',']',"\n *"));
+ }
+
+ private function isUpdatingCore($componentsWithUpdateFile)
+ {
+ foreach ($componentsWithUpdateFile as $componentName => $updates) {
+ if ($componentName == 'core') {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private function getCurrentVersionForCore(Updater $updater)
+ {
+ $currentVersion = $updater->getCurrentComponentVersion('core');
+ if ($currentVersion === false) {
+ $currentVersion = "<= 0.2.9";
+ }
+ return $currentVersion;
+ }
+
+ private function getPluginsToUpdate($componentsWithUpdateFile)
+ {
+ $plugins = array();
+ foreach ($componentsWithUpdateFile as $componentName => $updates) {
+ if ($componentName !== 'core'
+ && 0 !== strpos($componentName, 'log_')
+ ) {
+ $plugins[] = $componentName;
+ }
+ }
+ return $plugins;
+ }
+
+ private function getDimensionsToUpdate($componentsWithUpdateFile)
+ {
+ $dimensions = array();
+ foreach ($componentsWithUpdateFile as $componentName => $updates) {
+ if (0 === strpos($componentName, 'log_')) {
+ $dimensions[] = $componentName;
+ }
+ }
+ return $dimensions;
+ }
+
+ private function getMigrationQueriesToExecute(Updater $updater)
+ {
+ if (empty($this->migrationQueries)) {
+ $this->migrationQueries = $updater->getSqlQueriesToExecute();
+ }
+ return $this->migrationQueries;
+ }
+
+ private function makeUpdaterInstance(OutputInterface $output)
+ {
+ $updater = new Updater();
+
+ $migrationQueryCount = count($this->getMigrationQueriesToExecute($updater));
+ $updater->addUpdateObserver(new CliUpdateObserver($output, $migrationQueryCount));
- $output->writeln($content);
+ return $updater;
}
}
diff --git a/plugins/CoreUpdater/Commands/Update/CliUpdateObserver.php b/plugins/CoreUpdater/Commands/Update/CliUpdateObserver.php
new file mode 100644
index 0000000000..a8c95dc604
--- /dev/null
+++ b/plugins/CoreUpdater/Commands/Update/CliUpdateObserver.php
@@ -0,0 +1,54 @@
+<?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\Plugins\CoreUpdater\Commands\Update;
+
+use Piwik\Updater\UpdateObserver;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * UpdateObserver used to output progress of an update initiated on the command line. Prints the currently
+ * executing query and the total number of queries to run.
+ *
+ * @package CoreUpdater
+ */
+class CliUpdateObserver extends UpdateObserver
+{
+ /**
+ * @var OutputInterface
+ */
+ private $output;
+
+ /**
+ * @var int
+ */
+ private $totalMigrationQueryCount;
+
+ /**
+ * @var int
+ */
+ private $currentMigrationQueryExecutionCount = 0;
+
+ public function __construct(OutputInterface $output, $totalMigrationQueryCount)
+ {
+ $this->output = $output;
+ $this->totalMigrationQueryCount = $totalMigrationQueryCount;
+ }
+
+ public function onStartExecutingMigrationQuery($updateFile, $sql)
+ {
+ $this->output->write(" Executing <comment>$sql</comment>... ");
+
+ ++$this->currentMigrationQueryExecutionCount;
+ }
+
+ public function onFinishedExecutingMigrationQuery($updateFile, $sql)
+ {
+ $this->output->writeln("Done. <info>[{$this->currentMigrationQueryExecutionCount} / {$this->totalMigrationQueryCount}]</info>");
+ }
+} \ No newline at end of file
diff --git a/plugins/CoreUpdater/Controller.php b/plugins/CoreUpdater/Controller.php
index 49cb1080a2..d15dc231e6 100644
--- a/plugins/CoreUpdater/Controller.php
+++ b/plugins/CoreUpdater/Controller.php
@@ -110,7 +110,7 @@ class Controller extends \Piwik\Plugin\Controller
return $view->render();
}
- protected function redirectToDashboardWhenNoError($updater)
+ protected function redirectToDashboardWhenNoError(DbUpdater $updater)
{
if (count($updater->getSqlQueriesToExecute()) == 1
&& !$this->coreError
@@ -146,16 +146,15 @@ class Controller extends \Piwik\Plugin\Controller
public function runUpdaterAndExit($doDryRun = null)
{
$updater = new DbUpdater();
- $componentsWithUpdateFile = CoreUpdater::getComponentUpdates($updater);
+ $componentsWithUpdateFile = $updater->getComponentUpdates();
if (empty($componentsWithUpdateFile)) {
throw new NoUpdatesFoundException("Everything is already up to date.");
}
SettingsServer::setMaxExecutionTime(0);
- $cli = Common::isPhpCliMode() ? '_cli' : '';
- $welcomeTemplate = '@CoreUpdater/runUpdaterAndExit_welcome' . $cli;
- $doneTemplate = '@CoreUpdater/runUpdaterAndExit_done' . $cli;
+ $welcomeTemplate = '@CoreUpdater/runUpdaterAndExit_welcome';
+ $doneTemplate = '@CoreUpdater/runUpdaterAndExit_done';
$viewWelcome = new View($welcomeTemplate);
$this->addCustomLogoInfo($viewWelcome);
@@ -176,21 +175,6 @@ class Controller extends \Piwik\Plugin\Controller
return $viewWelcome->render();
}
- // CLI
- if (Common::isPhpCliMode()) {
- $this->doWelcomeUpdates($viewWelcome, $componentsWithUpdateFile);
- $output = $viewWelcome->render();
-
- // Proceed with upgrade in CLI only if user specifically asked for it, or if running console command
- $isUpdateRequested = Common::isRunningConsoleCommand() || Piwik::getModule() == 'CoreUpdater';
-
- if (!$this->coreError && $isUpdateRequested) {
- $this->doExecuteUpdates($viewDone, $updater, $componentsWithUpdateFile);
- $output .= $viewDone->render();
- }
- return $output;
- }
-
// Web
if ($doExecuteUpdates) {
$this->warningMessages = array();
@@ -255,9 +239,9 @@ class Controller extends \Piwik\Plugin\Controller
$view->coreToUpdate = $coreToUpdate;
}
- private function doExecuteUpdates($view, $updater, $componentsWithUpdateFile)
+ private function doExecuteUpdates($view, DbUpdater $updater, $componentsWithUpdateFile)
{
- $result = CoreUpdater::updateComponents($updater, $componentsWithUpdateFile);
+ $result = $updater->updateComponents($componentsWithUpdateFile);
$this->coreError = $result['coreError'];
$this->warningMessages = $result['warnings'];
diff --git a/plugins/CoreUpdater/CoreUpdater.php b/plugins/CoreUpdater/CoreUpdater.php
index f6f197db5d..584a465130 100644
--- a/plugins/CoreUpdater/CoreUpdater.php
+++ b/plugins/CoreUpdater/CoreUpdater.php
@@ -14,9 +14,8 @@ use Piwik\Common;
use Piwik\Filesystem;
use Piwik\FrontController;
use Piwik\Piwik;
-use Piwik\Columns\Updater as ColumnsUpdater;
use Piwik\UpdateCheck;
-use Piwik\Updater;
+use Piwik\Updater as PiwikCoreUpdater;
use Piwik\UpdaterErrorException;
use Piwik\Version;
@@ -36,89 +35,20 @@ class CoreUpdater extends \Piwik\Plugin
);
}
- public static function updateComponents(Updater $updater, $componentsWithUpdateFile)
+ /**
+ * @deprecated
+ */
+ public static function updateComponents(PiwikCoreUpdater $updater, $componentsWithUpdateFile)
{
- $warnings = array();
- $errors = array();
- $deactivatedPlugins = array();
- $coreError = false;
-
- if (!empty($componentsWithUpdateFile)) {
- $currentAccess = Access::getInstance();
- $hasSuperUserAccess = $currentAccess->hasSuperUserAccess();
-
- if (!$hasSuperUserAccess) {
- $currentAccess->setSuperUserAccess(true);
- }
-
- // if error in any core update, show message + help message + EXIT
- // if errors in any plugins updates, show them on screen, disable plugins that errored + CONTINUE
- // if warning in any core update or in any plugins update, show message + CONTINUE
- // if no error or warning, success message + CONTINUE
- foreach ($componentsWithUpdateFile as $name => $filenames) {
- try {
- $warnings = array_merge($warnings, $updater->update($name));
- } catch (UpdaterErrorException $e) {
- $errors[] = $e->getMessage();
- if ($name == 'core') {
- $coreError = true;
- break;
- } elseif (\Piwik\Plugin\Manager::getInstance()->isPluginActivated($name)) {
- \Piwik\Plugin\Manager::getInstance()->deactivatePlugin($name);
- $deactivatedPlugins[] = $name;
- }
- }
- }
-
- if (!$hasSuperUserAccess) {
- $currentAccess->setSuperUserAccess(false);
- }
- }
-
- Filesystem::deleteAllCacheOnUpdate();
-
- $result = array(
- 'warnings' => $warnings,
- 'errors' => $errors,
- 'coreError' => $coreError,
- 'deactivatedPlugins' => $deactivatedPlugins
- );
-
- /**
- * Triggered after Piwik has been updated.
- */
- Piwik::postEvent('CoreUpdater.update.end');
-
- return $result;
+ return $updater->updateComponents($componentsWithUpdateFile);
}
- public static function getComponentUpdates(Updater $updater)
+ /**
+ * @deprecated
+ */
+ public static function getComponentUpdates(PiwikCoreUpdater $updater)
{
- $updater->addComponentToCheck('core', Version::VERSION);
- $manager = \Piwik\Plugin\Manager::getInstance();
- $plugins = $manager->getLoadedPlugins();
- foreach ($plugins as $pluginName => $plugin) {
- if ($manager->isPluginInstalled($pluginName)) {
- $updater->addComponentToCheck($pluginName, $plugin->getVersion());
- }
- }
-
- $columnsVersions = ColumnsUpdater::getAllVersions();
- foreach ($columnsVersions as $component => $version) {
- $updater->addComponentToCheck($component, $version);
- }
-
- $componentsWithUpdateFile = $updater->getComponentsWithUpdateFile();
-
- if (count($componentsWithUpdateFile) == 0) {
- ColumnsUpdater::onNoUpdateAvailable($columnsVersions);
-
- if (!$updater->hasNewVersion('core')) {
- return null;
- }
- }
-
- return $componentsWithUpdateFile;
+ return $updater->getComponentUpdates();
}
public function dispatch()
@@ -126,13 +56,12 @@ class CoreUpdater extends \Piwik\Plugin
$module = Common::getRequestVar('module', '', 'string');
$action = Common::getRequestVar('action', '', 'string');
- $updater = new Updater();
- $updater->addComponentToCheck('core', Version::VERSION);
- $updates = $updater->getComponentsWithNewVersion();
+ $updater = new PiwikCoreUpdater();
+ $updates = $updater->getComponentsWithNewVersion(array('core' => Version::VERSION));
if (!empty($updates)) {
Filesystem::deleteAllCacheOnUpdate();
}
- if (self::getComponentUpdates($updater) !== null
+ if ($updater->getComponentUpdates() !== null
&& $module != 'CoreUpdater'
// Proxy module is used to redirect users to piwik.org, should still work when Piwik must be updated
&& $module != 'Proxy'
diff --git a/plugins/CoreUpdater/lang/en.json b/plugins/CoreUpdater/lang/en.json
index 2c3ffb9be2..83b946f002 100644
--- a/plugins/CoreUpdater/lang/en.json
+++ b/plugins/CoreUpdater/lang/en.json
@@ -48,9 +48,10 @@
"UpdateHasBeenCancelledExplanation": "Piwik One Click Update has been cancelled. If you can't fix the above error message, it is recommended that you manually update Piwik. %1$s Please check out the %2$sUpdate documentation%3$s to get started!",
"UpdateTitle": "Update",
"UpdateUsingHttpsFailed": "Downloading the latest Piwik version over secure HTTPS connection did not succeed, because of the following error:",
- "UpdateUsingHttpsFailedHelp": "Please note that downloading the latest Piwik version (over secure HTTPS connection) can fail for various reasons, for example in case of network error, slow network speed, or wrong system configuration.",
- "UpdateUsingHttpsFailedHelpWhatToDo": "You may continue the update via the non-secure standard HTTP connection by clicking on the button '%s'.",
- "UpdateUsingHttpMessage": "Update Piwik automatically (over the non secure HTTP connection)",
+ "UpdateUsingHttpsFailedHelp": "Why did it fail? Downloading the latest Piwik version (over secure HTTPS connection) can fail for various reasons, for example because of a network error, slow network speed or wrong system configuration. Note that it could also mean that your server is the target of a MITM attack and someone is trying to replace the update with a malicious version of Piwik.",
+ "UpdateUsingHttpsFailedHelpWhatToDo": "It is recommended to retry the download using the secure HTTPS connection as it prevents MITM attacks.",
+ "UsingHttps": "using the secure HTTPS connection (recommended)",
+ "UsingHttp": "using the non-secure HTTP connection",
"UpgradeComplete": "Upgrade complete!",
"UpgradePiwik": "Upgrade Piwik",
"VerifyingUnpackedFiles": "Verifying the unpacked files",
diff --git a/plugins/CoreUpdater/templates/oneClickResults.twig b/plugins/CoreUpdater/templates/oneClickResults.twig
index 8ded5fd604..664dc92097 100644
--- a/plugins/CoreUpdater/templates/oneClickResults.twig
+++ b/plugins/CoreUpdater/templates/oneClickResults.twig
@@ -1,50 +1,63 @@
{% extends '@CoreUpdater/layout.twig' %}
{% block content %}
-<br/>
-{% for message in feedbackMessages %}
- <p>&#10003; {{ message }}</p>
-{% endfor %}
-{% if httpsFail %}
- <br/>
- <br/>
- <div class="warning">
- <img src="plugins/Morpheus/images/warning_medium.png"/>
- {{ 'CoreUpdater_UpdateUsingHttpsFailed'|translate }}<br/>
- "{{ error }}"
- </div>
- <p>{{ 'CoreUpdater_UpdateUsingHttpsFailedHelp'|translate }}</p>
- <p>{{ 'CoreUpdater_UpdateUsingHttpsFailedHelpWhatToDo'|translate('CoreUpdater_UpdateAutomatically'|translate) }}</p>
- <div class="warning">
- {{ 'CoreUpdater_UpdateUsingHttpMessage'|translate }}
- <form id="oneclickupdate" action="index.php">
+ {% for message in feedbackMessages %}
+ <p>&#10003; {{ message }}</p>
+ {% endfor %}
+
+ {% if httpsFail %}
+ <br/>
+ <br/>
+ <div class="warning">
+ <img src="plugins/Morpheus/images/warning_medium.png"/>
+ {{ 'CoreUpdater_UpdateUsingHttpsFailed'|translate }}<br/>
+ "{{ error }}"
+ </div>
+ <p>{{ 'CoreUpdater_UpdateUsingHttpsFailedHelp'|translate }}</p>
+ <p>{{ 'CoreUpdater_UpdateUsingHttpsFailedHelpWhatToDo'|translate }}</p>
+ <form action="index.php">
+ <input type="hidden" name="module" value="CoreUpdater"/>
+ <input type="hidden" name="action" value="oneClickUpdate"/>
+ <input type="hidden" name="https" value="1"/>
+ <input id="updateUsingHttps" type="submit" value="{{ 'CoreUpdater_UpdateAutomatically'|translate }}"/>
+ {{ 'CoreUpdater_UsingHttps'|translate }}
+ </form>
+ <br/>
+ <form action="index.php">
<input type="hidden" name="module" value="CoreUpdater"/>
<input type="hidden" name="action" value="oneClickUpdate"/>
<input type="hidden" name="https" value="0"/>
- <input id="updateUsingHttp" type="submit" class="submit" value="{{ 'CoreUpdater_UpdateAutomatically'|translate }}"/>
+ <input id="updateUsingHttp" type="submit" value="{{ 'CoreUpdater_UpdateAutomatically'|translate }}"/>
+ {{ 'CoreUpdater_UsingHttp'|translate }}
+ </form>
+ <br/>
+ <form action="index.php">
+ <input type="submit" value="{{ 'General_ContinueToPiwik'|translate }}"/>
+ </form>
+ <br/>
+ {% elseif error %}
+ <br/>
+ <br/>
+ <div class="error">
+ <img src="plugins/Morpheus/images/error_medium.png"/>
+ {{ error }}
+ </div>
+ <br/>
+ <br/>
+ <div class="warning">
+ <img src="plugins/Morpheus/images/warning_medium.png"/>
+ {{ 'CoreUpdater_UpdateHasBeenCancelledExplanation'|translate("<br /><br />","<a target='_blank' href='?module=Proxy&action=redirect&url=http://piwik.org/docs/update/'>","</a>")|raw }}
+ </div>
+ <br/>
+ <br/>
+ <form action="index.php">
+ <input type="submit" class="submit" value="{{ 'General_ContinueToPiwik'|translate }}"/>
+ </form>
+ {% else %}
+ <form action="index.php">
+ <input type="submit" class="submit" value="{{ 'General_ContinueToPiwik'|translate }}"/>
</form>
- </div>
- <br/>
- <br/>
-{% elseif error %}
- <br/>
- <br/>
- <div class="error">
- <img src="plugins/Morpheus/images/error_medium.png"/>
- {{ error }}
- </div>
- <br/>
- <br/>
- <div class="warning">
- <img src="plugins/Morpheus/images/warning_medium.png"/>
- {{ 'CoreUpdater_UpdateHasBeenCancelledExplanation'|translate("<br /><br />","<a target='_blank' href='?module=Proxy&action=redirect&url=http://piwik.org/docs/update/'>","</a>")|raw }}
- </div>
- <br/>
- <br/>
-{% endif %}
+ {% endif %}
-<form action="index.php">
- <input type="submit" class="submit" value="{{ 'General_ContinueToPiwik'|translate }}"/>
-</form>
{% endblock %}
diff --git a/plugins/CoreUpdater/templates/runUpdaterAndExit_done_cli.twig b/plugins/CoreUpdater/templates/runUpdaterAndExit_done_cli.twig
deleted file mode 100644
index bee8fadcbf..0000000000
--- a/plugins/CoreUpdater/templates/runUpdaterAndExit_done_cli.twig
+++ /dev/null
@@ -1,51 +0,0 @@
-{% autoescape false %}
-{% set helpMessage %}{{- 'CoreUpdater_HelpMessageContent'|translate('[',']',"\n\n *") }}{% endset %}
-{% if coreError %}
- [X] {{ 'CoreUpdater_CriticalErrorDuringTheUpgradeProcess'|translate }}
-
- {% for message in errorMessages %}
- * {{ message }}
- {% endfor %}
-
- {{ 'CoreUpdater_HelpMessageIntroductionWhenError'|translate }}
-
- * {{ helpMessage }}
-
- {{ 'CoreUpdater_ErrorDIYHelp'|translate }}
- * {{ 'CoreUpdater_ErrorDIYHelp_1'|translate }}
- * {{ 'CoreUpdater_ErrorDIYHelp_2'|translate }}
- * {{ 'CoreUpdater_ErrorDIYHelp_3'|translate }}
- * {{ 'CoreUpdater_ErrorDIYHelp_4'|translate }}
- * {{ 'CoreUpdater_ErrorDIYHelp_5'|translate }}
-
-{% else %}
-{% if warningMessages|length > 0 %}
- [!] {{ 'CoreUpdater_WarningMessages'|translate }}
-
- {% for message in warningMessages -%}
- * {{ message }}
- {%- endfor %}
-{%- endif %}
-{% if errorMessages|length > 0 -%}
-
- [X] {{ 'CoreUpdater_ErrorDuringPluginsUpdates'|translate }}
-
- {% for message in errorMessages %}
- * {{ message }}
- {% endfor %}
-
- {% if deactivatedPlugins|length > 0 -%}
- {% set listOfDeactivatedPlugins %}{{ deactivatedPlugins|implode(', ') }}{% endset %}
-
- [!] {{ 'CoreUpdater_WeAutomaticallyDeactivatedTheFollowingPlugins'|translate(listOfDeactivatedPlugins) }}
- {% endif %}
-{% endif %}
-{% if errorMessages|length > 0 or warningMessages|length > 0 %}
- {{ 'CoreUpdater_HelpMessageIntroductionWhenWarning'|translate }}
-
- * {{ helpMessage }}
-{% else %}
- Done!
-{% endif %}
-{% endif %}
-{% endautoescape %}
diff --git a/plugins/CoreUpdater/templates/runUpdaterAndExit_welcome_cli.twig b/plugins/CoreUpdater/templates/runUpdaterAndExit_welcome_cli.twig
deleted file mode 100644
index 33c21a15e3..0000000000
--- a/plugins/CoreUpdater/templates/runUpdaterAndExit_welcome_cli.twig
+++ /dev/null
@@ -1,53 +0,0 @@
-{% autoescape false %}
-{% set helpMessage %}
-{{- 'CoreUpdater_HelpMessageContent'|translate('[',']','\n\n *') }}
-{% endset %}
-
-*** {{ 'CoreUpdater_UpdateTitle'|translate }} ***
-{% if coreError %}
-
- [X] {{ 'CoreUpdater_CriticalErrorDuringTheUpgradeProcess'|translate }}
-
- {% for message in errorMessages %}
- {{- message }}
- {% endfor %}
-
- {{ 'CoreUpdater_HelpMessageIntroductionWhenError'|translate }}
-
- * {{ helpMessage }}
-
-{% elseif coreToUpdate or pluginNamesToUpdate|length > 0 or dimensionsToUpdate|length > 0 %}
-
- {{ 'CoreUpdater_DatabaseUpgradeRequired'|translate }}
-
- {{ 'CoreUpdater_YourDatabaseIsOutOfDate'|translate }}
-
-{% if coreToUpdate %}
- {{ 'CoreUpdater_PiwikWillBeUpgradedFromVersionXToVersionY'|translate(current_piwik_version, new_piwik_version) }}
-{% endif %}
-
-{%- if pluginNamesToUpdate|length > 0 %}
- {%- set listOfPlugins %}{{ pluginNamesToUpdate|implode(', ') }}{% endset %}
- {{ 'CoreUpdater_TheFollowingPluginsWillBeUpgradedX'|translate( listOfPlugins) }}
-{% endif %}
-
-{%- if dimensionsToUpdate|length > 0 %}
- {%- set listOfDimensions %}{{ dimensionsToUpdate|implode(', ') }}{% endset %}
- {{ 'CoreUpdater_TheFollowingDimensionsWillBeUpgradedX'|translate( listOfDimensions) }}
-{% endif %}
-
-{# dry run #}
-{% if queries is defined and queries is not empty %}
-*** Note: this is a Dry Run ***
-
- {% for query in queries %}{{ query|trim }}
- {% endfor %}
-
-*** End of Dry Run ***
-{% else %}
- {{ 'CoreUpdater_TheUpgradeProcessMayTakeAWhilePleaseBePatient'|translate }}
-{% endif %}
-
-{% endif %}
-{% endautoescape %}
-
diff --git a/plugins/CoreUpdater/tests/Integration/Commands/UpdateTest.php b/plugins/CoreUpdater/tests/Integration/Commands/UpdateTest.php
new file mode 100644
index 0000000000..a3c97d86d8
--- /dev/null
+++ b/plugins/CoreUpdater/tests/Integration/Commands/UpdateTest.php
@@ -0,0 +1,126 @@
+<?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\Plugins\CoreUpdater\tests\Integration\Commands;
+
+use Piwik\Common;
+use Piwik\Config;
+use Piwik\DataAccess\ArchiveTableCreator;
+use Piwik\Date;
+use Piwik\Db;
+use Piwik\DbHelper;
+use Piwik\Option;
+use Piwik\Tests\Framework\TestCase\ConsoleCommandTestCase;
+use Piwik\Updates\Updates_2_10_0_b5;
+use Piwik\Version;
+use Symfony\Component\Console\Helper\QuestionHelper;
+
+require_once PIWIK_INCLUDE_PATH . '/core/Updates/2.10.0-b5.php';
+
+/**
+ * @group CoreUpdater
+ * @group CoreUpdater_Integration
+ */
+class UpdateTest extends ConsoleCommandTestCase
+{
+ const VERSION_TO_UPDATE_FROM = '2.9.0';
+ const EXPECTED_SQL_FROM_2_10 = "UPDATE report SET reports = REPLACE(reports, 'UserSettings_getBrowserVersion', 'DevicesDetection_getBrowserVersions');";
+
+ private $oldScriptName = null;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ Config::getInstance()->setTestEnvironment();
+ Option::set('version_core', self::VERSION_TO_UPDATE_FROM);
+
+ $this->oldScriptName = $_SERVER['SCRIPT_NAME'];
+ $_SERVER['SCRIPT_NAME'] = $_SERVER['SCRIPT_NAME'] . " console"; // update won't execute w/o this, see Common::isRunningConsoleCommand()
+
+ ArchiveTableCreator::clear();
+ DbHelper::getTablesInstalled($forceReload = true); // force reload so internal cache in Mysql.php is refreshed
+ Updates_2_10_0_b5::$archiveBlobTables = null;
+ }
+
+ public function tearDown()
+ {
+ $_SERVER['SCRIPT_NAME'] = $this->oldScriptName;
+
+ parent::tearDown();
+ }
+
+ public function test_UpdateCommand_SuccessfullyExecutesUpdate()
+ {
+ $result = $this->applicationTester->run(array(
+ 'command' => 'core:update',
+ '--yes' => true
+ ));
+
+ $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage());
+
+ $this->assertDryRunExecuted($this->applicationTester->getDisplay());
+
+ // make sure update went through
+ $this->assertEquals(Version::VERSION, Option::get('version_core'));
+ }
+
+ public function test_UpdateCommand_DoesntExecuteSql_WhenUserSaysNo()
+ {
+ /** @var QuestionHelper $dialog */
+ $dialog = $this->application->getHelperSet()->get('question');
+ $dialog->setInputStream($this->getInputStream("N\n"));
+
+ $result = $this->applicationTester->run(array(
+ 'command' => 'core:update'
+ ));
+
+ $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage());
+
+ $this->assertDryRunExecuted($this->applicationTester->getDisplay());
+
+ // make sure update did not go through
+ $this->assertEquals(self::VERSION_TO_UPDATE_FROM, Option::get('version_core'));
+ }
+
+ public function test_UpdateCommand_DoesNotExecuteUpdate_IfPiwikUpToDate()
+ {
+ Option::set('version_core', Version::VERSION);
+
+ $result = $this->applicationTester->run(array(
+ 'command' => 'core:update',
+ '--yes' => true
+ ));
+
+ $this->assertEquals(0, $result, $this->getCommandDisplayOutputErrorMessage());
+
+ // check no update occurred
+ $this->assertContains("Everything is already up to date.", $this->applicationTester->getDisplay());
+ $this->assertEquals(Version::VERSION, Option::get('version_core'));
+ }
+
+ public function test_UpdateCommand_ReturnsCorrectExitCode_WhenErrorOccurs()
+ {
+ // create a blob table, then drop it manually so update 2.10.0-b10 will fail
+ $tableName = ArchiveTableCreator::getBlobTable(Date::factory('2015-01-01'));
+ Db::exec("DROP TABLE $tableName");
+
+ $result = $this->applicationTester->run(array(
+ 'command' => 'core:update',
+ '--yes' => true
+ ));
+
+ $this->assertEquals(1, $result, $this->getCommandDisplayOutputErrorMessage());
+ $this->assertContains("Piwik could not be updated! See above for more information.", $this->applicationTester->getDisplay());
+ }
+
+ private function assertDryRunExecuted($output)
+ {
+ $this->assertContains("Note: this is a Dry Run", $output);
+ $this->assertContains(self::EXPECTED_SQL_FROM_2_10, $output);
+ }
+} \ No newline at end of file
diff --git a/plugins/CustomVariables/API.php b/plugins/CustomVariables/API.php
index 1a6fc8c794..36d4e1ff9a 100644
--- a/plugins/CustomVariables/API.php
+++ b/plugins/CustomVariables/API.php
@@ -34,7 +34,6 @@ class API extends \Piwik\Plugin\API
protected function getDataTable($idSite, $period, $date, $segment, $expanded, $flat, $idSubtable)
{
$dataTable = Archive::createDataTableFromArchive(Archiver::CUSTOM_VARIABLE_RECORD_NAME, $idSite, $period, $date, $segment, $expanded, $flat, $idSubtable);
- $dataTable->filter('Sort', array(Metrics::INDEX_NB_ACTIONS, 'desc', $naturalSort = false, $expanded));
$dataTable->queueFilter('ColumnDelete', 'nb_uniq_visitors');
if ($flat) {
diff --git a/plugins/CustomVariables/Archiver.php b/plugins/CustomVariables/Archiver.php
index 704038b0a1..b613f65d02 100644
--- a/plugins/CustomVariables/Archiver.php
+++ b/plugins/CustomVariables/Archiver.php
@@ -50,9 +50,16 @@ class Archiver extends \Piwik\Plugin\Archiver
public function aggregateMultipleReports()
{
+ $columnsAggregationOperation = null;
+
$this->getProcessor()->aggregateDataTableRecords(
- self::CUSTOM_VARIABLE_RECORD_NAME, $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable,
- $columnToSort = Metrics::INDEX_NB_VISITS);
+ self::CUSTOM_VARIABLE_RECORD_NAME,
+ $this->maximumRowsInDataTableLevelZero,
+ $this->maximumRowsInSubDataTable,
+ $columnToSort = Metrics::INDEX_NB_VISITS,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
}
public function aggregateDayReport()
diff --git a/plugins/CustomVariables/Reports/Base.php b/plugins/CustomVariables/Reports/Base.php
index 4a19c0a763..be48bf4d44 100644
--- a/plugins/CustomVariables/Reports/Base.php
+++ b/plugins/CustomVariables/Reports/Base.php
@@ -10,6 +10,8 @@ namespace Piwik\Plugins\CustomVariables\Reports;
abstract class Base extends \Piwik\Plugin\Report
{
+ protected $defaultSortColumn = 'nb_actions';
+
protected function init()
{
$this->category = 'General_Visitors';
diff --git a/plugins/DBStats/API.php b/plugins/DBStats/API.php
index 30def6cf79..47417dbc35 100644
--- a/plugins/DBStats/API.php
+++ b/plugins/DBStats/API.php
@@ -13,12 +13,6 @@ use Piwik\DataTable;
use Piwik\Piwik;
/**
- *
- * @see plugins/DBStats/MySQLMetadataProvider.php
- */
-require_once PIWIK_INCLUDE_PATH . '/plugins/DBStats/MySQLMetadataProvider.php';
-
-/**
* DBStats API is used to request the overall status of the Mysql tables in use by Piwik.
* @hideExceptForSuperUser
* @method static \Piwik\Plugins\DBStats\API getInstance()
@@ -30,12 +24,9 @@ class API extends \Piwik\Plugin\API
*/
private $metadataProvider;
- /**
- * Constructor.
- */
- protected function __construct()
+ public function __construct(MySQLMetadataProvider $metadataProvider)
{
- $this->metadataProvider = new MySQLMetadataProvider();
+ $this->metadataProvider = $metadataProvider;
}
/**
diff --git a/plugins/Dashboard/API.php b/plugins/Dashboard/API.php
index d8994c229e..f4637e862a 100644
--- a/plugins/Dashboard/API.php
+++ b/plugins/Dashboard/API.php
@@ -19,9 +19,9 @@ class API extends \Piwik\Plugin\API
{
private $dashboard = null;
- protected function __construct()
+ public function __construct(Dashboard $dashboard)
{
- $this->dashboard = new Dashboard();
+ $this->dashboard = $dashboard;
}
/**
diff --git a/plugins/DevicePlugins/API.php b/plugins/DevicePlugins/API.php
index c49839e8ae..b4b33b8db3 100644
--- a/plugins/DevicePlugins/API.php
+++ b/plugins/DevicePlugins/API.php
@@ -32,7 +32,6 @@ class API extends \Piwik\Plugin\API
Piwik::checkUserHasViewAccess($idSite);
$archive = Archive::build($idSite, $period, $date, $segment);
$dataTable = $archive->getDataTable($name);
- $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS));
$dataTable->queueFilter('ReplaceColumnNames');
$dataTable->queueFilter('ReplaceSummaryRowLabel');
return $dataTable;
@@ -42,18 +41,18 @@ class API extends \Piwik\Plugin\API
{
// fetch all archive data required
$dataTable = $this->getDataTable(Archiver::PLUGIN_RECORD_NAME, $idSite, $period, $date, $segment);
- $browserTypes = $this->getDataTable(DDArchiver::BROWSER_ENGINE_RECORD_NAME, $idSite, $period, $date, $segment);
+ $browserVersions = $this->getDataTable(DDArchiver::BROWSER_VERSION_RECORD_NAME, $idSite, $period, $date, $segment);
$archive = Archive::build($idSite, $period, $date, $segment);
$visitsSums = $archive->getDataTableFromNumeric('nb_visits');
// check whether given tables are arrays
if ($dataTable instanceof DataTable\Map) {
$dataTableMap = $dataTable->getDataTables();
- $browserTypesArray = $browserTypes->getDataTables();
+ $browserVersionsArray = $browserVersions->getDataTables();
$visitSumsArray = $visitsSums->getDataTables();
} else {
$dataTableMap = array($dataTable);
- $browserTypesArray = array($browserTypes);
+ $browserVersionsArray = array($browserVersions);
$visitSumsArray = array($visitsSums);
}
@@ -62,9 +61,18 @@ class API extends \Piwik\Plugin\API
// Calculate percentage, but ignore IE users because plugin detection doesn't work on IE
$ieVisits = 0;
- $ieStats = $browserTypesArray[$key]->getRowFromLabel('Trident');
- if ($ieStats !== false) {
- $ieVisits = $ieStats->getColumn(Metrics::INDEX_NB_VISITS);
+ $browserVersionsToExclude = array(
+ 'IE;10.0',
+ 'IE;9.0',
+ 'IE;8.0',
+ 'IE;7.0',
+ 'IE;6.0',
+ );
+ foreach ($browserVersionsToExclude as $browserVersionToExclude) {
+ $ieStats = $browserVersionsArray[$key]->getRowFromLabel($browserVersionToExclude);
+ if ($ieStats !== false) {
+ $ieVisits += $ieStats->getColumn(Metrics::INDEX_NB_VISITS);
+ }
}
// get according visitsSum
@@ -84,6 +92,7 @@ class API extends \Piwik\Plugin\API
$dataTable->queueFilter('ColumnCallbackAddMetadata', array('label', 'logo', __NAMESPACE__ . '\getPluginsLogo'));
$dataTable->queueFilter('ColumnCallbackReplace', array('label', 'ucfirst'));
+ $dataTable->queueFilter('RangeCheck', array('nb_visits_percentage', 0, 1));
return $dataTable;
}
diff --git a/plugins/DevicePlugins/Archiver.php b/plugins/DevicePlugins/Archiver.php
index 1b4b92213e..acec8c0cde 100644
--- a/plugins/DevicePlugins/Archiver.php
+++ b/plugins/DevicePlugins/Archiver.php
@@ -40,7 +40,16 @@ class Archiver extends \Piwik\Plugin\Archiver
$dataTableRecords = array(
self::PLUGIN_RECORD_NAME,
);
- $this->getProcessor()->aggregateDataTableRecords($dataTableRecords, $this->maximumRows);
+ $columnsAggregationOperation = null;
+ $this->getProcessor()->aggregateDataTableRecords(
+ $dataTableRecords,
+ $this->maximumRows,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array()
+ );
}
protected function aggregateByPlugin()
diff --git a/plugins/DevicesDetection/API.php b/plugins/DevicesDetection/API.php
index 36fcd8fb95..b6e484f924 100644
--- a/plugins/DevicesDetection/API.php
+++ b/plugins/DevicesDetection/API.php
@@ -34,7 +34,6 @@ class API extends \Piwik\Plugin\API
Piwik::checkUserHasViewAccess($idSite);
$archive = Archive::build($idSite, $period, $date, $segment);
$dataTable = $archive->getDataTable($name);
- $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS));
$dataTable->queueFilter('ReplaceColumnNames');
$dataTable->queueFilter('ReplaceSummaryRowLabel');
return $dataTable;
@@ -217,7 +216,11 @@ class API extends \Piwik\Plugin\API
*/
public function getBrowserFamilies($idSite, $period, $date, $segment = false)
{
- return $this->getBrowsers($idSite, $period, $date, $segment);
+ $table = $this->getBrowsers($idSite, $period, $date, $segment);
+ // this one will not be sorted automatically by nb_visits since there is no Report class for it.
+ $table->filter('Sort', array(Metrics::INDEX_NB_VISITS, 'desc'));
+
+ return $table;
}
/**
diff --git a/plugins/DevicesDetection/Archiver.php b/plugins/DevicesDetection/Archiver.php
index 3ecb310028..21e4b4cf01 100644
--- a/plugins/DevicesDetection/Archiver.php
+++ b/plugins/DevicesDetection/Archiver.php
@@ -56,9 +56,18 @@ class Archiver extends \Piwik\Plugin\Archiver
self::BROWSER_ENGINE_RECORD_NAME,
self::BROWSER_VERSION_RECORD_NAME
);
+
+ $columnsAggregationOperation = null;
+
foreach ($dataTablesToSum as $dt) {
$this->getProcessor()->aggregateDataTableRecords(
- $dt, $this->maximumRows, $this->maximumRows, $columnToSort = "nb_visits");
+ $dt,
+ $this->maximumRows,
+ $this->maximumRows,
+ $columnToSort = 'nb_visits',
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
}
}
diff --git a/plugins/DevicesDetection/Reports/Base.php b/plugins/DevicesDetection/Reports/Base.php
index 7ca883af3f..15eac96a4a 100644
--- a/plugins/DevicesDetection/Reports/Base.php
+++ b/plugins/DevicesDetection/Reports/Base.php
@@ -8,6 +8,8 @@
*/
namespace Piwik\Plugins\DevicesDetection\Reports;
+use Piwik\Metrics;
+
abstract class Base extends \Piwik\Plugin\Report
{
protected function init()
diff --git a/plugins/Ecommerce/Reports/BaseItem.php b/plugins/Ecommerce/Reports/BaseItem.php
index 693f0074de..5caf770a6b 100644
--- a/plugins/Ecommerce/Reports/BaseItem.php
+++ b/plugins/Ecommerce/Reports/BaseItem.php
@@ -21,6 +21,8 @@ use Piwik\Plugins\Goals\Columns\Metrics\ProductConversionRate;
abstract class BaseItem extends Base
{
+ protected $defaultSortColumn = 'revenue';
+
protected function init()
{
parent::init();
diff --git a/plugins/Events/Archiver.php b/plugins/Events/Archiver.php
index 5a5160bdb2..c928ec318e 100644
--- a/plugins/Events/Archiver.php
+++ b/plugins/Events/Archiver.php
@@ -98,7 +98,16 @@ class Archiver extends \Piwik\Plugin\Archiver
public function aggregateMultipleReports()
{
$dataTableToSum = $this->getRecordNames();
- $this->getProcessor()->aggregateDataTableRecords($dataTableToSum, $this->maximumRowsInDataTable, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation);
+ $columnsAggregationOperation = null;
+
+ $this->getProcessor()->aggregateDataTableRecords(
+ $dataTableToSum,
+ $this->maximumRowsInDataTable,
+ $this->maximumRowsInSubDataTable,
+ $this->columnToSortByBeforeTruncation,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
}
protected function getRecordNames()
diff --git a/plugins/Goals/API.php b/plugins/Goals/API.php
index 46f521375e..e4ecb322e5 100644
--- a/plugins/Goals/API.php
+++ b/plugins/Goals/API.php
@@ -230,8 +230,6 @@ class API extends \Piwik\Plugin\API
$archive = Archive::build($idSite, $period, $date, $segment);
$dataTable = $archive->getDataTable($recordNameFinal);
- $dataTable->filter('Sort', array(Metrics::INDEX_ECOMMERCE_ITEM_REVENUE));
-
$this->enrichItemsTableWithViewMetrics($dataTable, $recordName, $idSite, $period, $date, $segment);
// First rename the avg_price_viewed column
@@ -241,7 +239,6 @@ class API extends \Piwik\Plugin\API
$dataTable->queueFilter('ReplaceColumnNames');
$dataTable->queueFilter('ReplaceSummaryRowLabel');
- $ordersColumn = 'orders';
if ($abandonedCarts) {
$ordersColumn = 'abandoned_carts';
$dataTable->renameColumn(Metrics::INDEX_ECOMMERCE_ORDERS, $ordersColumn);
@@ -483,7 +480,7 @@ class API extends \Piwik\Plugin\API
$dataTable = $this->getGoalSpecificDataTable(
Archiver::DAYS_UNTIL_CONV_RECORD_NAME, $idSite, $period, $date, $segment, $idGoal);
- $dataTable->queueFilter('Sort', array('label', 'asc', true));
+ $dataTable->queueFilter('Sort', array('label', 'asc', true, false));
$dataTable->queueFilter(
'BeautifyRangeLabels', array(Piwik::translate('General_OneDay'), Piwik::translate('General_NDays')));
@@ -507,7 +504,7 @@ class API extends \Piwik\Plugin\API
$dataTable = $this->getGoalSpecificDataTable(
Archiver::VISITS_UNTIL_RECORD_NAME, $idSite, $period, $date, $segment, $idGoal);
- $dataTable->queueFilter('Sort', array('label', 'asc', true));
+ $dataTable->queueFilter('Sort', array('label', 'asc', true, false));
$dataTable->queueFilter(
'BeautifyRangeLabels', array(Piwik::translate('General_OneVisit'), Piwik::translate('General_NVisits')));
diff --git a/plugins/Goals/Archiver.php b/plugins/Goals/Archiver.php
index 379752a6e2..b96d9b94b7 100644
--- a/plugins/Goals/Archiver.php
+++ b/plugins/Goals/Archiver.php
@@ -361,7 +361,15 @@ class Archiver extends \Piwik\Plugin\Archiver
foreach ($this->dimensionRecord as $recordName) {
$dataTableToSum[] = self::getItemRecordNameAbandonedCart($recordName);
}
- $this->getProcessor()->aggregateDataTableRecords($dataTableToSum);
+ $columnsAggregationOperation = null;
+
+ $this->getProcessor()->aggregateDataTableRecords($dataTableToSum,
+ $maximumRowsInDataTableLevelZero = null,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
/*
* Archive General Goal metrics
@@ -383,16 +391,31 @@ class Archiver extends \Piwik\Plugin\Archiver
}
$this->getProcessor()->aggregateNumericMetrics($fieldsToSum);
+ $columnsAggregationOperation = null;
+
foreach ($goalIdsToSum as $goalId) {
// sum up the visits to conversion data table & the days to conversion data table
- $this->getProcessor()->aggregateDataTableRecords(array(
- self::getRecordName(self::VISITS_UNTIL_RECORD_NAME, $goalId),
- self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME, $goalId)));
+ $this->getProcessor()->aggregateDataTableRecords(
+ array(self::getRecordName(self::VISITS_UNTIL_RECORD_NAME, $goalId),
+ self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME, $goalId)),
+ $maximumRowsInDataTableLevelZero = null,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
}
+ $columnsAggregationOperation = null;
// sum up goal overview reports
- $this->getProcessor()->aggregateDataTableRecords(array(
- self::getRecordName(self::VISITS_UNTIL_RECORD_NAME),
- self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME)));
+ $this->getProcessor()->aggregateDataTableRecords(
+ array(self::getRecordName(self::VISITS_UNTIL_RECORD_NAME),
+ self::getRecordName(self::DAYS_UNTIL_CONV_RECORD_NAME)),
+ $maximumRowsInDataTableLevelZero = null,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
}
}
diff --git a/plugins/Goals/Reports/GetDaysToConversion.php b/plugins/Goals/Reports/GetDaysToConversion.php
index d41f2e8f06..a6a5060782 100644
--- a/plugins/Goals/Reports/GetDaysToConversion.php
+++ b/plugins/Goals/Reports/GetDaysToConversion.php
@@ -15,6 +15,8 @@ use Piwik\Plugins\Goals\Archiver;
class GetDaysToConversion extends Base
{
+ protected $defaultSortColumn = '';
+
protected function init()
{
parent::init();
diff --git a/plugins/Goals/Reports/GetVisitsUntilConversion.php b/plugins/Goals/Reports/GetVisitsUntilConversion.php
index ddf1da1fd9..649ffac17b 100644
--- a/plugins/Goals/Reports/GetVisitsUntilConversion.php
+++ b/plugins/Goals/Reports/GetVisitsUntilConversion.php
@@ -15,6 +15,8 @@ use Piwik\Plugins\Goals\Archiver;
class GetVisitsUntilConversion extends Base
{
+ protected $defaultSortColumn = '';
+
protected function init()
{
parent::init();
diff --git a/plugins/Insights/API.php b/plugins/Insights/API.php
index 20cb649649..4b3228ffc2 100644
--- a/plugins/Insights/API.php
+++ b/plugins/Insights/API.php
@@ -39,11 +39,9 @@ class API extends \Piwik\Plugin\API
*/
private $model;
- protected function __construct()
+ public function __construct(Model $model)
{
- parent::__construct();
-
- $this->model = new Model();
+ $this->model = $model;
}
private function getOverviewReports()
diff --git a/plugins/Installation/Controller.php b/plugins/Installation/Controller.php
index 7641624cb3..9976f4ad58 100644
--- a/plugins/Installation/Controller.php
+++ b/plugins/Installation/Controller.php
@@ -720,12 +720,12 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
return Access::doAsSuperUser(function () {
$updater = new Updater();
- $componentsWithUpdateFile = CoreUpdater::getComponentUpdates($updater);
+ $componentsWithUpdateFile = $updater->getComponentUpdates();
if (empty($componentsWithUpdateFile)) {
return false;
}
- $result = CoreUpdater::updateComponents($updater, $componentsWithUpdateFile);
+ $result = $updater->updateComponents($componentsWithUpdateFile);
return $result;
});
}
diff --git a/plugins/Live/Reports/GetLastVisits.php b/plugins/Live/Reports/GetLastVisits.php
new file mode 100644
index 0000000000..58d027ff72
--- /dev/null
+++ b/plugins/Live/Reports/GetLastVisits.php
@@ -0,0 +1,22 @@
+<?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\Plugins\Live\Reports;
+
+
+class GetLastVisits extends Base
+{
+ // this class only exists to disable the default sort column
+ protected $defaultSortColumn = '';
+
+ public function buildReportMetadata()
+ {
+ // do not add this report as metadata
+ }
+
+}
diff --git a/plugins/Live/Reports/GetLastVisitsDetails.php b/plugins/Live/Reports/GetLastVisitsDetails.php
index 7d5318b588..c54c97e924 100644
--- a/plugins/Live/Reports/GetLastVisitsDetails.php
+++ b/plugins/Live/Reports/GetLastVisitsDetails.php
@@ -15,6 +15,8 @@ use Piwik\WidgetsList;
class GetLastVisitsDetails extends Base
{
+ protected $defaultSortColumn = '';
+
protected function init()
{
parent::init();
diff --git a/plugins/Provider/API.php b/plugins/Provider/API.php
index bd4b5c6676..e11eaf54cc 100644
--- a/plugins/Provider/API.php
+++ b/plugins/Provider/API.php
@@ -29,7 +29,6 @@ class API extends \Piwik\Plugin\API
Piwik::checkUserHasViewAccess($idSite);
$archive = Archive::build($idSite, $period, $date, $segment);
$dataTable = $archive->getDataTable(Archiver::PROVIDER_RECORD_NAME);
- $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS));
$dataTable->filter('ColumnCallbackAddMetadata', array('label', 'url', __NAMESPACE__ . '\getHostnameUrl'));
$dataTable->filter('GroupBy', array('label', __NAMESPACE__ . '\getPrettyProviderName'));
$dataTable->queueFilter('ReplaceColumnNames');
diff --git a/plugins/Provider/Archiver.php b/plugins/Provider/Archiver.php
index a8a22c9527..ed140b05e7 100644
--- a/plugins/Provider/Archiver.php
+++ b/plugins/Provider/Archiver.php
@@ -24,6 +24,16 @@ class Archiver extends \Piwik\Plugin\Archiver
public function aggregateMultipleReports()
{
- $this->getProcessor()->aggregateDataTableRecords(array(self::PROVIDER_RECORD_NAME), $this->maximumRows);
+ $columnsAggregationOperation = null;
+
+ $this->getProcessor()->aggregateDataTableRecords(
+ array(self::PROVIDER_RECORD_NAME),
+ $this->maximumRows,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array()
+ );
}
}
diff --git a/plugins/Referrers/API.php b/plugins/Referrers/API.php
index 2cacbb2833..c615e69ed5 100644
--- a/plugins/Referrers/API.php
+++ b/plugins/Referrers/API.php
@@ -130,7 +130,6 @@ class API extends \Piwik\Plugin\API
}
$dataTable = $dataTable->mergeSubtables($labelColumn = 'referer_type', $useMetadataColumn = true);
- $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS, 'desc'));
$dataTable->queueFilter('ReplaceColumnNames');
$dataTable->queueFilter('ReplaceSummaryRowLabel');
diff --git a/plugins/Referrers/Archiver.php b/plugins/Referrers/Archiver.php
index 3093449a6c..ea4939f138 100644
--- a/plugins/Referrers/Archiver.php
+++ b/plugins/Referrers/Archiver.php
@@ -217,7 +217,16 @@ class Archiver extends \Piwik\Plugin\Archiver
public function aggregateMultipleReports()
{
$dataTableToSum = $this->getRecordNames();
- $nameToCount = $this->getProcessor()->aggregateDataTableRecords($dataTableToSum, $this->maximumRowsInDataTableLevelZero, $this->maximumRowsInSubDataTable, $this->columnToSortByBeforeTruncation);
+ $columnsAggregationOperation = null;
+ $nameToCount = $this->getProcessor()->aggregateDataTableRecords(
+ $dataTableToSum,
+ $this->maximumRowsInDataTableLevelZero,
+ $this->maximumRowsInSubDataTable,
+ $this->columnToSortByBeforeTruncation,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array(self::WEBSITES_RECORD_NAME)
+ );
$mappingFromArchiveName = array(
self::METRIC_DISTINCT_SEARCH_ENGINE_RECORD_NAME =>
diff --git a/plugins/Referrers/DataTable/Filter/UrlsForSocial.php b/plugins/Referrers/DataTable/Filter/UrlsForSocial.php
index ad86093060..0d61a8887f 100644
--- a/plugins/Referrers/DataTable/Filter/UrlsForSocial.php
+++ b/plugins/Referrers/DataTable/Filter/UrlsForSocial.php
@@ -43,7 +43,6 @@ class UrlsForSocial extends BaseFilter
// prettify the DataTable
$table->filter('ColumnCallbackReplace', array('label', 'Piwik\Plugins\Referrers\removeUrlProtocol'));
- $table->filter('Sort', array(Metrics::INDEX_NB_VISITS, 'desc', $naturalSort = false, $this->sortRecursive));
$table->queueFilter('ReplaceColumnNames');
}
} \ No newline at end of file
diff --git a/plugins/Resolution/API.php b/plugins/Resolution/API.php
index 8a39d78e0b..767cb0ff70 100644
--- a/plugins/Resolution/API.php
+++ b/plugins/Resolution/API.php
@@ -28,7 +28,6 @@ class API extends \Piwik\Plugin\API
Piwik::checkUserHasViewAccess($idSite);
$archive = Archive::build($idSite, $period, $date, $segment);
$dataTable = $archive->getDataTable($name);
- $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS));
$dataTable->queueFilter('ReplaceColumnNames');
$dataTable->queueFilter('ReplaceSummaryRowLabel');
return $dataTable;
diff --git a/plugins/Resolution/Archiver.php b/plugins/Resolution/Archiver.php
index f44d744c53..06937c24f3 100644
--- a/plugins/Resolution/Archiver.php
+++ b/plugins/Resolution/Archiver.php
@@ -39,7 +39,15 @@ class Archiver extends \Piwik\Plugin\Archiver
self::RESOLUTION_RECORD_NAME,
self::CONFIGURATION_RECORD_NAME,
);
- $this->getProcessor()->aggregateDataTableRecords($dataTableRecords, $this->maximumRows);
+ $columnsAggregationOperation = null;
+ $this->getProcessor()->aggregateDataTableRecords(
+ $dataTableRecords,
+ $this->maximumRows,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
}
protected function aggregateByConfiguration()
diff --git a/plugins/UserCountry/API.php b/plugins/UserCountry/API.php
index 38408b39e6..bd10d6f0ad 100644
--- a/plugins/UserCountry/API.php
+++ b/plugins/UserCountry/API.php
@@ -203,7 +203,6 @@ class API extends \Piwik\Plugin\API
Piwik::checkUserHasViewAccess($idSite);
$archive = Archive::build($idSite, $period, $date, $segment);
$dataTable = $archive->getDataTable($name);
- $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS));
$dataTable->queueFilter('ReplaceColumnNames');
return $dataTable;
}
diff --git a/plugins/UserCountry/Archiver.php b/plugins/UserCountry/Archiver.php
index d2203e2479..03a789b14c 100644
--- a/plugins/UserCountry/Archiver.php
+++ b/plugins/UserCountry/Archiver.php
@@ -61,8 +61,17 @@ class Archiver extends \Piwik\Plugin\Archiver
self::REGION_RECORD_NAME,
self::CITY_RECORD_NAME,
);
-
- $nameToCount = $this->getProcessor()->aggregateDataTableRecords($dataTableToSum);
+ $columnsAggregationOperation = null;
+
+ $nameToCount = $this->getProcessor()->aggregateDataTableRecords(
+ $dataTableToSum,
+ $maximumRowsInDataTableLevelZero = null,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array()
+ );
$this->getProcessor()->insertNumericRecord(self::DISTINCT_COUNTRIES_METRIC,
$nameToCount[self::COUNTRY_RECORD_NAME]['level0']);
}
diff --git a/plugins/UserCountry/Columns/Base.php b/plugins/UserCountry/Columns/Base.php
index 3e4678a0e9..d0a3b58118 100644
--- a/plugins/UserCountry/Columns/Base.php
+++ b/plugins/UserCountry/Columns/Base.php
@@ -11,17 +11,19 @@ namespace Piwik\Plugins\UserCountry\Columns;
use Piwik\Common;
use Piwik\Network\IPUtils;
use Piwik\Plugin\Dimension\VisitDimension;
+use Piwik\Plugins\UserCountry\VisitorGeolocator;
use Piwik\Plugins\UserCountry\LocationProvider\GeoIp;
use Piwik\Plugins\UserCountry\LocationProvider;
use Piwik\Plugins\PrivacyManager\Config as PrivacyManagerConfig;
-use Piwik\Plugins\UserCountry\LocationProvider\DefaultProvider;
use Piwik\Tracker\Visitor;
-use Piwik\Tracker\Visit;
use Piwik\Tracker\Request;
abstract class Base extends VisitDimension
{
- private static $cachedLocations = array();
+ /**
+ * @var VisitorGeolocator
+ */
+ private $visitorGeolocator;
protected function getUrlOverrideValueIfAllowed($urlParamToOverride, Request $request)
{
@@ -44,14 +46,23 @@ abstract class Base extends VisitDimension
protected function getLocationDetail($userInfo, $locationKey)
{
- $location = $this->getCachedLocation($userInfo);
+ $useLocationCache = empty($GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING']);
+ $location = $this->getVisitorGeolocator()->getLocation($userInfo, $useLocationCache);
+
+ if (!isset($location[$locationKey])) {
+ return false;
+ }
- if (!empty($location[$locationKey])) {
+ return $location[$locationKey];
+ }
- return $location[$locationKey];
+ protected function getVisitorGeolocator()
+ {
+ if ($this->visitorGeolocator === null) {
+ $this->visitorGeolocator = new VisitorGeolocator();
}
- return false;
+ return $this->visitorGeolocator;
}
protected function getUserInfo(Request $request, Visitor $visitor)
@@ -64,43 +75,6 @@ abstract class Base extends VisitDimension
return $userInfo;
}
- protected function getCachedLocation($userInfo)
- {
- require_once PIWIK_INCLUDE_PATH . "/plugins/UserCountry/LocationProvider.php";
-
- $key = md5(implode(',', $userInfo));
-
- if (array_key_exists($key, self::$cachedLocations) && empty($GLOBALS['PIWIK_TRACKER_LOCAL_TRACKING'])) {
- return self::$cachedLocations[$key];
- }
-
- $provider = $this->getProvider();
- $location = $this->getLocation($provider, $userInfo);
-
- if (empty($location)) {
- $providerId = $provider->getId();
- Common::printDebug("GEO: couldn't find a location with Geo Module '$providerId'");
-
- if (!$this->isDefaultProvider($provider)) {
- Common::printDebug("Using default provider as fallback...");
- $provider = $this->getDefaultProvider();
- $location = $this->getLocation($provider, $userInfo);
- }
- }
-
- if (empty($location)) {
- $location = array();
- }
-
- if (empty($location['country_code'])) { // sanity check
- $location['country_code'] = Visit::UNKNOWN_CODE;
- }
-
- self::$cachedLocations[$key] = $location;
-
- return $location;
- }
-
private function getIpAddress($anonymizedIp, \Piwik\Tracker\Request $request)
{
$privacyConfig = new PrivacyManagerConfig();
@@ -115,51 +89,4 @@ abstract class Base extends VisitDimension
return $ipAddress;
}
-
- /**
- * @param \Piwik\Plugins\UserCountry\LocationProvider $provider
- * @param array $userInfo
- * @return array|null
- */
- private function getLocation($provider, $userInfo)
- {
- $location = $provider->getLocation($userInfo);
- $providerId = $provider->getId();
- $ipAddress = $userInfo['ip'];
-
- if ($location === false) {
- return false;
- }
-
- Common::printDebug("GEO: Found IP $ipAddress location (provider '" . $providerId . "'): " . var_export($location, true));
-
- return $location;
- }
-
- private function getDefaultProvider()
- {
- $id = DefaultProvider::ID;
- $provider = LocationProvider::getProviderById($id);
-
- return $provider;
- }
-
- private function isDefaultProvider($provider)
- {
- return !empty($provider) && DefaultProvider::ID == $provider->getId();
- }
-
- private function getProvider()
- {
- $id = Common::getCurrentLocationProviderId();
- $provider = LocationProvider::getProviderById($id);
-
- if ($provider === false) {
- $provider = $this->getDefaultProvider();
- Common::printDebug("GEO: no current location provider sent, falling back to default '$id' one.");
- }
-
- return $provider;
- }
-
-} \ No newline at end of file
+}
diff --git a/plugins/UserCountry/Columns/Country.php b/plugins/UserCountry/Columns/Country.php
index 458e92b068..841c88f16b 100644
--- a/plugins/UserCountry/Columns/Country.php
+++ b/plugins/UserCountry/Columns/Country.php
@@ -67,7 +67,6 @@ class Country extends Base
$country = $this->getLocationDetail($userInfo, LocationProvider::COUNTRY_CODE_KEY);
if (!empty($country) && $country != Visit::UNKNOWN_CODE) {
-
return strtolower($country);
}
diff --git a/plugins/UserCountry/Commands/AttributeHistoricalDataWithLocations.php b/plugins/UserCountry/Commands/AttributeHistoricalDataWithLocations.php
new file mode 100644
index 0000000000..11fba6c215
--- /dev/null
+++ b/plugins/UserCountry/Commands/AttributeHistoricalDataWithLocations.php
@@ -0,0 +1,218 @@
+<?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\Plugins\UserCountry\Commands;
+
+use Piwik\Plugin\ConsoleCommand;
+use Piwik\Plugins\UserCountry\VisitorGeolocator;
+use Piwik\Plugins\UserCountry\LocationProvider;
+use Piwik\DataAccess\RawLogDao;
+use Piwik\Timer;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class AttributeHistoricalDataWithLocations extends ConsoleCommand
+{
+ const DATES_RANGE_ARGUMENT = 'dates-range';
+ const PERCENT_STEP_ARGUMENT = 'percent-step';
+ const PERCENT_STEP_ARGUMENT_DEFAULT = 5;
+ const PROVIDER_ARGUMENT = 'provider';
+ const SEGMENT_LIMIT_OPTION = 'segment-limit';
+ const SEGMENT_LIMIT_OPTION_DEFAULT = 1000;
+ const FORCE_OPTION = 'force';
+
+ /**
+ * @var RawLogDao
+ */
+ protected $dao;
+
+ /**
+ * @var VisitorGeolocator
+ */
+ protected $visitorGeolocator;
+
+ /**
+ * @var int
+ */
+ private $processed = 0;
+
+ /**
+ * @var int
+ */
+ private $amountOfVisits;
+
+ /**
+ * @var Timer
+ */
+ private $timer;
+
+ /**
+ * @var int
+ */
+ private $percentStep;
+
+ /**
+ * @var int
+ */
+ private $processedPercent = 0;
+
+ public function __construct(RawLogDao $dao = null)
+ {
+ parent::__construct();
+
+ $this->dao = $dao ?: new RawLogDao();
+ }
+
+ protected function configure()
+ {
+ $this->setName('usercountry:attribute');
+
+ $this->addArgument(self::DATES_RANGE_ARGUMENT, InputArgument::REQUIRED, 'Attribute visits in this date range. Eg, 2012-01-01,2013-01-01');
+ $this->addOption(self::PERCENT_STEP_ARGUMENT, null, InputArgument::OPTIONAL,
+ 'How often to display the command progress. A status update will be printed after N percent of visits are processed, '
+ . 'where N is the value of this option.', self::PERCENT_STEP_ARGUMENT_DEFAULT);
+ $this->addOption(self::PROVIDER_ARGUMENT, null, InputOption::VALUE_REQUIRED, 'Provider id which should be used to attribute visits. If empty then'
+ . ' Piwik will use the currently configured provider. If no provider is configured, the default provider is used.');
+ $this->addOption(self::SEGMENT_LIMIT_OPTION, null, InputOption::VALUE_OPTIONAL, 'Number of visits to process at a time.', self::SEGMENT_LIMIT_OPTION_DEFAULT);
+ $this->addOption(self::FORCE_OPTION, null, InputOption::VALUE_NONE, "Force geolocation, even if the requested provider does not appear to work. It is not "
+ . "recommended to use this option.");
+ }
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return void|int
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ list($from, $to) = $this->getDateRangeToAttribute($input);
+
+ $this->visitorGeolocator = $this->createGeolocator($output, $input);
+
+ $this->percentStep = $this->getPercentStep($input);
+ $this->amountOfVisits = $this->dao->countVisitsWithDatesLimit($from, $to);
+
+ $output->writeln(
+ sprintf('Re-attribution for date range: %s to %s. %d visits to process with provider "%s".',
+ $from, $to, $this->amountOfVisits, $this->visitorGeolocator->getProvider()->getId())
+ );
+
+ $this->timer = new Timer();
+
+ $this->processSpecifiedLogsInChunks($output, $from, $to, $input->getOption(self::SEGMENT_LIMIT_OPTION));
+
+ $output->writeln("Completed. <comment>" . $this->timer->__toString() . "</comment>");
+
+ return 0;
+ }
+
+ protected function processSpecifiedLogsInChunks(OutputInterface $output, $from, $to, $segmentLimit)
+ {
+ $visitFieldsToSelect = array_merge(array('idvisit', 'location_ip'), array_keys(VisitorGeolocator::$logVisitFieldsToUpdate));
+
+ $lastId = 0;
+ do {
+ $logs = $this->dao->getVisitsWithDatesLimit($from, $to, $visitFieldsToSelect, $lastId, $segmentLimit);
+ if (!empty($logs)) {
+ $lastId = $logs[count($logs) - 1]['idvisit'];
+
+ $this->reattributeVisitLogs($output, $logs);
+ }
+ } while (count($logs) == $segmentLimit);
+ }
+
+ protected function reattributeVisitLogs(OutputInterface $output, $logRows)
+ {
+ foreach ($logRows as $row) {
+ $this->visitorGeolocator->attributeExistingVisit($row);
+
+ $this->onVisitProcessed($output);
+ }
+ }
+
+ /**
+ * @param InputInterface $input
+ * @return int
+ */
+ protected function getPercentStep(InputInterface $input)
+ {
+ $percentStep = $input->getOption(self::PERCENT_STEP_ARGUMENT);
+
+ if (!is_numeric($percentStep)) {
+ return self::PERCENT_STEP_ARGUMENT_DEFAULT;
+ }
+
+ // Percent step should be between maximum percent value and minimum percent value (1-100)
+ if ($percentStep > 99 || $percentStep < 1) {
+ return 100;
+ }
+
+ return $percentStep;
+ }
+
+ /**
+ * Print information about progress.
+ * @param OutputInterface $output
+ */
+ protected function onVisitProcessed(OutputInterface $output)
+ {
+ ++$this->processed;
+
+ $percent = ceil($this->processed / $this->amountOfVisits * 100);
+
+ if ($percent > $this->processedPercent
+ && $percent % $this->percentStep === 0
+ ) {
+ $output->writeln(sprintf('%d%% processed. <comment>%s</comment>', $percent, $this->timer->__toString()));
+
+ $this->processedPercent = $percent;
+ }
+ }
+
+ private function getDateRangeToAttribute(InputInterface $input)
+ {
+ $dateRangeString = $input->getArgument(self::DATES_RANGE_ARGUMENT);
+
+ $dates = explode(',', $dateRangeString);
+ $dates = array_map(array('Piwik\Date', 'factory'), $dates);
+
+ if (count($dates) != 2) {
+ throw new \InvalidArgumentException('Invalid date range supplied: ' . $dateRangeString);
+ }
+
+ return $dates;
+ }
+
+ private function createGeolocator(OutputInterface $output, InputInterface $input)
+ {
+ $providerId = $input->getOption(self::PROVIDER_ARGUMENT);
+ $geolocator = new VisitorGeolocator(LocationProvider::getProviderById($providerId) ?: null);
+
+ $usedProvider = $geolocator->getProvider();
+ if (!$usedProvider->isAvailable()) {
+ throw new \InvalidArgumentException("The provider '$providerId' is not currently available, please make sure it is configured correctly.");
+ }
+
+ $isWorkingOrErrorMessage = $usedProvider->isWorking();
+ if ($isWorkingOrErrorMessage !== true) {
+ $errorMessage = "The provider '$providerId' does not appear to be working correctly. Details: $isWorkingOrErrorMessage";
+
+ $forceGeolocation = $input->getOption(self::FORCE_OPTION);
+ if ($forceGeolocation) {
+ $output->writeln("<error>$errorMessage</error>");
+ $output->writeln("<comment>Ignoring location provider issue, geolocating anyway due to --force option.</comment>");
+ } else {
+ throw new \InvalidArgumentException($errorMessage);
+ }
+ }
+
+ return $geolocator;
+ }
+} \ No newline at end of file
diff --git a/plugins/UserCountry/VisitorGeolocator.php b/plugins/UserCountry/VisitorGeolocator.php
new file mode 100644
index 0000000000..be960c02f9
--- /dev/null
+++ b/plugins/UserCountry/VisitorGeolocator.php
@@ -0,0 +1,277 @@
+<?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\Plugins\UserCountry;
+
+use Piwik\Cache\Cache;
+use Piwik\Cache\Transient;
+use Piwik\Common;
+use Piwik\Container\StaticContainer;
+use Piwik\DataAccess\RawLogDao;
+use Piwik\Network\IPUtils;
+use Piwik\Plugins\UserCountry\LocationProvider\DefaultProvider;
+use Piwik\Tracker\Visit;
+use Psr\Log\LoggerInterface;
+
+require_once PIWIK_INCLUDE_PATH . "/plugins/UserCountry/LocationProvider.php";
+
+/**
+ * Service that determines a visitor's location using visitor information.
+ *
+ * Individual locations are provided by a LocationProvider instance. By default,
+ * the configured LocationProvider (as determined by
+ * `Common::getCurrentLocationProviderId()` is used.
+ *
+ * If the configured location provider cannot provide a location for the visitor,
+ * the default location provider (`DefaultProvider`) is used.
+ *
+ * A cache is used internally to speed up location retrieval. By default, an
+ * in-memory cache is used, but another type of cache can be supplied during
+ * construction.
+ *
+ * This service can be used from within the tracker.
+ */
+class VisitorGeolocator
+{
+ const LAT_LONG_COMPARE_EPSILON = 0.0001;
+
+ /**
+ * @var string[]
+ */
+ public static $logVisitFieldsToUpdate = array(
+ 'location_country' => LocationProvider::COUNTRY_CODE_KEY,
+ 'location_region' => LocationProvider::REGION_CODE_KEY,
+ 'location_city' => LocationProvider::CITY_NAME_KEY,
+ 'location_latitude' => LocationProvider::LATITUDE_KEY,
+ 'location_longitude' => LocationProvider::LONGITUDE_KEY
+ );
+
+ /**
+ * @var Cache
+ */
+ protected static $defaultLocationCache = null;
+
+ /**
+ * @var LocationProvider
+ */
+ private $provider;
+
+ /**
+ * @var LocationProvider
+ */
+ private $backupProvider;
+
+ /**
+ * @var Cache
+ */
+ private $locationCache;
+
+ /**
+ * @var RawLogDao
+ */
+ protected $dao;
+
+ /**
+ * @var LoggerInterface
+ */
+ protected $logger;
+
+ public function __construct(LocationProvider $provider = null, LocationProvider $backupProvider = null, Cache $locationCache = null,
+ RawLogDao $dao = null, LoggerInterface $logger = null)
+ {
+ if ($provider === null) {
+ // note: Common::getCurrentLocationProviderId() uses the tracker cache, which is why it's used here instead
+ // of accessing the option table
+ $provider = LocationProvider::getProviderById(Common::getCurrentLocationProviderId());
+
+ if (empty($provider)) {
+ Common::printDebug("GEO: no current location provider sent, falling back to default '" . DefaultProvider::ID . "' one.");
+
+ $provider = $this->getDefaultProvider();
+ }
+ }
+ $this->provider = $provider;
+
+ $this->backupProvider = $backupProvider ?: $this->getDefaultProvider();
+ $this->locationCache = $locationCache ?: self::getDefaultLocationCache();
+ $this->dao = $dao ?: new RawLogDao();
+ $this->logger = $logger ?: StaticContainer::get('Psr\Log\LoggerInterface');
+ }
+
+ public function getLocation($userInfo, $useClassCache = true)
+ {
+ $userInfoKey = md5(implode(',', $userInfo));
+ if ($useClassCache
+ && $this->locationCache->contains($userInfoKey)
+ ) {
+ return $this->locationCache->fetch($userInfoKey);
+ }
+
+ $location = $this->getLocationObject($this->provider, $userInfo);
+
+ if (empty($location)) {
+ $providerId = $this->provider->getId();
+ Common::printDebug("GEO: couldn't find a location with Geo Module '$providerId'");
+
+ if ($providerId != $this->backupProvider->getId()) {
+ Common::printDebug("Using default provider as fallback...");
+
+ $location = $this->getLocationObject($this->backupProvider, $userInfo);
+ }
+ }
+
+ $location = $location ?: array();
+ if (empty($location['country_code'])) {
+ $location['country_code'] = Visit::UNKNOWN_CODE;
+ }
+
+ $this->locationCache->save($userInfoKey, $location);
+
+ return $location;
+ }
+
+ /**
+ * @param LocationProvider $provider
+ * @param array $userInfo
+ * @return array|false
+ */
+ private function getLocationObject(LocationProvider $provider, $userInfo)
+ {
+ $location = $provider->getLocation($userInfo);
+ $providerId = $provider->getId();
+ $ipAddress = $userInfo['ip'];
+
+ if ($location === false) {
+ return false;
+ }
+
+ Common::printDebug("GEO: Found IP $ipAddress location (provider '" . $providerId . "'): " . var_export($location, true));
+
+ return $location;
+ }
+
+ /**
+ * Geolcates an existing visit and then updates it if it's current attributes are different than
+ * what was geolocated. Also updates all conversions of a visit.
+ *
+ * **This method should NOT be used from within the tracker.**
+ *
+ * @param array $visit The visit information. Must contain an `"idvisit"` element and `"location_ip"` element.
+ * @param bool $useClassCache
+ * @return array|null The visit properties that were updated in the DB mapped to the updated values. If null,
+ * required information was missing from `$visit`.
+ */
+ public function attributeExistingVisit($visit, $useClassCache = true)
+ {
+ if (empty($visit['idvisit'])) {
+ $this->logger->debug('Empty idvisit field. Skipping re-attribution..');
+ return null;
+ }
+
+ $idVisit = $visit['idvisit'];
+
+ if (empty($visit['location_ip'])) {
+ $this->logger->debug('Empty location_ip field for idvisit = %s. Skipping re-attribution.', array('idvisit' => $idVisit));
+ return null;
+ }
+
+ $ip = IPUtils::binaryToStringIP($visit['location_ip']);
+ $location = $this->getLocation(array('ip' => $ip), $useClassCache);
+
+ $valuesToUpdate = $this->getVisitFieldsToUpdate($visit, $location);
+
+ if (!empty($valuesToUpdate)) {
+ $this->logger->debug('Updating visit with idvisit = {idVisit} (IP = {ip}). Changes: {changes}', array(
+ 'idVisit' => $idVisit,
+ 'ip' => $ip,
+ 'changes' => $valuesToUpdate
+ ));
+
+ $this->dao->updateVisits($valuesToUpdate, $idVisit);
+ $this->dao->updateConversions($valuesToUpdate, $idVisit);
+ } else {
+ $this->logger->debug('Nothing to update for idvisit = %s (IP = {ip}). Existing location info is same as geolocated.', array(
+ 'idVisit' => $idVisit,
+ 'ip' => $ip
+ ));
+ }
+
+ return $valuesToUpdate;
+ }
+
+ /**
+ * Returns location log values that are different than the values currently in a log row.
+ *
+ * @param array $row The visit row.
+ * @param array $location The location information.
+ * @return array The location properties to update.
+ */
+ private function getVisitFieldsToUpdate(array $row, $location)
+ {
+ if (isset($location[LocationProvider::COUNTRY_CODE_KEY])) {
+ $location[LocationProvider::COUNTRY_CODE_KEY] = strtolower($location[LocationProvider::COUNTRY_CODE_KEY]);
+ }
+
+ $valuesToUpdate = array();
+ foreach (self::$logVisitFieldsToUpdate as $column => $locationKey) {
+ if (empty($location[$locationKey])) {
+ continue;
+ }
+
+ $locationPropertyValue = $location[$locationKey];
+ $existingPropertyValue = $row[$column];
+
+ if (!$this->areLocationPropertiesEqual($locationKey, $locationPropertyValue, $existingPropertyValue)) {
+ $valuesToUpdate[$column] = $locationPropertyValue;
+ }
+ }
+ return $valuesToUpdate;
+ }
+
+ /**
+ * @return LocationProvider
+ */
+ public function getProvider()
+ {
+ return $this->provider;
+ }
+
+ /**
+ * @return LocationProvider
+ */
+ public function getBackupProvider()
+ {
+ return $this->backupProvider;
+ }
+
+ private function areLocationPropertiesEqual($locationKey, $locationPropertyValue, $existingPropertyValue)
+ {
+ if (($locationKey == LocationProvider::LATITUDE_KEY
+ || $locationKey == LocationProvider::LONGITUDE_KEY)
+ && $existingPropertyValue != 0
+ ) {
+ // floating point comparison
+ return abs(($locationPropertyValue - $existingPropertyValue) / $existingPropertyValue) < self::LAT_LONG_COMPARE_EPSILON;
+ } else {
+ return $locationPropertyValue == $existingPropertyValue;
+ }
+ }
+
+ private function getDefaultProvider()
+ {
+ return LocationProvider::getProviderById(DefaultProvider::ID);
+ }
+
+ public static function getDefaultLocationCache()
+ {
+ if (self::$defaultLocationCache === null) {
+ self::$defaultLocationCache = new Transient();
+ }
+ return self::$defaultLocationCache;
+ }
+} \ No newline at end of file
diff --git a/plugins/UserCountry/tests/Integration/VisitorGeolocatorTest.php b/plugins/UserCountry/tests/Integration/VisitorGeolocatorTest.php
new file mode 100644
index 0000000000..4217f189e6
--- /dev/null
+++ b/plugins/UserCountry/tests/Integration/VisitorGeolocatorTest.php
@@ -0,0 +1,378 @@
+<?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\Plugins\UserCountry\tests\Integration;
+
+use PHPUnit_Framework_MockObject_MockObject;
+use Piwik\Common;
+use Piwik\Db;
+use Piwik\Network\IPUtils;
+use Piwik\Plugins\UserCountry\VisitorGeolocator;
+use Piwik\Plugins\UserCountry\LocationProvider;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Piwik\Tracker\Cache;
+use Piwik\Tracker\Visit;
+use Piwik\Tests\Framework\Mock\LocationProvider as MockLocationProvider;
+
+require_once PIWIK_INCLUDE_PATH . '/tests/PHPUnit/Framework/Mock/LocationProvider.php';
+
+/**
+ * @group UserCountry
+ */
+class VisitorGeolocatorTest extends IntegrationTestCase
+{
+ const TEST_IP = '1.2.3.4';
+
+ public function test_getLocation_shouldReturnLocationForProvider_IfLocationIsSetForCurrentProvider()
+ {
+ $location = array(
+ 'city' => 'Wroclaw',
+ 'country_code' => 'pl'
+ );
+
+ $provider = $this->getProviderMock();
+ $provider->expects($this->once())
+ ->method('getLocation')
+ ->will($this->returnValue($location));
+
+ $geolocator = new VisitorGeolocator($provider);
+
+ $this->assertEquals(
+ $location,
+ $geolocator->getLocation(array('ip' => '127.0.0.1'))
+ );
+ }
+
+ public function test_getLocation_shouldReturnLocationForProvider_IfLocationCountryCodeIsNotSetShouldSetAsxx()
+ {
+ $location = array(
+ 'city' => 'Wroclaw'
+ );
+
+ $provider = $this->getProviderMock();
+ $provider->expects($this->once())
+ ->method('getLocation')
+ ->will($this->returnValue($location));
+
+ $geolocator = new VisitorGeolocator($provider);
+
+ $this->assertEquals(
+ array_merge(
+ $location,
+ array(
+ 'country_code' => Visit::UNKNOWN_CODE
+ )
+ ),
+ $geolocator->getLocation(array('ip' => '127.0.0.2'))
+ );
+ }
+
+ public function test_getLocation_shouldReturnLocationForProviderAndReadFromCacheIfIPIsNotChanged()
+ {
+ $locations = array(
+ 'pl' => array(
+ 'city' => 'Wroclaw',
+ 'country_code' => 'pl'
+ ),
+
+ 'nz' => array(
+ 'city' => 'Wellington',
+ 'country_code' => 'nz'
+ ),
+ );
+
+ $poland = $this->getProviderMock();
+ $poland->expects($this->once())
+ ->method('getLocation')
+ ->will($this->returnValue($locations['pl']));
+
+ $geolocator = new VisitorGeolocator($poland);
+ $geolocator->getLocation(array('ip' => '10.0.0.1'));
+
+ $nz = $this->getProviderMock();
+ $nz->expects($this->once())
+ ->method('getLocation')
+ ->will($this->returnValue($locations['nz']));
+
+ $geolocator = new VisitorGeolocator($nz);
+ $geolocator->getLocation(array('ip' => '10.0.0.2'));
+
+ $this->assertEquals(
+ $locations,
+ array(
+ 'pl' => $geolocator->getLocation(array('ip' => '10.0.0.1')),
+ 'nz' => $geolocator->getLocation(array('ip' => '10.0.0.2'))
+ )
+ );
+ }
+
+ public function test_get_shouldReturnDefaultProvider_IfCurrentProviderReturnFalse()
+ {
+ Cache::setCacheGeneral(array('currentLocationProviderId' => 'nonexistant'));
+ $geolocator = new VisitorGeolocator();
+
+ $this->assertEquals(LocationProvider\DefaultProvider::ID, $geolocator->getProvider()->getId());
+ }
+
+ public function test_get_shouldReturnCurrentProvider_IfCurrentProviderIsSet()
+ {
+ Cache::setCacheGeneral(array('currentLocationProviderId' => MockLocationProvider::ID));
+ $geolocator = new VisitorGeolocator();
+
+ $this->assertEquals(MockLocationProvider::ID, $geolocator->getProvider()->getId());
+ }
+
+ public function getDataForAttributeExistingVisitTests()
+ {
+ $basicTestLocation = array(
+ LocationProvider::COUNTRY_CODE_KEY => 'US',
+ LocationProvider::REGION_CODE_KEY => 'rg',
+ LocationProvider::CITY_NAME_KEY => 'the city',
+ LocationProvider::LATITUDE_KEY => '29.959698',
+ LocationProvider::LONGITUDE_KEY => '-90.064880'
+ );
+
+ // note: floating point values should be used for expected properties so floating point comparison is done
+ // by PHPUnit
+ $basicExpectedVisitProperties = array(
+ 'location_country' => 'us',
+ 'location_region' => 'rg',
+ 'location_city' => 'the city',
+ 'location_latitude' => 29.959698,
+ 'location_longitude' => -90.064880
+ );
+
+ return array(
+ array( // test normal re-attribution
+ $basicTestLocation,
+
+ $basicExpectedVisitProperties
+ ),
+
+ array( // test w/ garbage in location provider result
+ array(
+ LocationProvider::COUNTRY_CODE_KEY => 'US',
+ 'garbage' => 'field',
+ LocationProvider::REGION_CODE_KEY => 'rg',
+ LocationProvider::CITY_NAME_KEY => 'the city',
+ LocationProvider::LATITUDE_KEY => '29.959698',
+ LocationProvider::LONGITUDE_KEY => '-90.064880',
+ 'another' => 'garbage field'
+ ),
+
+ array(
+ 'location_country' => 'us',
+ 'location_region' => 'rg',
+ 'location_city' => 'the city',
+ 'location_latitude' => 29.959698,
+ 'location_longitude' => -90.064880
+ )
+ ),
+
+ array( // test when visit has some correct properties already
+ $basicTestLocation,
+
+ $basicExpectedVisitProperties,
+
+ array(
+ 'location_country' => 'US',
+ 'location_region' => 'rg',
+ 'location_city' => 'the city'
+ ),
+
+ array(
+ 'location_country' => 'us',
+ 'location_latitude' => 29.959698,
+ 'location_longitude' => -90.064880
+ )
+ ),
+
+ array( // test when visit has all correct properties already
+ $basicTestLocation,
+
+ $basicExpectedVisitProperties,
+
+ $basicExpectedVisitProperties,
+
+ array()
+ )
+ );
+ }
+
+ /**
+ * @dataProvider getDataForAttributeExistingVisitTests
+ */
+ public function test_attributeExistingVisit_CorrectlySetsLocationProperties_AndReturnsCorrectResult(
+ $mockLocation, $expectedVisitProperties, $visitProperties = array(), $expectedUpdateValues = null)
+ {
+ $mockLocationProvider = $this->getProviderMockThatGeolocates($mockLocation);
+
+ $visit = $this->insertVisit($visitProperties);
+ $this->insertTwoConversions($visit);
+
+ $geolocator = new VisitorGeolocator($mockLocationProvider);
+ $valuesUpdated = $geolocator->attributeExistingVisit($visit, $useCache = false);
+
+ $this->assertEquals($expectedVisitProperties, $this->getVisit($visit['idvisit']), $message = '', $delta = 0.001);
+
+ $expectedUpdateValues = $expectedUpdateValues === null ? $expectedVisitProperties : $expectedUpdateValues;
+ $this->assertEquals($expectedUpdateValues, $valuesUpdated, $message = '', $delta = 0.001);
+
+ $conversions = $this->getConversions($visit);
+ $this->assertEquals(array($expectedVisitProperties, $expectedVisitProperties), $conversions, $message = '', $delta = 0.001);
+ }
+
+ public function test_attributeExistingVisit_ReturnsNull_AndSkipsAttribution_IfIdVisitMissingFromInput()
+ {
+ $mockLocationProvider = $this->getProviderMock();
+ $geolocator = new VisitorGeolocator($mockLocationProvider);
+
+ $result = $geolocator->attributeExistingVisit(array());
+
+ $this->assertNull($result);
+ }
+
+ public function test_attributeExistingVisit_ReturnsNull_AndSkipsAttribution_IfIdVisitPresent_AndLocationIpMissingFromInput()
+ {
+ $mockLocationProvider = $this->getProviderMock();
+ $geolocator = new VisitorGeolocator($mockLocationProvider);
+
+ $result = $geolocator->attributeExistingVisit(array('idvisit' => 1));
+
+ $this->assertNull($result);
+ }
+
+ /**
+ * @return PHPUnit_Framework_MockObject_MockObject|LocationProvider
+ */
+ protected function getProviderMock()
+ {
+ return $this->getMockBuilder('\Piwik\Plugins\UserCountry\LocationProvider')
+ ->setMethods(array('getId', 'getLocation', 'isAvailable', 'isWorking', 'getSupportedLocationInfo'))
+ ->disableOriginalConstructor()
+ ->getMock();
+ }
+
+ private function getProviderMockThatGeolocates($locationResult)
+ {
+ $mock = $this->getProviderMock();
+ $mock->expects($this->once())->method('getLocation')->will($this->returnCallback(function ($info) use ($locationResult) {
+ if ($info['ip'] == VisitorGeolocatorTest::TEST_IP) {
+ return $locationResult;
+ } else {
+ return null;
+ }
+ }));
+ return $mock;
+ }
+
+ private function insertVisit($visit = array())
+ {
+ $defaultProperties = array(
+ 'idsite' => 1,
+ 'idvisitor' => hex2bin('ea95f303f2165aa0'),
+ 'visit_last_action_time' => '2012-01-01 00:00:00',
+ 'config_id' => hex2bin('ea95f303f2165aa0'),
+ 'location_ip' => IPUtils::stringToBinaryIP(self::TEST_IP),
+ 'visitor_localtime' => '2012-01-01 00:00:00',
+ 'location_country' => 'xx',
+ 'config_os' => 'xxx',
+ 'visit_total_events' => 0,
+ 'visitor_days_since_last' => 0,
+ 'config_quicktime' => 0,
+ 'config_pdf' => 0,
+ 'config_realplayer' => 0,
+ 'config_silverlight' => 0,
+ 'config_windowsmedia' => 0,
+ 'config_java' => 0,
+ 'config_gears' => 0,
+ 'config_resolution' => 0,
+ 'config_resolution' => '',
+ 'config_cookie' => 0,
+ 'config_director' => 0,
+ 'config_flash' => 0,
+ 'config_browser_version' => '',
+ 'visitor_count_visits' => 1,
+ 'visitor_returning' => 0,
+ 'visit_total_time' => 123,
+ 'visit_entry_idaction_name' => 0,
+ 'visit_entry_idaction_url' => 0,
+ 'visitor_days_since_order' => 0,
+ 'visitor_days_since_first' => 0,
+ 'visit_first_action_time' => '2012-01-01 00:00:00',
+ 'visit_goal_buyer' => 0,
+ 'visit_goal_converted' => 0,
+ 'visit_exit_idaction_name' => 0,
+ 'referer_url' => '',
+ 'location_browser_lang' => 'xx',
+ 'config_browser_engine' => '',
+ 'config_browser_name' => '',
+ 'referer_type' => 0,
+ 'referer_name' => '',
+ 'visit_total_actions' => 0,
+ 'visit_total_searches' => 0
+ );
+
+ $visit = array_merge($defaultProperties, $visit);
+
+ $this->insertInto('log_visit', $visit);
+
+ $idVisit = Db::fetchOne("SELECT LAST_INSERT_ID()");
+ return $this->getVisit($idVisit, $allColumns = true);
+ }
+
+ private function getVisit($idVisit, $allColumns = false)
+ {
+ $columns = $allColumns ? "*" : "location_country, location_region, location_city, location_latitude, location_longitude";
+ $visit = Db::fetchRow("SELECT $columns FROM " . Common::prefixTable('log_visit') . " WHERE idvisit = ?", array($idVisit));
+
+ return $visit;
+ }
+
+ private function insertTwoConversions($visit)
+ {
+ $conversionProperties = array(
+ 'idvisit' => $visit['idvisit'],
+ 'idsite' => $visit['idsite'],
+ 'idvisitor' => $visit['idvisitor'],
+ 'server_time' => '2012-01-01 00:00:00',
+ 'idgoal' => 1,
+ 'buster' => 1,
+ 'url' => '',
+ 'location_longitude' => $visit['location_longitude'],
+ 'location_latitude' => $visit['location_latitude'],
+ 'location_region' => $visit['location_region'],
+ 'location_country' => $visit['location_country'],
+ 'location_city' => $visit['location_city'],
+ 'visitor_count_visits' => $visit['visitor_count_visits'],
+ 'visitor_returning' => $visit['visitor_returning'],
+ 'visitor_days_since_order' => 0,
+ 'visitor_days_since_first' => 0
+ );
+
+ $this->insertInto('log_conversion', $conversionProperties);
+
+ $conversionProperties['buster'] = 2;
+ $this->insertInto('log_conversion', $conversionProperties);
+ }
+
+ private function insertInto($table, $row)
+ {
+ $columns = implode(', ', array_keys($row));
+ $columnsPlaceholders = Common::getSqlStringFieldsArray($row);
+ $values = array_values($row);
+
+ Db::query("INSERT INTO " . Common::prefixTable($table) . " ($columns) VALUES ($columnsPlaceholders)", $values);
+ }
+
+ private function getConversions($visit)
+ {
+ return Db::fetchAll("SELECT location_country, location_region, location_city, location_latitude, location_longitude
+ FROM " . Common::prefixTable('log_conversion') . " WHERE idvisit = ?", array($visit['idvisit']));
+ }
+} \ No newline at end of file
diff --git a/plugins/UserCountry/tests/System/AttributeHistoricalDataWithLocationsTest.php b/plugins/UserCountry/tests/System/AttributeHistoricalDataWithLocationsTest.php
new file mode 100644
index 0000000000..c9ceaa3791
--- /dev/null
+++ b/plugins/UserCountry/tests/System/AttributeHistoricalDataWithLocationsTest.php
@@ -0,0 +1,148 @@
+<?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\Plugins\UserCountry\Test\Integration;
+
+use Piwik\Common;
+use Piwik\Db;
+use Piwik\Plugins\UserCountry\Commands\AttributeHistoricalDataWithLocations;
+use Piwik\Tests\Fixtures\ManyVisitsWithGeoIP;
+use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
+use Symfony\Component\Console\Application;
+use Symfony\Component\Console\Tester\CommandTester;
+
+/**
+ * Class AttributeHistoricalDataWithLocationsTest
+ * @package Piwik\Plugins\UserCountry\Test\Integration
+ *
+ * @group UserCountry
+ */
+class AttributeHistoricalDataWithLocationsTest extends IntegrationTestCase
+{
+ /**
+ * @var ManyVisitsWithGeoIP
+ */
+ public static $fixture = null;
+
+ public function setUp()
+ {
+ parent::setUp();
+
+ $tablesToUpdate = array('log_visit', 'log_conversion');
+ $columnsToUpdate = array(
+ 'location_country' => '"xx"',
+ 'location_region' => 'NULL',
+ 'location_city' => 'NULL',
+ 'location_latitude' => 'NULL',
+ 'location_longitude' => 'NULL'
+ );
+
+ foreach ($tablesToUpdate as $table) {
+ $sql = "UPDATE `" . Common::prefixTable($table) . "` SET ";
+
+ $sets = array();
+ foreach ($columnsToUpdate as $column => $defaultValue) {
+ $sets[] = $column . ' = ' . $defaultValue;
+ }
+
+ $sql .= implode(', ', $sets);
+
+ Db::query($sql);
+ }
+
+ self::$fixture->setLocationProvider('GeoIPCity.dat');
+ }
+
+ /**
+ * @expectedException \RuntimeException
+ * @expectedExceptionMessage Not enough arguments
+ */
+ public function testExecute_ShouldThrowException_IfArgumentIsMissing()
+ {
+ $this->executeCommand(null);
+ }
+
+ /**
+ * @expectedException \Exception
+ * @expectedExceptionMessage General_ExceptionInvalidDateFormat
+ */
+ public function testExecute_ShouldReturnMessage_IfDatesAreInvalid()
+ {
+ $this->executeCommand('test');
+ }
+
+ public function testExecute_ShouldReturnEmptyWorkingProcessLogs_IfThereIsNoData()
+ {
+ $this->assertRegExp(
+ '/Re-attribution for date range: 2014-06-01 to 2014-06-06. 0 visits to process with provider "geoip_php"./',
+ $this->executeCommand('2014-06-01,2014-06-06')
+ );
+ }
+
+ public function testExecute_ShouldReturnLogAfterWorkingWithSomeData()
+ {
+ $result = $this->executeCommand('2010-01-03,2010-06-03');
+
+ $this->assertContains(
+ 'Re-attribution for date range: 2010-01-03 to 2010-06-03. 35 visits to process with provider "geoip_php".',
+ $result
+ );
+
+ $this->assertRegExp('/100% processed. Time elapsed: [0-9.]+s/', $result);
+
+ $queryParams = array(
+ 'idSite' => self::$fixture->idSite,
+ 'date' => self::$fixture->dateTime,
+ 'period' => 'month'
+ );
+
+ $this->assertApiResponseEqualsExpected("UserCountry.getCountry", $queryParams);
+ $this->assertApiResponseEqualsExpected("UserCountry.getContinent", $queryParams);
+ $this->assertApiResponseEqualsExpected("UserCountry.getRegion", $queryParams);
+ $this->assertApiResponseEqualsExpected("UserCountry.getCity", $queryParams);
+ }
+
+ /**
+ * @param string|null $dates
+ *
+ * @return string
+ */
+ private function executeCommand($dates)
+ {
+ $command = new AttributeHistoricalDataWithLocations();
+
+ $application = new Application();
+ $application->add($command);
+
+ $commandTester = new CommandTester($command);
+
+ if (is_null($dates)) {
+ $params = array();
+ } else {
+ $params = array(AttributeHistoricalDataWithLocations::DATES_RANGE_ARGUMENT => $dates);
+ }
+
+ $params['command'] = $command->getName();
+ $commandTester->execute($params);
+ $result = $commandTester->getDisplay();
+
+ return $result;
+ }
+
+ public static function configureFixture($fixture)
+ {
+ // empty (undo IntegrationTestCase configuring)
+ }
+
+ public static function getPathToTestDirectory()
+ {
+ return __DIR__;
+ }
+}
+
+AttributeHistoricalDataWithLocationsTest::$fixture = new ManyVisitsWithGeoIP();
diff --git a/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getCity_month.xml b/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getCity_month.xml
new file mode 100644
index 0000000000..defdedc063
--- /dev/null
+++ b/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getCity_month.xml
@@ -0,0 +1,166 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <label>General_Unknown</label>
+ <nb_visits>22</nb_visits>
+ <nb_actions>39</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>13871</sum_visit_length>
+ <bounce_count>11</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>22</nb_conversions>
+ <nb_visits_converted>22</nb_visits_converted>
+ <revenue>110</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>11</nb_conversions>
+ <nb_visits_converted>11</nb_visits_converted>
+ <revenue>55</revenue>
+ </row>
+ </goals>
+ <nb_conversions>33</nb_conversions>
+ <revenue>165</revenue>
+ <sum_daily_nb_uniq_visitors>11</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <city_name>General_Unknown</city_name>
+ <city>xx</city>
+ <region>xx</region>
+ <country>xx</country>
+ <country_name>General_Unknown</country_name>
+ <region_name>General_Unknown</region_name>
+ <logo>plugins/UserCountry/images/flags/xx.png</logo>
+ </row>
+ <row>
+ <label>Vancouver, British Columbia, UserCountry_country_ca</label>
+ <nb_visits>6</nb_visits>
+ <nb_actions>10</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>3783</sum_visit_length>
+ <bounce_count>3</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>6</nb_conversions>
+ <nb_visits_converted>6</nb_visits_converted>
+ <revenue>30</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>3</nb_conversions>
+ <nb_visits_converted>3</nb_visits_converted>
+ <revenue>15</revenue>
+ </row>
+ </goals>
+ <nb_conversions>9</nb_conversions>
+ <revenue>45</revenue>
+ <sum_daily_nb_uniq_visitors>3</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <lat>49.25</lat>
+ <long>-123.133</long>
+ <segment>city==Vancouver;regionCode==BC;countryCode==ca</segment>
+ <city_name>Vancouver</city_name>
+ <region>BC</region>
+ <country>ca</country>
+ <country_name>UserCountry_country_ca</country_name>
+ <region_name>British Columbia</region_name>
+ <logo>plugins/UserCountry/images/flags/ca.png</logo>
+ </row>
+ <row>
+ <label>Besançon, Franche-Comte, UserCountry_country_fr</label>
+ <nb_visits>3</nb_visits>
+ <nb_actions>5</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>2</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>3</nb_conversions>
+ <nb_visits_converted>3</nb_visits_converted>
+ <revenue>15</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>4</nb_conversions>
+ <revenue>20</revenue>
+ <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>1</sum_daily_nb_users>
+ <lat>47.249</lat>
+ <long>6.018</long>
+ <segment>city==Besan%C3%A7on;regionCode==A6;countryCode==fr</segment>
+ <city_name>Besançon</city_name>
+ <region>A6</region>
+ <country>fr</country>
+ <country_name>UserCountry_country_fr</country_name>
+ <region_name>Franche-Comte</region_name>
+ <logo>plugins/UserCountry/images/flags/fr.png</logo>
+ </row>
+ <row>
+ <label>Lhasa, General_Unknown, UserCountry_country_ti</label>
+ <nb_visits>2</nb_visits>
+ <nb_actions>3</nb_actions>
+ <max_actions>2</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>1</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>2</nb_conversions>
+ <nb_visits_converted>2</nb_visits_converted>
+ <revenue>10</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>3</nb_conversions>
+ <revenue>15</revenue>
+ <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <lat>29.65</lat>
+ <long>91.1</long>
+ <segment>city==Lhasa;regionCode==1;countryCode==ti</segment>
+ <city_name>Lhasa</city_name>
+ <region>1</region>
+ <country>ti</country>
+ <country_name>UserCountry_country_ti</country_name>
+ <region_name>General_Unknown</region_name>
+ <logo>plugins/UserCountry/images/flags/ti.png</logo>
+ </row>
+ <row>
+ <label>Rome, Lazio, UserCountry_country_it</label>
+ <nb_visits>2</nb_visits>
+ <nb_actions>4</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>1</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>2</nb_conversions>
+ <nb_visits_converted>2</nb_visits_converted>
+ <revenue>10</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>3</nb_conversions>
+ <revenue>15</revenue>
+ <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <lat>41.9</lat>
+ <long>12.483</long>
+ <segment>city==Rome;regionCode==07;countryCode==it</segment>
+ <city_name>Rome</city_name>
+ <region>07</region>
+ <country>it</country>
+ <country_name>UserCountry_country_it</country_name>
+ <region_name>Lazio</region_name>
+ <logo>plugins/UserCountry/images/flags/it.png</logo>
+ </row>
+</result>
diff --git a/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getContinent_month.xml b/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getContinent_month.xml
new file mode 100644
index 0000000000..e78869187a
--- /dev/null
+++ b/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getContinent_month.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <label>UserCountry_continent_asi</label>
+ <nb_visits>22</nb_visits>
+ <nb_actions>38</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>13871</sum_visit_length>
+ <bounce_count>11</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>22</nb_conversions>
+ <nb_visits_converted>22</nb_visits_converted>
+ <revenue>110</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>11</nb_conversions>
+ <nb_visits_converted>11</nb_visits_converted>
+ <revenue>55</revenue>
+ </row>
+ </goals>
+ <nb_conversions>33</nb_conversions>
+ <revenue>165</revenue>
+ <sum_daily_nb_uniq_visitors>11</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <code>UserCountry_continent_asi</code>
+ </row>
+ <row>
+ <label>UserCountry_continent_amn</label>
+ <nb_visits>8</nb_visits>
+ <nb_actions>14</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>5044</sum_visit_length>
+ <bounce_count>4</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>8</nb_conversions>
+ <nb_visits_converted>8</nb_visits_converted>
+ <revenue>40</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>4</nb_conversions>
+ <nb_visits_converted>4</nb_visits_converted>
+ <revenue>20</revenue>
+ </row>
+ </goals>
+ <nb_conversions>12</nb_conversions>
+ <revenue>60</revenue>
+ <sum_daily_nb_uniq_visitors>4</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <code>UserCountry_continent_amn</code>
+ </row>
+ <row>
+ <label>UserCountry_continent_eur</label>
+ <nb_visits>5</nb_visits>
+ <nb_actions>9</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>2522</sum_visit_length>
+ <bounce_count>3</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>5</nb_conversions>
+ <nb_visits_converted>5</nb_visits_converted>
+ <revenue>25</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>2</nb_conversions>
+ <nb_visits_converted>2</nb_visits_converted>
+ <revenue>10</revenue>
+ </row>
+ </goals>
+ <nb_conversions>7</nb_conversions>
+ <revenue>35</revenue>
+ <sum_daily_nb_uniq_visitors>3</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>1</sum_daily_nb_users>
+ <code>UserCountry_continent_eur</code>
+ </row>
+</result>
diff --git a/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getCountry_month.xml b/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getCountry_month.xml
new file mode 100644
index 0000000000..ddbce4e502
--- /dev/null
+++ b/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getCountry_month.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <label>UserCountry_country_cn</label>
+ <nb_visits>18</nb_visits>
+ <nb_actions>32</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>11349</sum_visit_length>
+ <bounce_count>9</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>18</nb_conversions>
+ <nb_visits_converted>18</nb_visits_converted>
+ <revenue>90</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>9</nb_conversions>
+ <nb_visits_converted>9</nb_visits_converted>
+ <revenue>45</revenue>
+ </row>
+ </goals>
+ <nb_conversions>27</nb_conversions>
+ <revenue>135</revenue>
+ <sum_daily_nb_uniq_visitors>9</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <code>cn</code>
+ <logo>plugins/UserCountry/images/flags/cn.png</logo>
+ <segment>countryCode==cn</segment>
+ <logoWidth>16</logoWidth>
+ <logoHeight>11</logoHeight>
+ </row>
+ <row>
+ <label>UserCountry_country_ca</label>
+ <nb_visits>6</nb_visits>
+ <nb_actions>10</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>3783</sum_visit_length>
+ <bounce_count>3</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>6</nb_conversions>
+ <nb_visits_converted>6</nb_visits_converted>
+ <revenue>30</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>3</nb_conversions>
+ <nb_visits_converted>3</nb_visits_converted>
+ <revenue>15</revenue>
+ </row>
+ </goals>
+ <nb_conversions>9</nb_conversions>
+ <revenue>45</revenue>
+ <sum_daily_nb_uniq_visitors>3</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <code>ca</code>
+ <logo>plugins/UserCountry/images/flags/ca.png</logo>
+ <segment>countryCode==ca</segment>
+ <logoWidth>16</logoWidth>
+ <logoHeight>11</logoHeight>
+ </row>
+ <row>
+ <label>UserCountry_country_fr</label>
+ <nb_visits>3</nb_visits>
+ <nb_actions>5</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>2</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>3</nb_conversions>
+ <nb_visits_converted>3</nb_visits_converted>
+ <revenue>15</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>4</nb_conversions>
+ <revenue>20</revenue>
+ <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>1</sum_daily_nb_users>
+ <code>fr</code>
+ <logo>plugins/UserCountry/images/flags/fr.png</logo>
+ <segment>countryCode==fr</segment>
+ <logoWidth>16</logoWidth>
+ <logoHeight>11</logoHeight>
+ </row>
+ <row>
+ <label>UserCountry_country_id</label>
+ <nb_visits>2</nb_visits>
+ <nb_actions>3</nb_actions>
+ <max_actions>2</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>1</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>2</nb_conversions>
+ <nb_visits_converted>2</nb_visits_converted>
+ <revenue>10</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>3</nb_conversions>
+ <revenue>15</revenue>
+ <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <code>id</code>
+ <logo>plugins/UserCountry/images/flags/id.png</logo>
+ <segment>countryCode==id</segment>
+ <logoWidth>16</logoWidth>
+ <logoHeight>11</logoHeight>
+ </row>
+ <row>
+ <label>UserCountry_country_it</label>
+ <nb_visits>2</nb_visits>
+ <nb_actions>4</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>1</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>2</nb_conversions>
+ <nb_visits_converted>2</nb_visits_converted>
+ <revenue>10</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>3</nb_conversions>
+ <revenue>15</revenue>
+ <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <code>it</code>
+ <logo>plugins/UserCountry/images/flags/it.png</logo>
+ <segment>countryCode==it</segment>
+ <logoWidth>16</logoWidth>
+ <logoHeight>11</logoHeight>
+ </row>
+ <row>
+ <label>UserCountry_country_ti</label>
+ <nb_visits>2</nb_visits>
+ <nb_actions>3</nb_actions>
+ <max_actions>2</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>1</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>2</nb_conversions>
+ <nb_visits_converted>2</nb_visits_converted>
+ <revenue>10</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>3</nb_conversions>
+ <revenue>15</revenue>
+ <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <code>ti</code>
+ <logo>plugins/UserCountry/images/flags/ti.png</logo>
+ <segment>countryCode==ti</segment>
+ <logoWidth>16</logoWidth>
+ <logoHeight>11</logoHeight>
+ </row>
+ <row>
+ <label>UserCountry_country_us</label>
+ <nb_visits>2</nb_visits>
+ <nb_actions>4</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>1</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>2</nb_conversions>
+ <nb_visits_converted>2</nb_visits_converted>
+ <revenue>10</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>3</nb_conversions>
+ <revenue>15</revenue>
+ <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <code>us</code>
+ <logo>plugins/UserCountry/images/flags/us.png</logo>
+ <segment>countryCode==us</segment>
+ <logoWidth>16</logoWidth>
+ <logoHeight>11</logoHeight>
+ </row>
+</result>
diff --git a/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getRegion_month.xml b/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getRegion_month.xml
new file mode 100644
index 0000000000..1a166ac62a
--- /dev/null
+++ b/plugins/UserCountry/tests/System/expected/test_AttributeHistoricalDataWithLocationsTest_testExecute_ShouldReturnLogAfterWorkingWithSomeData__UserCountry.getRegion_month.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<result>
+ <row>
+ <label>General_Unknown</label>
+ <nb_visits>22</nb_visits>
+ <nb_actions>39</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>13871</sum_visit_length>
+ <bounce_count>11</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>22</nb_conversions>
+ <nb_visits_converted>22</nb_visits_converted>
+ <revenue>110</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>11</nb_conversions>
+ <nb_visits_converted>11</nb_visits_converted>
+ <revenue>55</revenue>
+ </row>
+ </goals>
+ <nb_conversions>33</nb_conversions>
+ <revenue>165</revenue>
+ <sum_daily_nb_uniq_visitors>11</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <region>xx</region>
+ <country>xx</country>
+ <country_name>General_Unknown</country_name>
+ <region_name>General_Unknown</region_name>
+ <logo>plugins/UserCountry/images/flags/xx.png</logo>
+ </row>
+ <row>
+ <label>British Columbia, UserCountry_country_ca</label>
+ <nb_visits>6</nb_visits>
+ <nb_actions>10</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>3783</sum_visit_length>
+ <bounce_count>3</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>6</nb_conversions>
+ <nb_visits_converted>6</nb_visits_converted>
+ <revenue>30</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>3</nb_conversions>
+ <nb_visits_converted>3</nb_visits_converted>
+ <revenue>15</revenue>
+ </row>
+ </goals>
+ <nb_conversions>9</nb_conversions>
+ <revenue>45</revenue>
+ <sum_daily_nb_uniq_visitors>3</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <segment>regionCode==BC;countryCode==ca</segment>
+ <region>BC</region>
+ <country>ca</country>
+ <country_name>UserCountry_country_ca</country_name>
+ <region_name>British Columbia</region_name>
+ <logo>plugins/UserCountry/images/flags/ca.png</logo>
+ </row>
+ <row>
+ <label>Franche-Comte, UserCountry_country_fr</label>
+ <nb_visits>3</nb_visits>
+ <nb_actions>5</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>2</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>3</nb_conversions>
+ <nb_visits_converted>3</nb_visits_converted>
+ <revenue>15</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>4</nb_conversions>
+ <revenue>20</revenue>
+ <sum_daily_nb_uniq_visitors>2</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>1</sum_daily_nb_users>
+ <segment>regionCode==A6;countryCode==fr</segment>
+ <region>A6</region>
+ <country>fr</country>
+ <country_name>UserCountry_country_fr</country_name>
+ <region_name>Franche-Comte</region_name>
+ <logo>plugins/UserCountry/images/flags/fr.png</logo>
+ </row>
+ <row>
+ <label>General_Unknown, UserCountry_country_ti</label>
+ <nb_visits>2</nb_visits>
+ <nb_actions>3</nb_actions>
+ <max_actions>2</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>1</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>2</nb_conversions>
+ <nb_visits_converted>2</nb_visits_converted>
+ <revenue>10</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>3</nb_conversions>
+ <revenue>15</revenue>
+ <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <segment>regionCode==1;countryCode==ti</segment>
+ <region>1</region>
+ <country>ti</country>
+ <country_name>UserCountry_country_ti</country_name>
+ <region_name>General_Unknown</region_name>
+ <logo>plugins/UserCountry/images/flags/ti.png</logo>
+ </row>
+ <row>
+ <label>Lazio, UserCountry_country_it</label>
+ <nb_visits>2</nb_visits>
+ <nb_actions>4</nb_actions>
+ <max_actions>3</max_actions>
+ <sum_visit_length>1261</sum_visit_length>
+ <bounce_count>1</bounce_count>
+ <goals>
+ <row idgoal='1'>
+ <nb_conversions>2</nb_conversions>
+ <nb_visits_converted>2</nb_visits_converted>
+ <revenue>10</revenue>
+ </row>
+ <row idgoal='2'>
+ <nb_conversions>1</nb_conversions>
+ <nb_visits_converted>1</nb_visits_converted>
+ <revenue>5</revenue>
+ </row>
+ </goals>
+ <nb_conversions>3</nb_conversions>
+ <revenue>15</revenue>
+ <sum_daily_nb_uniq_visitors>1</sum_daily_nb_uniq_visitors>
+ <sum_daily_nb_users>0</sum_daily_nb_users>
+ <segment>regionCode==07;countryCode==it</segment>
+ <region>07</region>
+ <country>it</country>
+ <country_name>UserCountry_country_it</country_name>
+ <region_name>Lazio</region_name>
+ <logo>plugins/UserCountry/images/flags/it.png</logo>
+ </row>
+</result>
diff --git a/plugins/UserCountry/tests/Unit/UserCountryTest.php b/plugins/UserCountry/tests/Unit/UserCountryTest.php
index b4ada3e4d1..9b62518e3e 100644
--- a/plugins/UserCountry/tests/Unit/UserCountryTest.php
+++ b/plugins/UserCountry/tests/Unit/UserCountryTest.php
@@ -19,7 +19,7 @@ use Exception;
require_once PIWIK_INCLUDE_PATH . '/plugins/UserCountry/UserCountry.php';
require_once PIWIK_INCLUDE_PATH . '/plugins/UserCountry/functions.php';
-class UserCountryTest extends \PHPUnit_Framework_Testcase
+class UserCountryTest extends \PHPUnit_Framework_TestCase
{
/**
* @group Plugins
diff --git a/plugins/UserLanguage/API.php b/plugins/UserLanguage/API.php
index 30d6ffd8ce..3baa4cc159 100644
--- a/plugins/UserLanguage/API.php
+++ b/plugins/UserLanguage/API.php
@@ -30,7 +30,6 @@ class API extends \Piwik\Plugin\API
Piwik::checkUserHasViewAccess($idSite);
$archive = Archive::build($idSite, $period, $date, $segment);
$dataTable = $archive->getDataTable($name);
- $dataTable->filter('Sort', array(Metrics::INDEX_NB_VISITS));
$dataTable->queueFilter('ReplaceColumnNames');
$dataTable->queueFilter('ReplaceSummaryRowLabel');
return $dataTable;
diff --git a/plugins/UserLanguage/Archiver.php b/plugins/UserLanguage/Archiver.php
index 14fd369eec..161630b7f2 100644
--- a/plugins/UserLanguage/Archiver.php
+++ b/plugins/UserLanguage/Archiver.php
@@ -46,7 +46,15 @@ class Archiver extends \Piwik\Plugin\Archiver
$dataTableRecords = array(
self::LANGUAGE_RECORD_NAME,
);
- $this->getProcessor()->aggregateDataTableRecords($dataTableRecords, $this->maximumRows);
+ $columnsAggregationOperation = null;
+ $this->getProcessor()->aggregateDataTableRecords(
+ $dataTableRecords,
+ $this->maximumRows,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
}
protected function aggregateByLanguage()
diff --git a/plugins/UserLanguage/tests/System/expected/test___UserLanguage.getLanguageCode_day.xml b/plugins/UserLanguage/tests/System/expected/test___UserLanguage.getLanguageCode_day.xml
index 02c15ad520..704c934193 100644
--- a/plugins/UserLanguage/tests/System/expected/test___UserLanguage.getLanguageCode_day.xml
+++ b/plugins/UserLanguage/tests/System/expected/test___UserLanguage.getLanguageCode_day.xml
@@ -45,7 +45,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Czech - Czech Republic (cs-cz)</label>
+ <label>Basque - Spain (eu-es)</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -56,7 +56,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>German (de)</label>
+ <label>Chinese - Singapore (zh-sg)</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -67,7 +67,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Greek - Greece (el-gr)</label>
+ <label>Czech - Czech Republic (cs-cz)</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -78,7 +78,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Basque - Spain (eu-es)</label>
+ <label>French (fr)</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -89,7 +89,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>French (fr)</label>
+ <label>French - Switzerland (fr-ch)</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -100,7 +100,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>French - Switzerland (fr-ch)</label>
+ <label>German (de)</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -111,7 +111,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Serbian - Serbia Montenegro (sr-cs)</label>
+ <label>Greek - Greece (el-gr)</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -122,7 +122,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Thai (th)</label>
+ <label>Serbian - Serbia Montenegro (sr-cs)</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -133,7 +133,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Unknown (xx)</label>
+ <label>Thai (th)</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -144,7 +144,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Chinese - Singapore (zh-sg)</label>
+ <label>Unknown (xx)</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
diff --git a/plugins/UserLanguage/tests/System/expected/test___UserLanguage.getLanguage_day.xml b/plugins/UserLanguage/tests/System/expected/test___UserLanguage.getLanguage_day.xml
index 18d4468a2e..04af2f2ccf 100644
--- a/plugins/UserLanguage/tests/System/expected/test___UserLanguage.getLanguage_day.xml
+++ b/plugins/UserLanguage/tests/System/expected/test___UserLanguage.getLanguage_day.xml
@@ -1,6 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<result>
<row>
+ <label>French</label>
+ <nb_uniq_visitors>3</nb_uniq_visitors>
+ <nb_visits>4</nb_visits>
+ <nb_actions>4</nb_actions>
+ <nb_users>0</nb_users>
+ <max_actions>1</max_actions>
+ <sum_visit_length>0</sum_visit_length>
+ <bounce_count>4</bounce_count>
+ <nb_visits_converted>0</nb_visits_converted>
+ </row>
+ <row>
<label>Polish</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>3</nb_visits>
@@ -23,17 +34,6 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>French</label>
- <nb_uniq_visitors>3</nb_uniq_visitors>
- <nb_visits>4</nb_visits>
- <nb_actions>4</nb_actions>
- <nb_users>0</nb_users>
- <max_actions>1</max_actions>
- <sum_visit_length>0</sum_visit_length>
- <bounce_count>4</bounce_count>
- <nb_visits_converted>0</nb_visits_converted>
- </row>
- <row>
<label>Arabic</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
@@ -45,7 +45,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Czech</label>
+ <label>Basque</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -56,7 +56,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>German</label>
+ <label>Chinese</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -67,7 +67,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Greek</label>
+ <label>Czech</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -78,7 +78,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Basque</label>
+ <label>German</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -89,7 +89,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Serbian</label>
+ <label>Greek</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -100,7 +100,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Thai</label>
+ <label>Serbian</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -111,7 +111,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Unknown</label>
+ <label>Thai</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
@@ -122,7 +122,7 @@
<nb_visits_converted>0</nb_visits_converted>
</row>
<row>
- <label>Chinese</label>
+ <label>Unknown</label>
<nb_uniq_visitors>1</nb_uniq_visitors>
<nb_visits>1</nb_visits>
<nb_actions>1</nb_actions>
diff --git a/plugins/UsersManager/API.php b/plugins/UsersManager/API.php
index b1c7c227e3..ccb4dac126 100644
--- a/plugins/UsersManager/API.php
+++ b/plugins/UsersManager/API.php
@@ -45,9 +45,9 @@ class API extends \Piwik\Plugin\API
private static $instance = null;
- protected function __construct()
+ public function __construct(Model $model)
{
- $this->model = new Model();
+ $this->model = $model;
}
/**
@@ -71,7 +71,7 @@ class API extends \Piwik\Plugin\API
self::$instance = $instance;
} catch (Exception $e) {
- self::$instance = new self;
+ self::$instance = StaticContainer::get('Piwik\Plugins\UsersManager\API');
StaticContainer::getContainer()->set('UsersManager_API', self::$instance);
}
diff --git a/plugins/VisitTime/API.php b/plugins/VisitTime/API.php
index 928e8c95f3..e1f90db70e 100644
--- a/plugins/VisitTime/API.php
+++ b/plugins/VisitTime/API.php
@@ -33,7 +33,7 @@ class API extends \Piwik\Plugin\API
$archive = Archive::build($idSite, $period, $date, $segment);
$dataTable = $archive->getDataTable($name);
- $dataTable->filter('Sort', array('label', 'asc', true));
+ $dataTable->filter('Sort', array('label', 'asc', true, false));
$dataTable->queueFilter('ColumnCallbackReplace', array('label', __NAMESPACE__ . '\getTimeLabel'));
$dataTable->queueFilter('ReplaceColumnNames');
return $dataTable;
diff --git a/plugins/VisitTime/Archiver.php b/plugins/VisitTime/Archiver.php
index e7fd9c7a19..30bb3dd869 100644
--- a/plugins/VisitTime/Archiver.php
+++ b/plugins/VisitTime/Archiver.php
@@ -30,7 +30,15 @@ class Archiver extends \Piwik\Plugin\Archiver
self::LOCAL_TIME_RECORD_NAME,
self::SERVER_TIME_RECORD_NAME,
);
- $this->getProcessor()->aggregateDataTableRecords($dataTableRecords);
+ $columnsAggregationOperation = null;
+ $this->getProcessor()->aggregateDataTableRecords(
+ $dataTableRecords,
+ $maximumRowsInDataTableLevelZero = null,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
}
protected function aggregateByServerTime()
diff --git a/plugins/VisitTime/Reports/GetByDayOfWeek.php b/plugins/VisitTime/Reports/GetByDayOfWeek.php
index d695ba5a7e..d9a3a58ee8 100644
--- a/plugins/VisitTime/Reports/GetByDayOfWeek.php
+++ b/plugins/VisitTime/Reports/GetByDayOfWeek.php
@@ -18,6 +18,8 @@ use Piwik\Site;
class GetByDayOfWeek extends Base
{
+ protected $defaultSortColumn = '';
+
protected function init()
{
parent::init();
diff --git a/plugins/VisitTime/Reports/GetVisitInformationPerLocalTime.php b/plugins/VisitTime/Reports/GetVisitInformationPerLocalTime.php
index bbc9aa5b1b..6546463cd1 100644
--- a/plugins/VisitTime/Reports/GetVisitInformationPerLocalTime.php
+++ b/plugins/VisitTime/Reports/GetVisitInformationPerLocalTime.php
@@ -16,6 +16,9 @@ use Piwik\Plugins\VisitTime\Columns\LocalTime;
class GetVisitInformationPerLocalTime extends Base
{
+
+ protected $defaultSortColumn = '';
+
protected function init()
{
parent::init();
diff --git a/plugins/VisitTime/Reports/GetVisitInformationPerServerTime.php b/plugins/VisitTime/Reports/GetVisitInformationPerServerTime.php
index ea8e1b9f7e..ce213fb211 100644
--- a/plugins/VisitTime/Reports/GetVisitInformationPerServerTime.php
+++ b/plugins/VisitTime/Reports/GetVisitInformationPerServerTime.php
@@ -15,6 +15,9 @@ use Piwik\Plugins\VisitTime\Columns\ServerTime;
class GetVisitInformationPerServerTime extends Base
{
+
+ protected $defaultSortColumn = '';
+
protected function init()
{
parent::init();
diff --git a/plugins/VisitorInterest/API.php b/plugins/VisitorInterest/API.php
index 33f6d97b58..74f050c33a 100644
--- a/plugins/VisitorInterest/API.php
+++ b/plugins/VisitorInterest/API.php
@@ -33,7 +33,7 @@ class API extends \Piwik\Plugin\API
public function getNumberOfVisitsPerVisitDuration($idSite, $period, $date, $segment = false)
{
$dataTable = $this->getDataTable(Archiver::TIME_SPENT_RECORD_NAME, $idSite, $period, $date, $segment);
- $dataTable->queueFilter('Sort', array('label', 'asc', true));
+ $dataTable->queueFilter('Sort', array('label', 'asc', true, false));
$dataTable->queueFilter('BeautifyTimeRangeLabels', array(
Piwik::translate('VisitorInterest_BetweenXYSeconds'),
Piwik::translate('VisitorInterest_OneMinute'),
@@ -44,7 +44,7 @@ class API extends \Piwik\Plugin\API
public function getNumberOfVisitsPerPage($idSite, $period, $date, $segment = false)
{
$dataTable = $this->getDataTable(Archiver::PAGES_VIEWED_RECORD_NAME, $idSite, $period, $date, $segment);
- $dataTable->queueFilter('Sort', array('label', 'asc', true));
+ $dataTable->queueFilter('Sort', array('label', 'asc', true, false));
$dataTable->queueFilter('BeautifyRangeLabels', array(
Piwik::translate('VisitorInterest_OnePage'),
Piwik::translate('VisitorInterest_NPages')));
diff --git a/plugins/VisitorInterest/Archiver.php b/plugins/VisitorInterest/Archiver.php
index cba45c6934..8d432a70b2 100644
--- a/plugins/VisitorInterest/Archiver.php
+++ b/plugins/VisitorInterest/Archiver.php
@@ -128,7 +128,15 @@ class Archiver extends \Piwik\Plugin\Archiver
self::VISITS_COUNT_RECORD_NAME,
self::DAYS_SINCE_LAST_RECORD_NAME
);
- $this->getProcessor()->aggregateDataTableRecords($dataTableRecords);
+ $columnsAggregationOperation = null;
+ $this->getProcessor()->aggregateDataTableRecords(
+ $dataTableRecords,
+ $maximumRowsInDataTableLevelZero = null,
+ $maximumRowsInSubDataTable = null,
+ $columnToSortByBeforeTruncation = null,
+ $columnsAggregationOperation,
+ $columnsToRenameAfterAggregation = null,
+ $countRowsRecursive = array());
}
/**
diff --git a/plugins/VisitorInterest/Reports/GetNumberOfVisitsByDaysSinceLast.php b/plugins/VisitorInterest/Reports/GetNumberOfVisitsByDaysSinceLast.php
index c5a2aa471b..fd935d47c6 100644
--- a/plugins/VisitorInterest/Reports/GetNumberOfVisitsByDaysSinceLast.php
+++ b/plugins/VisitorInterest/Reports/GetNumberOfVisitsByDaysSinceLast.php
@@ -14,6 +14,8 @@ use Piwik\Plugins\VisitorInterest\Columns\VisitsByDaysSinceLastVisit;
class GetNumberOfVisitsByDaysSinceLast extends Base
{
+ protected $defaultSortColumn = '';
+
protected function init()
{
parent::init();
diff --git a/plugins/VisitorInterest/Reports/GetNumberOfVisitsByVisitCount.php b/plugins/VisitorInterest/Reports/GetNumberOfVisitsByVisitCount.php
index 0cebfe2e2a..74dbf69c14 100644
--- a/plugins/VisitorInterest/Reports/GetNumberOfVisitsByVisitCount.php
+++ b/plugins/VisitorInterest/Reports/GetNumberOfVisitsByVisitCount.php
@@ -16,6 +16,8 @@ use Piwik\Plugins\CoreHome\Columns\Metrics\VisitsPercent;
class GetNumberOfVisitsByVisitCount extends Base
{
+ protected $defaultSortColumn = '';
+
protected function init()
{
parent::init();
diff --git a/plugins/VisitorInterest/Reports/GetNumberOfVisitsPerPage.php b/plugins/VisitorInterest/Reports/GetNumberOfVisitsPerPage.php
index 355b694406..de15ed5f5f 100644
--- a/plugins/VisitorInterest/Reports/GetNumberOfVisitsPerPage.php
+++ b/plugins/VisitorInterest/Reports/GetNumberOfVisitsPerPage.php
@@ -16,6 +16,8 @@ use Piwik\Plugins\VisitorInterest\Columns\PagesPerVisit;
class GetNumberOfVisitsPerPage extends Base
{
+ protected $defaultSortColumn = '';
+
protected function init()
{
parent::init();
diff --git a/plugins/VisitorInterest/Reports/GetNumberOfVisitsPerVisitDuration.php b/plugins/VisitorInterest/Reports/GetNumberOfVisitsPerVisitDuration.php
index f518ce47c2..ead1849b7f 100644
--- a/plugins/VisitorInterest/Reports/GetNumberOfVisitsPerVisitDuration.php
+++ b/plugins/VisitorInterest/Reports/GetNumberOfVisitsPerVisitDuration.php
@@ -16,6 +16,8 @@ use Piwik\Plugins\VisitorInterest\Columns\VisitDuration;
class GetNumberOfVisitsPerVisitDuration extends Base
{
+ protected $defaultSortColumn = '';
+
protected function init()
{
parent::init();